From 1b36237cae0f5e4014d32397799ef3fd393351b5 Mon Sep 17 00:00:00 2001 From: Marc Sommerhalder Date: Wed, 2 Oct 2024 12:46:12 +0200 Subject: [PATCH] PB-1044 Add interface for cognito --- .env.default | 9 + .gitignore | 3 + Pipfile | 2 + Pipfile.lock | 210 +++++++++++++----- README.md | 13 ++ app/cognito/__init__.py | 0 app/cognito/apps.py | 6 + app/cognito/management/__init__.py | 0 app/cognito/management/commands/__init__.py | 0 .../management/commands/cognito_sync.py | 109 +++++++++ app/cognito/tests/__init__.py | 0 app/cognito/tests/test_client.py | 64 ++++++ app/cognito/tests/test_sync_command.py | 103 +++++++++ app/cognito/tests/test_user_utils.py | 80 +++++++ app/cognito/utils/__init__.py | 0 app/cognito/utils/client.py | 53 +++++ app/cognito/utils/user.py | 58 +++++ app/config/settings_base.py | 7 +- app/utils/command.py | 81 +++++++ app/utils/tests/test_command.py | 192 ++++++++++++++++ docker-compose.yml | 17 ++ 21 files changed, 949 insertions(+), 58 deletions(-) create mode 100644 app/cognito/__init__.py create mode 100644 app/cognito/apps.py create mode 100644 app/cognito/management/__init__.py create mode 100644 app/cognito/management/commands/__init__.py create mode 100644 app/cognito/management/commands/cognito_sync.py create mode 100644 app/cognito/tests/__init__.py create mode 100644 app/cognito/tests/test_client.py create mode 100644 app/cognito/tests/test_sync_command.py create mode 100644 app/cognito/tests/test_user_utils.py create mode 100644 app/cognito/utils/__init__.py create mode 100644 app/cognito/utils/client.py create mode 100644 app/cognito/utils/user.py create mode 100644 app/utils/command.py create mode 100644 app/utils/tests/test_command.py diff --git a/.env.default b/.env.default index 0a46770..89e1b32 100644 --- a/.env.default +++ b/.env.default @@ -7,5 +7,14 @@ DB_HOST=localhost DB_NAME_TEST=postgres_test DJANGO_SETTINGS_MODULE=config.settings_dev SECRET_KEY=django-insecure-6-72r#zx=sv6v@-4k@uf1gv32me@%yr*oqa*fu8&5l&a!ws)5# +COGNITO_POOL_ID=local_PoolPrty +COGNITO_PORT=9229 +COGNITO_ENDPOINT_URL=http://localhost:9229 + +# used for local development +AWS_REGION=eu-central-2 +AWS_DEFAULT_REGION=eu-central-2 +AWS_ACCESS_KEY_ID=123 +AWS_SECRET_ACCESS_KEY=123 DEBUG=True diff --git a/.gitignore b/.gitignore index ff7bde8..e55f58c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ var/ # IDE config files .vscode + +# Local database +.volumes diff --git a/Pipfile b/Pipfile index 735f28c..b501d71 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ gunicorn = "~=23.0" pyyaml = "~=6.0.2" gevent = "~=24.2" logging-utilities = "~=4.4.1" +boto3 = "~=1.35" [dev-packages] yapf = "*" @@ -30,6 +31,7 @@ mypy = "*" types-gevent = "*" django-stubs = "~=5.0.4" debugpy = "*" +boto3-stubs = {extras = ["cognito-idp"], version = "~=1.35"} [requires] python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock index 6290538..eff0b65 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "acc173fb0cc3902d21a7fa6619dc68dc5e02061caa8a8fc15d270f6b7dbb437c" + "sha256": "7ca2e1e568b5f9ab47c733174e67f850d1eaf6cc12c2f670406741c7ccd93265" }, "pipfile-spec": 6, "requires": { @@ -32,6 +32,23 @@ "markers": "python_version >= '3.8'", "version": "==3.8.1" }, + "boto3": { + "hashes": [ + "sha256:385ca77bf8ea4ab2d97f6e2435bdb29f77d9301e2f7ac796c2f465753c2adf3c", + "sha256:470d981583885859fed2fd1c185eeb01cc03e60272d499bafe41b12625b158c8" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.35.37" + }, + "botocore": { + "hashes": [ + "sha256:64f965d4ba7adb8d79ce044c3aef7356e05dd74753cf7e9115b80f477845d920", + "sha256:b2b4d29bafd95b698344f2f0577bb67064adbf1735d8a0e3c7473daa59c23ba6" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.37" + }, "django": { "hashes": [ "sha256:6333870d342329b60174da3a60dbd302e533f3b0bb0971516750e974a99b5a39", @@ -195,6 +212,14 @@ "markers": "python_version >= '3.7'", "version": "==23.0.0" }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, "logging-utilities": { "hashes": [ "sha256:0e144647749fdb153a43cdede0b077b9bcfd50fc6a068e07716b1a002173e8ef", @@ -394,6 +419,14 @@ "markers": "python_version >= '3.8'", "version": "==2.23.4" }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, "pyyaml": { "hashes": [ "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", @@ -454,6 +487,14 @@ "markers": "python_version >= '3.8'", "version": "==6.0.2" }, + "s3transfer": { + "hashes": [ + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.3" + }, "setuptools": { "hashes": [ "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", @@ -462,6 +503,14 @@ "markers": "python_version >= '3.8'", "version": "==75.1.0" }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, "sqlparse": { "hashes": [ "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", @@ -478,6 +527,14 @@ "markers": "python_version < '3.13'", "version": "==4.12.2" }, + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "markers": "python_version >= '3.10'", + "version": "==2.2.3" + }, "zope.event": { "hashes": [ "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", @@ -488,43 +545,40 @@ }, "zope.interface": { "hashes": [ - "sha256:01e6e58078ad2799130c14a1d34ec89044ada0e1495329d72ee0407b9ae5100d", - "sha256:064ade95cb54c840647205987c7b557f75d2b2f7d1a84bfab4cf81822ef6e7d1", - "sha256:11fa1382c3efb34abf16becff8cb214b0b2e3144057c90611621f2d186b7e1b7", - "sha256:1bee1b722077d08721005e8da493ef3adf0b7908e0cd85cc7dc836ac117d6f32", - "sha256:1eeeb92cb7d95c45e726e3c1afe7707919370addae7ed14f614e22217a536958", - "sha256:21a207c6b2c58def5011768140861a73f5240f4f39800625072ba84e76c9da0b", - "sha256:2545d6d7aac425d528cd9bf0d9e55fcd47ab7fd15f41a64b1c4bf4c6b24946dc", - "sha256:2c4316a30e216f51acbd9fb318aa5af2e362b716596d82cbb92f9101c8f8d2e7", - "sha256:35062d93bc49bd9b191331c897a96155ffdad10744ab812485b6bad5b588d7e4", - "sha256:382d31d1e68877061daaa6499468e9eb38eb7625d4369b1615ac08d3860fe896", - "sha256:3aa8fcbb0d3c2be1bfd013a0f0acd636f6ed570c287743ae2bbd467ee967154d", - "sha256:3d4b91821305c8d8f6e6207639abcbdaf186db682e521af7855d0bea3047c8ca", - "sha256:3de1d553ce72868b77a7e9d598c9bff6d3816ad2b4cc81c04f9d8914603814f3", - "sha256:3fcdc76d0cde1c09c37b7c6b0f8beba2d857d8417b055d4f47df9c34ec518bdd", - "sha256:5112c530fa8aa2108a3196b9c2f078f5738c1c37cfc716970edc0df0414acda8", - "sha256:53d678bb1c3b784edbfb0adeebfeea6bf479f54da082854406a8f295d36f8386", - "sha256:6195c3c03fef9f87c0dbee0b3b6451df6e056322463cf35bca9a088e564a3c58", - "sha256:6d04b11ea47c9c369d66340dbe51e9031df2a0de97d68f442305ed7625ad6493", - "sha256:6dd647fcd765030638577fe6984284e0ebba1a1008244c8a38824be096e37fe3", - "sha256:799ef7a444aebbad5a145c3b34bff012b54453cddbde3332d47ca07225792ea4", - "sha256:7d92920416f31786bc1b2f34cc4fc4263a35a407425319572cbf96b51e835cd3", - "sha256:7e0c151a6c204f3830237c59ee4770cc346868a7a1af6925e5e38650141a7f05", - "sha256:84f8794bd59ca7d09d8fce43ae1b571be22f52748169d01a13d3ece8394d8b5b", - "sha256:95e5913ec718010dc0e7c215d79a9683b4990e7026828eedfda5268e74e73e11", - "sha256:9b9369671a20b8d039b8e5a1a33abd12e089e319a3383b4cc0bf5c67bd05fe7b", - "sha256:ab985c566a99cc5f73bc2741d93f1ed24a2cc9da3890144d37b9582965aff996", - "sha256:af94e429f9d57b36e71ef4e6865182090648aada0cb2d397ae2b3f7fc478493a", - "sha256:c96b3e6b0d4f6ddfec4e947130ec30bd2c7b19db6aa633777e46c8eecf1d6afd", - "sha256:cd2690d4b08ec9eaf47a85914fe513062b20da78d10d6d789a792c0b20307fb1", - "sha256:d3b7ce6d46fb0e60897d62d1ff370790ce50a57d40a651db91a3dde74f73b738", - "sha256:d976fa7b5faf5396eb18ce6c132c98e05504b52b60784e3401f4ef0b2e66709b", - "sha256:db6237e8fa91ea4f34d7e2d16d74741187e9105a63bbb5686c61fea04cdbacca", - "sha256:ecd32f30f40bfd8511b17666895831a51b532e93fc106bfa97f366589d3e4e0e", - "sha256:f418c88f09c3ba159b95a9d1cfcdbe58f208443abb1f3109f4b9b12fd60b187c" - ], - "markers": "python_version >= '3.8'", - "version": "==7.0.3" + "sha256:07add15de0cc7e69917f7d286b64d54125c950aeb43efed7a5ea7172f000fbc1", + "sha256:0ac20581fc6cd7c754f6dff0ae06fedb060fa0e9ea6309d8be8b2701d9ea51c4", + "sha256:124149e2d42067b9c6597f4dafdc7a0983d0163868f897b7bb5dc850b14f9a87", + "sha256:27cfb5205d68b12682b6e55ab8424662d96e8ead19550aad0796b08dd2c9a45e", + "sha256:2a29ac607e970b5576547f0e3589ec156e04de17af42839eedcf478450687317", + "sha256:2b6a4924f5bad9fe21d99f66a07da60d75696a136162427951ec3cb223a5570d", + "sha256:2bd9e9f366a5df08ebbdc159f8224904c1c5ce63893984abb76954e6fbe4381a", + "sha256:3bcff5c09d0215f42ba64b49205a278e44413d9bf9fa688fd9e42bfe472b5f4f", + "sha256:3f005869a1a05e368965adb2075f97f8ee9a26c61898a9e52a9764d93774f237", + "sha256:4a00ead2e24c76436e1b457a5132d87f83858330f6c923640b7ef82d668525d1", + "sha256:4af4a12b459a273b0b34679a5c3dc5e34c1847c3dd14a628aa0668e19e638ea2", + "sha256:5501e772aff595e3c54266bc1bfc5858e8f38974ce413a8f1044aae0f32a83a3", + "sha256:5e28ea0bc4b084fc93a483877653a033062435317082cdc6388dec3438309faf", + "sha256:5fcf379b875c610b5a41bc8a891841533f98de0520287d7f85e25386cd10d3e9", + "sha256:6159e767d224d8f18deff634a1d3722e68d27488c357f62ebeb5f3e2f5288b1f", + "sha256:661d5df403cd3c5b8699ac480fa7f58047a3253b029db690efa0c3cf209993ef", + "sha256:80a3c00b35f6170be5454b45abe2719ea65919a2f09e8a6e7b1362312a872cd3", + "sha256:848b6fa92d7c8143646e64124ed46818a0049a24ecc517958c520081fd147685", + "sha256:9733a9a0f94ef53d7aa64661811b20875b5bc6039034c6e42fb9732170130573", + "sha256:9940d5bc441f887c5f375ec62bcf7e7e495a2d5b1da97de1184a88fb567f06af", + "sha256:9e3e48f3dea21c147e1b10c132016cb79af1159facca9736d231694ef5a740a8", + "sha256:a14c9decf0eb61e0892631271d500c1e306c7b6901c998c7035e194d9150fdd1", + "sha256:a735f82d2e3ed47ca01a20dfc4c779b966b16352650a8036ab3955aad151ed8a", + "sha256:a99240b1d02dc469f6afbe7da1bf617645e60290c272968f4e53feec18d7dce8", + "sha256:b7b25db127db3e6b597c5f74af60309c4ad65acd826f89609662f0dc33a54728", + "sha256:b936d61dbe29572fd2cfe13e30b925e5383bed1aba867692670f5a2a2eb7b4e9", + "sha256:bec001798ab62c3fc5447162bf48496ae9fba02edc295a9e10a0b0c639a6452e", + "sha256:e53c291debef523b09e1fe3dffe5f35dde164f1c603d77f770b88a1da34b7ed6", + "sha256:ec59fe53db7d32abb96c6d4efeed84aab4a7c38c62d7a901a9b20c09dd936e7a", + "sha256:f245d039f72e6f802902375755846f5de1ee1e14c3e8736c078565599bcab621", + "sha256:ff115ef91c0eeac69cd92daeba36a9d8e14daee445b504eeea2b1c0b55821984" + ], + "markers": "python_version >= '3.8'", + "version": "==7.1.0" } }, "develop": { @@ -538,11 +592,11 @@ }, "astroid": { "hashes": [ - "sha256:5eba185467253501b62a9f113c263524b4f5d55e1b30456370eed4cdbd6438fd", - "sha256:e73d0b62dd680a7c07cb2cd0ce3c22570b044dd01bd994bc3a2dd16c6cbba162" + "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d", + "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8" ], "markers": "python_full_version >= '3.9.0'", - "version": "==3.3.4" + "version": "==3.3.5" }, "asttokens": { "hashes": [ @@ -551,6 +605,25 @@ ], "version": "==2.4.1" }, + "boto3-stubs": { + "extras": [ + "cognito-idp" + ], + "hashes": [ + "sha256:32b70602af1c34ddd244bbdafcc7f32b9824135c2f14dcf41d8e5a33b914b19a", + "sha256:dc12838481a10d65802f75b4ca1e4eb334e06dbb2f0d5c565f43e0363c2037d6" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.37" + }, + "botocore-stubs": { + "hashes": [ + "sha256:0fd4ce53636196fcb72b8dad1c67cb774f2f1941891a9c5293f91401ff6d8589", + "sha256:834f1d55c8e8a815bbb446fe9a7d3da09c4402312ff1a8fcf13fb6b4b894ab92" + ], + "markers": "python_version >= '3.8'", + "version": "==1.35.37" + }, "debugpy": { "hashes": [ "sha256:0a85707c6a84b0c5b3db92a2df685b5230dd8fb8c108298ba4f11dba157a615a", @@ -590,12 +663,12 @@ }, "dill": { "hashes": [ - "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", - "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7" + "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a", + "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.3.8" + "version": "==0.3.9" }, "django": { "hashes": [ @@ -660,12 +733,12 @@ }, "faker": { "hashes": [ - "sha256:32d0ee7d42925ff06e4a7d906ee7efbf34f5052a41a2a1eb8bb174a422a5498f", - "sha256:34e89aec594cad9773431ca479ee95c7ce03dd9f22fda2524e2373b880a2fa77" + "sha256:8760fbb34564fbb2f394345eef24aec5b8f6506b6cfcefe8195ed66dd1032bdb", + "sha256:e8a15fd1b0f72992b008f5ea94c70d3baa0cb51b0d5a0e899c17b1d1b23d2771" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==29.0.0" + "version": "==30.3.0" }, "importlib-metadata": { "hashes": [ @@ -694,11 +767,11 @@ }, "ipython": { "hashes": [ - "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e", - "sha256:f68b3cb8bde357a5d7adc9598d57e22a45dfbea19eb6b98286fa3b288c9cd55c" + "sha256:0d0d15ca1e01faeb868ef56bc7ee5a0de5bd66885735682e8a322ae289a13d1a", + "sha256:530ef1e7bb693724d3cdc37287c80b07ad9b25986c007a53aa1857272dac3f35" ], "markers": "python_version >= '3.11'", - "version": "==8.27.0" + "version": "==8.28.0" }, "isort": { "hashes": [ @@ -767,6 +840,13 @@ "markers": "python_version >= '3.8'", "version": "==1.11.2" }, + "mypy-boto3-cognito-idp": { + "hashes": [ + "sha256:4ad98e0e89ad4ddbed8d82f9e1e3500565ae54ba33301faca30a5929e1ac617d", + "sha256:f129a0879bfa3012e256adcdbeca3bf4f62a1ded41b157b7623b170ce59efd04" + ], + "version": "==1.35.18" + }, "mypy-extensions": { "hashes": [ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", @@ -922,11 +1002,11 @@ }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.1" + "markers": "python_version >= '3.8'", + "version": "==2.0.2" }, "tomlkit": { "hashes": [ @@ -944,14 +1024,22 @@ "markers": "python_version >= '3.8'", "version": "==5.14.3" }, + "types-awscrt": { + "hashes": [ + "sha256:67a660c90bad360c339f6a79310cc17094d12472042c7ca5a41450aaf5fc9a54", + "sha256:b2c196bbd3226bab42d80fae13c34548de9ddc195f5a366d79c15d18e5897aa9" + ], + "markers": "python_version >= '3.8'", + "version": "==0.22.0" + }, "types-gevent": { "hashes": [ - "sha256:0cab6b787fb3d5e05d7e1d1337658006a28a47b1c3e2f7dc776056d0aebc1424", - "sha256:f1ac35c63e5e79b4a0fa1f001ce18f19f9c5d36f1f42030aa61bf66d4b6ed6fb" + "sha256:368dc2dee833446946ddba90c297198b3c0519eecec8f4021afebd80a2ce12fd", + "sha256:ab11490ddd06bba20a6ad0cd79cc50efb342ae5676a9650eaac668abb94f7188" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.2.0.20240806" + "version": "==24.2.0.20241003" }, "types-greenlet": { "hashes": [ @@ -977,6 +1065,14 @@ "markers": "python_version >= '3.8'", "version": "==6.0.12.20240917" }, + "types-s3transfer": { + "hashes": [ + "sha256:d34c5a82f531af95bb550927136ff5b737a1ed3087f90a59d545591dfde5b4cc", + "sha256:f761b2876ac4c208e6c6b75cdf5f6939009768be9950c545b11b0225e7703ee7" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.3" + }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", diff --git a/README.md b/README.md index 332e1ff..60c7683 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,19 @@ You may want to do an initial sync of your database by applying the most recent app/manage.py migrate ``` +## Local Cognito + +For local testing the connection to Amazon Cognito user identity and access management, +[cognito-local](https://github.com/jagregory/cognito-local) is used. `cognito-local` stores all of +its data as simple JSON files in its volume (`.volumes/cognito/db/`). + +You can also use the AWS CLI together with `cognito-loca` by specifying the local endpoint, +for example: + +```bash +aws --endpoint http://localhost:9229 cognito-idp list-user-pools --max-results 100 +``` + ## Local Development ### vs code Integration diff --git a/app/cognito/__init__.py b/app/cognito/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/cognito/apps.py b/app/cognito/apps.py new file mode 100644 index 0000000..1ce5ed7 --- /dev/null +++ b/app/cognito/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CognitoConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'cognito' diff --git a/app/cognito/management/__init__.py b/app/cognito/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/cognito/management/commands/__init__.py b/app/cognito/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/cognito/management/commands/cognito_sync.py b/app/cognito/management/commands/cognito_sync.py new file mode 100644 index 0000000..2ea9585 --- /dev/null +++ b/app/cognito/management/commands/cognito_sync.py @@ -0,0 +1,109 @@ +from typing import Any + +from cognito.utils.client import Client +from cognito.utils.user import User +from mypy_boto3_cognito_idp.type_defs import UserTypeTypeDef +from utils.command import CommandHandler +from utils.command import CustomBaseCommand + +from django.core.management.base import CommandParser + + +def get_local_users() -> list[User]: + # TODO: remove me! + return [] + + +class Handler(CommandHandler): + + def __init__(self, command: CustomBaseCommand, options: dict[str, Any]) -> None: + super().__init__(command, options) + self.clear = options['clear'] + self.dry_run = options['dry_run'] + self.client = Client() + self.counts = {'added': 0, 'deleted': 0, 'updated': 0} + + def attributes_to_dict(self, user: UserTypeTypeDef) -> dict[str, str]: + """ Converts the attributes from a cognito uses to a dict. """ + return {attribute['Name']: attribute['Value'] for attribute in user.get('Attributes', {})} + + def clear_users(self) -> None: + """ Remove all existing cognito users. """ + + for user in self.client.get_users(): + self.counts['deleted'] += 1 + username = user['Username'] + self.print(f'deleting user {username}') + if not self.dry_run: + self.client.delete_user(username) + + def sync_users(self) -> None: + """ Synchronizes local and cognito users. """ + + # Get all remote and local users + local = {str(user.id): user for user in get_local_users()} + remote = {user['Username']: user for user in self.client.get_users()} + + # Add local only user + for username in set(local.keys()).difference(set(remote.keys())): + self.counts['added'] += 1 + self.print(f'adding user {username}') + if not self.dry_run: + self.client.create_user(username, local[username].email) + + # Remove remote only user + for username in set(remote.keys()).difference(set(local.keys())): + self.counts['deleted'] += 1 + self.print(f'deleting user {username}') + if not self.dry_run: + self.client.delete_user(username) + + # Update common users + for username in set(local.keys()).intersection(set(remote.keys())): + if local[username].email != self.attributes_to_dict(remote[username]).get('email'): + self.counts['updated'] += 1 + self.print(f'updating user {username}') + if not self.dry_run: + self.client.update_user(username, local[username].email) + + def run(self) -> None: + """ Main entry point of command. """ + + # Clear data + if self.clear: + self.clear_users() + + # Sync data + self.sync_users() + + # Print counts + printed = False + for operation, count in self.counts.items(): + if count: + printed = True + self.print_success(f'{count} user(s) {operation}') + if not printed: + self.print_success('nothing to be done') + + if self.dry_run: + self.print_warning('dry run, nothing has been done') + + +class Command(CustomBaseCommand): + help = "Synchronizes local users with cognito" + + def add_arguments(self, parser: CommandParser) -> None: + super().add_arguments(parser) + parser.add_argument( + '--clear', + action='store_true', + help='Delete existing users in cognito before synchronizing', + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Dry run, abort transaction in the end', + ) + + def handle(self, *args: Any, **options: Any) -> None: + Handler(self, options).run() diff --git a/app/cognito/tests/__init__.py b/app/cognito/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/cognito/tests/test_client.py b/app/cognito/tests/test_client.py new file mode 100644 index 0000000..71f208c --- /dev/null +++ b/app/cognito/tests/test_client.py @@ -0,0 +1,64 @@ +from unittest.mock import call +from unittest.mock import patch + +from cognito.utils.client import Client + +from django.test import TestCase + + +class ClientTestCase(TestCase): + + @patch('cognito.utils.client.client') + def test_get_users(self, boto3): + client = Client() + client.get_users() + self.assertIn(call().list_users(UserPoolId=client.user_pool_id), boto3.mock_calls) + + @patch('cognito.utils.client.client') + def test_get_user(self, boto3): + client = Client() + client.get_user('1234') + self.assertIn( + call().admin_get_user(UserPoolId=client.user_pool_id, Username='1234'), + boto3.mock_calls + ) + + @patch('cognito.utils.client.client') + def test_create_user(self, boto3): + client = Client() + client.create_user('1234', 'test@example.org') + self.assertIn( + call().admin_create_user( + UserPoolId=client.user_pool_id, + Username='1234', + UserAttributes=[{ + 'Name': 'email', 'Value': 'test@example.org' + }], + DesiredDeliveryMediums=['EMAIL'] + ), + boto3.mock_calls + ) + + @patch('cognito.utils.client.client') + def test_delete_user(self, boto3): + client = Client() + client.delete_user('1234') + self.assertIn( + call().admin_delete_user(UserPoolId=client.user_pool_id, Username='1234'), + boto3.mock_calls + ) + + @patch('cognito.utils.client.client') + def test_update_user(self, boto3): + client = Client() + client.update_user('1234', 'test@example.org') + self.assertIn( + call().admin_update_user_attributes( + UserPoolId=client.user_pool_id, + Username='1234', + UserAttributes=[{ + 'Name': 'email', 'Value': 'test@example.org' + }] + ), + boto3.mock_calls + ) diff --git a/app/cognito/tests/test_sync_command.py b/app/cognito/tests/test_sync_command.py new file mode 100644 index 0000000..88b332d --- /dev/null +++ b/app/cognito/tests/test_sync_command.py @@ -0,0 +1,103 @@ +from io import StringIO +from unittest.mock import call +from unittest.mock import patch + +from django.core.management import call_command +from django.test import TestCase + + +class DummyUser: + + def __init__(self, id_, email): + self.id = id_ + self.email = email + + +def cognito_user(username, email): + return {'Username': username, 'Attributes': [{'Name': 'email', 'Value': email}]} + + +class CognitoSyncCommandTest(TestCase): + + @patch('cognito.management.commands.cognito_sync.get_local_users') + @patch('cognito.management.commands.cognito_sync.Client') + def test_command_adds(self, client, users): + users.return_value = [DummyUser(1, '1@example.org')] + client.return_value.get_users.return_value = [] + + out = StringIO() + call_command('cognito_sync', verbosity=2, stdout=out) + + self.assertIn('adding user 1', out.getvalue()) + self.assertIn('1 user(s) added', out.getvalue()) + self.assertIn(call().create_user('1', '1@example.org'), client.mock_calls) + + @patch('cognito.management.commands.cognito_sync.get_local_users') + @patch('cognito.management.commands.cognito_sync.Client') + def test_command_removes(self, client, users): + users.return_value = [] + client.return_value.get_users.return_value = [cognito_user('1', '1@example.org')] + + out = StringIO() + call_command('cognito_sync', verbosity=2, stdout=out) + + self.assertIn('deleting user 1', out.getvalue()) + self.assertIn('1 user(s) deleted', out.getvalue()) + self.assertIn(call().delete_user('1'), client.mock_calls) + + @patch('cognito.management.commands.cognito_sync.get_local_users') + @patch('cognito.management.commands.cognito_sync.Client') + def test_command_updates(self, client, users): + users.return_value = [DummyUser(1, '1@example.org')] + client.return_value.get_users.return_value = [cognito_user('1', '2@example.org')] + + out = StringIO() + call_command('cognito_sync', verbosity=2, stdout=out) + + self.assertIn('updating user 1', out.getvalue()) + self.assertIn('1 user(s) updated', out.getvalue()) + self.assertIn(call().update_user('1', '1@example.org'), client.mock_calls) + + @patch('cognito.management.commands.cognito_sync.get_local_users') + @patch('cognito.management.commands.cognito_sync.Client') + def test_command_does_not_updates_if_unchanged(self, client, users): + users.return_value = [DummyUser(1, '1@example.org')] + client.return_value.get_users.return_value = [cognito_user('1', '1@example.org')] + + out = StringIO() + call_command('cognito_sync', verbosity=2, stdout=out) + + self.assertIn('nothing to be done', out.getvalue()) + + @patch('cognito.management.commands.cognito_sync.get_local_users') + @patch('cognito.management.commands.cognito_sync.Client') + def test_command_clears(self, client, users): + users.return_value = [DummyUser(1, '1@example.org')] + client.return_value.get_users.side_effect = [[cognito_user('1', '1@example.org')], []] + + out = StringIO() + call_command('cognito_sync', clear=True, verbosity=2, stdout=out) + + self.assertIn('deleting user 1', out.getvalue()) + self.assertIn('1 user(s) deleted', out.getvalue()) + self.assertIn('adding user 1', out.getvalue()) + self.assertIn('1 user(s) added', out.getvalue()) + self.assertIn(call().delete_user('1'), client.mock_calls) + self.assertIn(call().create_user('1', '1@example.org'), client.mock_calls) + + @patch('cognito.management.commands.cognito_sync.get_local_users') + @patch('cognito.management.commands.cognito_sync.Client') + def test_command_runs_dry(self, client, users): + users.return_value = [DummyUser(1, '1@example.org'), DummyUser(2, '2@example.org')] + client.return_value.get_users.return_value = [ + cognito_user('1', '10@example.org'), cognito_user('3', '3@example.org') + ] + + out = StringIO() + call_command('cognito_sync', dry_run=True, verbosity=2, stdout=out) + + self.assertIn('adding user 2', out.getvalue()) + self.assertIn('deleting user 3', out.getvalue()) + self.assertIn('updating user 1', out.getvalue()) + self.assertIn('dry run', out.getvalue()) + self.assertEqual([call(), call().get_users()], client.mock_calls) diff --git a/app/cognito/tests/test_user_utils.py b/app/cognito/tests/test_user_utils.py new file mode 100644 index 0000000..8d3c0f9 --- /dev/null +++ b/app/cognito/tests/test_user_utils.py @@ -0,0 +1,80 @@ +from unittest.mock import call +from unittest.mock import patch + +from cognito.utils.user import add_user +from cognito.utils.user import delete_user +from cognito.utils.user import update_user + +from django.test import TestCase + + +class DummyUser: + + def __init__(self, id_, email): + self.id = id_ + self.email = email + + +class ClientTestCase(TestCase): + + @patch('cognito.utils.user.Client') + @patch('cognito.utils.user.logger') + def test_add_user_adds_user(self, logger, client): + client.return_value.get_user.return_value = None + + add_user(DummyUser(123, 'test@example.org')) + + self.assertIn(call().create_user('123', 'test@example.org'), client.mock_calls) + self.assertIn(call.info('User %s created', '123'), logger.mock_calls) + + @patch('cognito.utils.user.Client') + @patch('cognito.utils.user.logger') + def test_add_user_updates_existing_user(self, logger, client): + client.return_value.get_user.return_value = {'Username': '123'} + + add_user(DummyUser(123, 'test@example.org')) + + self.assertIn(call().update_user('123', 'test@example.org'), client.mock_calls) + self.assertIn(call.info('User %s created', '123'), logger.mock_calls) + self.assertIn(call.warning('User %s already exists, updating', '123'), logger.mock_calls) + + @patch('cognito.utils.user.Client') + @patch('cognito.utils.user.logger') + def test_delete_user_deletes_user(self, logger, client): + client.return_value.get_user.return_value = {'Username': '123'} + + delete_user(DummyUser(123, 'test@example.org')) + + self.assertIn(call().delete_user('123'), client.mock_calls) + self.assertIn(call.info('User %s deleted', '123'), logger.mock_calls) + + @patch('cognito.utils.user.Client') + @patch('cognito.utils.user.logger') + def test_delete_when_user_not_exists(self, logger, client): + client.return_value.get_user.return_value = None + + delete_user(DummyUser(123, 'test@example.org')) + + self.assertIn(call.info('User %s deleted', '123'), logger.mock_calls) + self.assertIn(call.warning('User %s does not exist, ignoring', '123'), logger.mock_calls) + + @patch('cognito.utils.user.Client') + @patch('cognito.utils.user.logger') + def test_update_user_updates_user(self, logger, client): + client.return_value.get_user.return_value = {'Username': '123'} + + update_user(DummyUser(123, 'test@example.org')) + + self.assertIn(call().update_user('123', 'test@example.org'), client.mock_calls) + self.assertIn(call.info('User %s updated', '123'), logger.mock_calls) + + @patch('cognito.utils.user.Client') + @patch('cognito.utils.user.logger') + def test_update_user_adds_non_existing_user(self, logger, client): + client.return_value.get_user.return_value = None + + update_user(DummyUser(123, 'test@example.org')) + + self.assertNotIn(call().add_user('123', 'test@example.org'), client.mock_calls) + self.assertIn(call.info('User %s updated', '123'), logger.mock_calls) + self.assertIn(call.warning('User %s does not exist, adding', '123'), logger.mock_calls) diff --git a/app/cognito/utils/__init__.py b/app/cognito/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/cognito/utils/client.py b/app/cognito/utils/client.py new file mode 100644 index 0000000..6d4e5ea --- /dev/null +++ b/app/cognito/utils/client.py @@ -0,0 +1,53 @@ +from boto3 import client +from mypy_boto3_cognito_idp.type_defs import AdminGetUserResponseTypeDef +from mypy_boto3_cognito_idp.type_defs import UserTypeTypeDef + +from django.conf import settings + + +class Client: + """ A low level client for managing cognito users. """ + + def __init__(self) -> None: + self.endpoint_url = settings.COGNITO_ENDPOINT_URL + self.user_pool_id = settings.COGNITO_POOL_ID + self.client = client("cognito-idp", endpoint_url=self.endpoint_url) + + def get_users(self) -> list[UserTypeTypeDef]: + response = self.client.list_users(UserPoolId=self.user_pool_id) + return response['Users'] + + def get_user(self, username: str) -> AdminGetUserResponseTypeDef | None: + try: + response = self.client.admin_get_user(UserPoolId=self.user_pool_id, Username=username) + except self.client.exceptions.UserNotFoundException: + return None + + return response + + def create_user(self, username: str, email: str) -> UserTypeTypeDef: + response = self.client.admin_create_user( + UserPoolId=self.user_pool_id, + Username=username, + UserAttributes=[ + { + "Name": "email", "Value": email + }, + ], + DesiredDeliveryMediums=['EMAIL'] + ) + return response['User'] + + def delete_user(self, username: str) -> None: + self.client.admin_delete_user(UserPoolId=self.user_pool_id, Username=username) + + def update_user(self, username: str, email: str) -> None: + self.client.admin_update_user_attributes( + UserPoolId=self.user_pool_id, + Username=username, + UserAttributes=[ + { + "Name": "email", "Value": email + }, + ], + ) diff --git a/app/cognito/utils/user.py b/app/cognito/utils/user.py new file mode 100644 index 0000000..add2452 --- /dev/null +++ b/app/cognito/utils/user.py @@ -0,0 +1,58 @@ +from logging import getLogger + +from cognito.utils.client import Client + +logger = getLogger(__name__) + + +# TODO: Replace me with the actual user model +class User: + id: int + email: str + + +def add_user(user: User) -> None: + """ Add the given user to cognito. + + Update the user, if he already exists. + """ + + client = Client() + username = str(user.id) + existing = client.get_user(username) + if existing is not None: + logger.warning("User %s already exists, updating", username) + client.update_user(username, user.email) + else: + client.create_user(username, user.email) + logger.info("User %s created", username) + + +def delete_user(user: User) -> None: + """ Delete the given user from cognito. """ + + client = Client() + username = str(user.id) + existing = client.get_user(username) + if existing is not None: + client.delete_user(username) + else: + logger.warning("User %s does not exist, ignoring", username) + logger.info("User %s deleted", username) + + +def update_user(user: User) -> None: + """ Update the given user in cognito. + + Add the user, if he not already exists. + """ + + client = Client() + username = str(user.id) + existing = client.get_user(username) + if existing is not None: + client.update_user(username, user.email) + else: + logger.warning("User %s does not exist, adding", username) + client.create_user(username, user.email) + logger.info("User %s updated", username) diff --git a/app/config/settings_base.py b/app/config/settings_base.py index ddd9628..b2c8a17 100644 --- a/app/config/settings_base.py +++ b/app/config/settings_base.py @@ -40,7 +40,8 @@ 'django.contrib.staticfiles', 'django.contrib.messages', 'provider', - 'distributions' + 'distributions', + 'cognito' ] MIDDLEWARE = [ @@ -128,3 +129,7 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Cognito +COGNITO_ENDPOINT_URL = env.str('COGNITO_ENDPOINT_URL', 'http://localhost:9229') +COGNITO_POOL_ID = env.str('COGNITO_POOL_ID', 'local') diff --git a/app/utils/command.py b/app/utils/command.py new file mode 100644 index 0000000..77ad30e --- /dev/null +++ b/app/utils/command.py @@ -0,0 +1,81 @@ +import inspect +import logging +from typing import Any + +from django.core.management.base import BaseCommand +from django.core.management.base import CommandParser + + +class CustomBaseCommand(BaseCommand): + + def handle(self, *args: Any, **options: Any) -> None: + """ + The actual logic of the command. Subclasses must implement + this method. + """ + raise NotImplementedError("subclasses of CustomBaseCommand must provide a handle() method") + + def add_arguments(self, parser: CommandParser) -> None: + parser.add_argument('--logger', action='store_true', help='use logger configuration') + + +class CommandHandler(): + """Base class for management command handler + + This class add proper support for printing to the console for management command + """ + + def __init__(self, command: CustomBaseCommand, options: dict['str', Any]): + frm = inspect.stack()[1] + mod = inspect.getmodule(frm[0]) + self.logger = logging.getLogger(mod.__name__ if mod else None) + self.options = options + self.verbosity = options['verbosity'] + self.use_logger = options.get('logger') + self.stdout = command.stdout + self.stderr = command.stderr + self.style = command.style + self.command = command + + def print(self, message: str, *args: Any, level: int = 2, **kwargs: Any) -> None: + if self.verbosity >= level: + if self.use_logger: + self.logger.info(message, *args, **kwargs) + else: + if len(kwargs) > 0: + message = message + " " + ", ".join( + f"{key}={value}" for key, value in kwargs.items() + ) + self.stdout.write(message % (args)) + + def print_warning(self, message: str, *args: Any, level: int = 1, **kwargs: Any) -> None: + if self.verbosity >= level: + if self.use_logger: + self.logger.warning(self.style.WARNING(message % (args)), **kwargs) + else: + if len(kwargs) > 0: + message = message + " " + ", ".join( + f"{key}={value}" for key, value in kwargs.items() + ) + self.stdout.write(self.style.WARNING(message % (args))) + + def print_success(self, message: str, *args: Any, level: int = 1, **kwargs: Any) -> None: + if self.verbosity >= level: + if self.use_logger: + self.logger.info(self.style.SUCCESS(message % (args)), **kwargs) + else: + if len(kwargs) > 0: + message = message + " " + ", ".join( + f"{key}={value}" for key, value in kwargs.items() + ) + self.stdout.write(self.style.SUCCESS(message % (args))) + + def print_error(self, message: str, *args: Any, **kwargs: Any) -> None: + if self.use_logger: + self.logger.error(self.style.ERROR(message % (args)), **kwargs) + else: + if len(kwargs) > 0: + message = message + "\n" + ", ".join( + f"{key}={value}" for key, value in kwargs.items() + ) + self.stderr.write(self.style.ERROR(message % (args))) diff --git a/app/utils/tests/test_command.py b/app/utils/tests/test_command.py new file mode 100644 index 0000000..54d6fea --- /dev/null +++ b/app/utils/tests/test_command.py @@ -0,0 +1,192 @@ +from io import StringIO +from unittest.mock import MagicMock +from unittest.mock import call + +from utils.command import CommandHandler +from utils.command import CustomBaseCommand + +from django.core.management import call_command +from django.test import TestCase + + +class Handler(CommandHandler): + + def __init__(self, command, options): + super().__init__(command, options) + self.logger = MagicMock() + + def run(self): + # level + self.print("Print default") + self.print("Print 0", level=0) + self.print("Print 1", level=1) + self.print("Print 2", level=2) + self.print_success("Success default") + self.print_success("Success 0", level=0) + self.print_success("Success 1", level=1) + self.print_success("Success 2", level=2) + self.print_warning("Warning default") + self.print_warning("Warning 0", level=0) + self.print_warning("Warning 1", level=1) + self.print_warning("Warning 2", level=2) + self.print_error("Error") + + # args and kwargs + self.print("Print %s", "JohnDoe") + self.print("Print", extra={"n": "JohnDoe"}) + self.print("Print %s", "John", extra={"n": "Doe"}) + self.print_success("Success %s", "JohnDoe") + self.print_success("Success", extra={"n": "JohnDoe"}) + self.print_success("Success %s", "John", extra={"n": "Doe"}) + self.print_warning("Warning %s", "JohnDoe") + self.print_warning("Warning", extra={"n": "JohnDoe"}) + self.print_warning("Warning %s", "John", extra={"n": "Doe"}) + self.print_error("Error %s", "JohnDoe") + self.print_error("Error", extra={"n": "JohnDoe"}) + self.print_error("Error %s", "John", extra={"n": "Doe"}) + + +class Command(CustomBaseCommand): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.handler = None + + def handle(self, *args, **options): + self.handler = Handler(self, options) + self.handler.run() + + +class CustomCommandTest(TestCase): + + def test_print_to_stdout_default_verbosity(self): + # no verbosity given = 1 + out = StringIO() + err = StringIO() + call_command(Command(), stdout=out, stderr=err) + self.assertNotIn("Print default", out.getvalue()) + self.assertIn("Print 0", out.getvalue()) + self.assertIn("Print 1", out.getvalue()) + self.assertNotIn("Print 2", out.getvalue()) + self.assertIn("Success default", out.getvalue()) + self.assertIn("Success 0", out.getvalue()) + self.assertIn("Success 1", out.getvalue()) + self.assertNotIn("Success 2", out.getvalue()) + self.assertIn("Warning default", out.getvalue()) + self.assertIn("Warning 0", out.getvalue()) + self.assertIn("Warning 1", out.getvalue()) + self.assertNotIn("Warning 2", out.getvalue()) + self.assertIn("Error", err.getvalue()) + + def test_print_to_stdout_verbosity_0(self): + out = StringIO() + err = StringIO() + call_command(Command(), verbosity=0, stdout=out, stderr=err) + self.assertIn("Print 0", out.getvalue()) + self.assertNotIn("Print 1", out.getvalue()) + self.assertNotIn("Print 2", out.getvalue()) + self.assertIn("Success 0", out.getvalue()) + self.assertNotIn("Success 1", out.getvalue()) + self.assertNotIn("Success 2", out.getvalue()) + self.assertIn("Warning 0", out.getvalue()) + self.assertNotIn("Warning 1", out.getvalue()) + self.assertNotIn("Warning 2", out.getvalue()) + self.assertIn("Error", err.getvalue()) + + def test_print_to_stdout_verbosity_3(self): + out = StringIO() + err = StringIO() + call_command(Command(), verbosity=3, stdout=out, stderr=err) + self.assertIn("Print 0", out.getvalue()) + self.assertIn("Print 1", out.getvalue()) + self.assertIn("Print 2", out.getvalue()) + self.assertIn("Success 0", out.getvalue()) + self.assertIn("Success 1", out.getvalue()) + self.assertIn("Success 2", out.getvalue()) + self.assertIn("Warning 0", out.getvalue()) + self.assertIn("Warning 1", out.getvalue()) + self.assertIn("Warning 2", out.getvalue()) + self.assertIn("Error", err.getvalue()) + + def test_print_to_stdout_args_kwargs(self): + out = StringIO() + err = StringIO() + call_command(Command(), verbosity=3, stdout=out, stderr=err) + self.assertIn("Print JohnDoe", out.getvalue()) + self.assertIn("Print extra={'n': 'JohnDoe'}", out.getvalue()) + self.assertIn("Print John extra={'n': 'Doe'}", out.getvalue()) + self.assertIn("Success JohnDoe", out.getvalue()) + self.assertIn("Success extra={'n': 'JohnDoe'}", out.getvalue()) + self.assertIn("Success John extra={'n': 'Doe'}", out.getvalue()) + self.assertIn("Warning JohnDoe", out.getvalue()) + self.assertIn("Warning extra={'n': 'JohnDoe'}", out.getvalue()) + self.assertIn("Warning John extra={'n': 'Doe'}", out.getvalue()) + self.assertIn("Error JohnDoe", err.getvalue()) + self.assertIn("Error\nextra={'n': 'JohnDoe'}", err.getvalue()) + self.assertIn("Error John\nextra={'n': 'Doe'}", err.getvalue()) + + def test_print_to_log_default_verbosity(self): + # no verbosity given = 1 + command = Command() + call_command(command, logger=True) + calls = command.handler.logger.mock_calls + self.assertNotIn(call.info("Print default"), calls) + self.assertIn(call.info("Print 0"), calls) + self.assertIn(call.info("Print 1"), calls) + self.assertNotIn(call.info("Print 2"), calls) + self.assertIn(call.info("Success default"), calls) + self.assertIn(call.info("Success 0"), calls) + self.assertIn(call.info("Success 1"), calls) + self.assertNotIn(call.info("Success 2"), calls) + self.assertIn(call.warning("Warning default"), calls) + self.assertIn(call.warning("Warning 0"), calls) + self.assertIn(call.warning("Warning 1"), calls) + self.assertNotIn(call.warning("Warning 2"), calls) + self.assertIn(call.error("Error"), calls) + + def test_print_to_log_verbosity_0(self): + command = Command() + call_command(command, verbosity=0, logger=True) + calls = command.handler.logger.mock_calls + self.assertIn(call.info("Print 0"), calls) + self.assertNotIn(call.info("Print 1"), calls) + self.assertNotIn(call.info("Print 2"), calls) + self.assertIn(call.info("Success 0"), calls) + self.assertNotIn(call.info("Success 1"), calls) + self.assertNotIn(call.info("Success 2"), calls) + self.assertIn(call.warning("Warning 0"), calls) + self.assertNotIn(call.warning("Warning 1"), calls) + self.assertNotIn(call.warning("Warning 2"), calls) + self.assertIn(call.error("Error"), calls) + + def test_print_to_log_verbosity_3(self): + command = Command() + call_command(command, verbosity=3, logger=True) + calls = command.handler.logger.mock_calls + self.assertIn(call.info("Print 0"), calls) + self.assertIn(call.info("Print 1"), calls) + self.assertIn(call.info("Print 2"), calls) + self.assertIn(call.info("Success 0"), calls) + self.assertIn(call.info("Success 1"), calls) + self.assertIn(call.info("Success 2"), calls) + self.assertIn(call.warning("Warning 0"), calls) + self.assertIn(call.warning("Warning 1"), calls) + self.assertIn(call.warning("Warning 2"), calls) + self.assertIn(call.error("Error"), calls) + + def test_print_to_log_args_kwargs(self): + command = Command() + call_command(command, verbosity=3, logger=True) + calls = command.handler.logger.mock_calls + self.assertIn(call.info('Print %s', 'JohnDoe'), calls) + self.assertIn(call.info('Print', extra={'n': 'JohnDoe'}), calls) + self.assertIn(call.info('Print %s', 'John', extra={'n': 'Doe'}), calls) + self.assertIn(call.info('Success JohnDoe'), calls) + self.assertIn(call.info('Success', extra={'n': 'JohnDoe'}), calls) + self.assertIn(call.info('Success John', extra={'n': 'Doe'}), calls) + self.assertIn(call.warning('Warning JohnDoe'), calls) + self.assertIn(call.warning('Warning', extra={'n': 'JohnDoe'}), calls) + self.assertIn(call.warning('Warning John', extra={'n': 'Doe'}), calls) + self.assertIn(call.error('Error JohnDoe'), calls) + self.assertIn(call.error('Error', extra={'n': 'JohnDoe'}), calls) + self.assertIn(call.error('Error John', extra={'n': 'Doe'}), calls) diff --git a/docker-compose.yml b/docker-compose.yml index c8aaf4d..3ecbee1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,3 +15,20 @@ services: ports: - "${DB_PORT}:5432" + cognito: + image: jagregory/cognito-local:latest + volumes: + - source: ${PWD}/.volumes/cognito + target: /app/.cognito + type: bind + bind: + create_host_path: true + ports: + - "${COGNITO_PORT}:9229" + # add an empty user pool json if not already existing, then run the service + entrypoint: > + /bin/sh -c " + [ ! -f /app/.cognito/db/${COGNITO_POOL_ID}.json ] && + echo '{}' > /app/.cognito/db/${COGNITO_POOL_ID}.json + node /app/start.js; + "