From 23cccebb9685d9404f97eb8122610c8ea08ba7e9 Mon Sep 17 00:00:00 2001 From: Jens L Date: Fri, 11 Sep 2020 23:21:11 +0200 Subject: [PATCH] pytest (#209) --- Makefile | 2 +- Pipfile | 3 +- Pipfile.lock | 167 ++++++++++++++++-- e2e/docker-compose.yml | 2 +- e2e/test_flows_enroll.py | 53 ++---- e2e/test_flows_login.py | 4 + e2e/test_flows_stage_setup.py | 14 +- e2e/test_provider_oauth2_github.py | 53 +++--- e2e/test_provider_oauth2_oidc.py | 38 ++-- e2e/test_provider_saml.py | 7 +- e2e/test_source_saml.py | 7 +- e2e/test_sources_oauth.py | 44 ++--- e2e/utils.py | 31 +++- lifecycle/gunicorn.conf.py | 13 +- passbook/admin/views/applications.py | 2 +- passbook/admin/views/certificate_key_pair.py | 2 +- passbook/admin/views/flows.py | 2 +- passbook/admin/views/groups.py | 2 +- passbook/admin/views/outposts.py | 2 +- passbook/admin/views/policies.py | 2 +- passbook/admin/views/policies_bindings.py | 2 +- passbook/admin/views/property_mapping.py | 2 +- passbook/admin/views/providers.py | 2 +- passbook/admin/views/sources.py | 2 +- passbook/admin/views/stages.py | 2 +- passbook/admin/views/stages_bindings.py | 2 +- passbook/admin/views/stages_invitations.py | 2 +- passbook/admin/views/stages_prompts.py | 2 +- passbook/admin/views/tokens.py | 2 +- passbook/admin/views/users.py | 2 +- passbook/api/v2/urls.py | 5 +- passbook/core/views/utils.py | 2 +- .../flows/migrations/0009_source_flows.py | 2 +- passbook/flows/templates/flows/shell.html | 10 +- passbook/flows/tests/test_views.py | 10 +- passbook/providers/oauth2/models.py | 2 +- passbook/providers/oauth2/utils.py | 2 +- passbook/providers/oauth2/views/userinfo.py | 2 +- passbook/providers/saml/models.py | 2 +- passbook/root/settings.py | 11 +- passbook/root/test_runner.py | 35 ++++ passbook/root/urls.py | 2 +- passbook/sources/oauth/clients.py | 6 +- passbook/sources/oauth/views/callback.py | 2 +- passbook/sources/oauth/views/user.py | 2 +- passbook/stages/captcha/tests.py | 4 +- passbook/stages/consent/tests.py | 8 +- passbook/stages/dummy/tests.py | 4 +- passbook/stages/email/tests.py | 4 +- passbook/stages/identification/tests.py | 8 +- passbook/stages/invitation/tests.py | 8 +- passbook/stages/otp_time/stage.py | 4 +- passbook/stages/password/tests.py | 10 +- passbook/stages/prompt/tests.py | 10 +- passbook/stages/user_delete/tests.py | 6 +- passbook/stages/user_login/tests.py | 8 +- passbook/stages/user_logout/tests.py | 4 +- passbook/stages/user_write/tests.py | 8 +- pytest.ini | 6 + 59 files changed, 403 insertions(+), 254 deletions(-) create mode 100644 passbook/root/test_runner.py create mode 100644 pytest.ini diff --git a/Makefile b/Makefile index 86435e31c..a62c4621b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ all: lint-fix lint coverage gen coverage: - coverage run --concurrency=multiprocessing manage.py test passbook --failfast + coverage run --concurrency=multiprocessing manage.py test --failfast coverage combine coverage html coverage report diff --git a/Pipfile b/Pipfile index e76bfb0cb..9d60ce1c1 100644 --- a/Pipfile +++ b/Pipfile @@ -59,5 +59,6 @@ docker = "*" pylint = "*" pylint-django = "*" selenium = "*" -unittest-xml-reporting = "*" prospector = "*" +pytest = "*" +pytest-django = "*" diff --git a/Pipfile.lock b/Pipfile.lock index ae114fb36..68131f7c5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a798bbd0b97857cac136c1743b8d6ad8bf8c3d95e2760c71d324bb2a7f47f678" + "sha256": "80570636236962f4b934a884817292de9f7bb48520aa964afc2959b0f795fb57" }, "pipfile-spec": 6, "requires": { @@ -28,6 +28,7 @@ "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21", "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.6.1" }, "asgiref": { @@ -35,6 +36,7 @@ "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], + "markers": "python_version >= '3.5'", "version": "==3.2.10" }, "async-timeout": { @@ -42,6 +44,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -49,6 +52,7 @@ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.0" }, "autobahn": { @@ -56,6 +60,7 @@ "sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b", "sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb" ], + "markers": "python_version >= '3.5'", "version": "==20.7.1" }, "automat": { @@ -92,6 +97,7 @@ "sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98", "sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20" ], + "markers": "python_version ~= '3.5'", "version": "==4.1.1" }, "celery": { @@ -170,6 +176,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "constantly": { @@ -338,6 +345,7 @@ "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" ], + "markers": "python_version >= '3.5'", "version": "==3.11.1" }, "djangorestframework-guardian": { @@ -354,6 +362,7 @@ "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.15.2" }, "drf-yasg": { @@ -383,6 +392,7 @@ "hashes": [ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.2" }, "google-auth": { @@ -390,6 +400,7 @@ "sha256:bcbd9f970e7144fe933908aa286d7a12c44b7deb6d78a76871f0377a29d09789", "sha256:f4d5093f13b1b1c0a434ab1dc851cd26a983f86a4d75c95239974e33ed406a87" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.21.1" }, "gunicorn": { @@ -456,6 +467,7 @@ "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "httptools": { @@ -502,6 +514,7 @@ "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" ], + "markers": "python_version >= '3.5'", "version": "==0.5.1" }, "itypes": { @@ -516,6 +529,7 @@ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.2" }, "jmespath": { @@ -523,6 +537,7 @@ "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.0" }, "jsonschema": { @@ -537,6 +552,7 @@ "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a", "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.6.11" }, "kubernetes": { @@ -549,8 +565,11 @@ }, "ldap3": { "hashes": [ + "sha256:10bdd23b612e942ce90ea4dbc744dfd88735949833e46c5467a2dcf68e60f469", "sha256:37d633e20fa360c302b1263c96fe932d40622d0119f1bddcb829b03462eeeeb7", - "sha256:7c3738570766f5e5e74a56fade15470f339d5c436d821cf476ef27da0a4de8b0" + "sha256:7c3738570766f5e5e74a56fade15470f339d5c436d821cf476ef27da0a4de8b0", + "sha256:8f59a7b5399555b22db06f153daa76c77ded2dd84bc0f0ffe5b0b33901b6eac4", + "sha256:bed71c6ce2f70a00a330eed0c8370664c065239d45bcbe1b82517b6f6eed7f25" ], "index": "pypi", "version": "==2.8.1" @@ -628,6 +647,7 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "msgpack": { @@ -658,6 +678,7 @@ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.1.0" }, "packaging": { @@ -715,15 +736,37 @@ }, "pyasn1": { "hashes": [ + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" ], "version": "==0.4.8" }, "pyasn1-modules": { "hashes": [ + "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", + "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", + "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", + "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", + "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", + "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", + "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", + "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", + "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", + "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", + "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" ], "version": "==0.2.8" }, @@ -732,6 +775,7 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pycryptodome": { @@ -803,6 +847,7 @@ "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46", "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.9.8" }, "pyhamcrest": { @@ -810,6 +855,7 @@ "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" ], + "markers": "python_version >= '3.5'", "version": "==2.0.2" }, "pyjwkest": { @@ -831,19 +877,21 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pyrsistent": { "hashes": [ - "sha256:27515d2d5db0629c7dadf6fbe76973eb56f098c1b01d36de42eb69220d2c19e4" + "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3" ], - "version": "==0.17.2" + "version": "==0.16.0" }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "pytz": { @@ -883,6 +931,7 @@ "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.5.3" }, "requests": { @@ -890,12 +939,14 @@ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.24.0" }, "requests-oauthlib": { "hashes": [ "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", + "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" ], "index": "pypi", "version": "==1.3.0" @@ -940,7 +991,7 @@ "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" ], - "markers": "platform_python_implementation == 'CPython' and python_version < '3.9'", + "markers": "python_version < '3.9' and platform_python_implementation == 'CPython'", "version": "==0.2.2" }, "s3transfer": { @@ -979,6 +1030,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "sqlparse": { @@ -986,6 +1038,7 @@ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3.1" }, "structlog": { @@ -1033,6 +1086,7 @@ "sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467", "sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==20.3.0" }, "txaio": { @@ -1040,6 +1094,7 @@ "sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d", "sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae" ], + "markers": "python_version >= '3.5'", "version": "==20.4.1" }, "uritemplate": { @@ -1047,6 +1102,7 @@ "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.1" }, "urllib3": { @@ -1058,7 +1114,6 @@ "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "index": "pypi", - "markers": null, "version": "==1.25.10" }, "uvicorn": { @@ -1089,6 +1144,7 @@ "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.3.0" }, "websocket-client": { @@ -1123,6 +1179,7 @@ "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" ], + "markers": "python_full_version >= '3.6.1'", "version": "==8.1" }, "zope.interface": { @@ -1168,6 +1225,7 @@ "sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6", "sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==5.1.0" } }, @@ -1184,6 +1242,7 @@ "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], + "markers": "python_version >= '3.5'", "version": "==3.2.10" }, "astroid": { @@ -1191,6 +1250,7 @@ "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" ], + "markers": "python_version >= '3.5'", "version": "==2.4.1" }, "attrs": { @@ -1198,6 +1258,7 @@ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.0" }, "autopep8": { @@ -1228,6 +1289,7 @@ "sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0", "sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984" ], + "markers": "python_version >= '3.5'", "version": "==1.0.0" }, "bumpversion": { @@ -1257,6 +1319,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { @@ -1343,6 +1406,7 @@ "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.8.3" }, "flake8-polyfill": { @@ -1357,6 +1421,7 @@ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], + "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { @@ -1364,6 +1429,7 @@ "sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912", "sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910" ], + "markers": "python_version >= '3.4'", "version": "==3.1.8" }, "idna": { @@ -1373,11 +1439,19 @@ ], "version": "==2.10" }, + "iniconfig": { + "hashes": [ + "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", + "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + ], + "version": "==1.0.1" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.3.21" }, "lazy-object-proxy": { @@ -1404,6 +1478,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -1413,6 +1488,22 @@ ], "version": "==0.6.1" }, + "more-itertools": { + "hashes": [ + "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", + "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c" + ], + "markers": "python_version >= '3.5'", + "version": "==8.5.0" + }, + "packaging": { + "hashes": [ + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + ], + "index": "pypi", + "version": "==20.4" + }, "pathspec": { "hashes": [ "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", @@ -1425,6 +1516,7 @@ "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea", "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15" ], + "markers": "python_version >= '2.6'", "version": "==5.5.0" }, "pep8-naming": { @@ -1434,6 +1526,14 @@ ], "version": "==0.10.0" }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, "prospector": { "hashes": [ "sha256:43e5e187c027336b0e4c4aa6a82d66d3b923b5ec5b51968126132e32f9d14a2f" @@ -1441,11 +1541,20 @@ "index": "pypi", "version": "==1.3.0" }, + "py": { + "hashes": [ + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.9.0" + }, "pycodestyle": { "hashes": [ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -1453,6 +1562,7 @@ "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], + "markers": "python_version >= '3.5'", "version": "==5.1.1" }, "pyflakes": { @@ -1460,6 +1570,7 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pylint": { @@ -1497,6 +1608,30 @@ ], "version": "==0.6" }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4", + "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad" + ], + "index": "pypi", + "version": "==6.0.1" + }, + "pytest-django": { + "hashes": [ + "sha256:64f99d565dd9497af412fcab2989fe40982c1282d4118ff422b407f3f7275ca5", + "sha256:664e5f42242e5e182519388f01b9f25d824a9feb7cd17d8f863c8d776f38baf9" + ], + "index": "pypi", + "version": "==3.9.0" + }, "pytz": { "hashes": [ "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", @@ -1552,6 +1687,7 @@ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.24.0" }, "requirements-detector": { @@ -1579,6 +1715,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "smmap": { @@ -1586,6 +1723,7 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "snowballstemmer": { @@ -1600,6 +1738,7 @@ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3.1" }, "stevedore": { @@ -1607,6 +1746,7 @@ "sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e", "sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee" ], + "markers": "python_version >= '3.6'", "version": "==3.2.1" }, "toml": { @@ -1642,14 +1782,6 @@ ], "version": "==1.4.1" }, - "unittest-xml-reporting": { - "hashes": [ - "sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca", - "sha256:984cebba69e889401bfe3adb9088ca376b3a1f923f0590d005126c1bffd1a695" - ], - "index": "pypi", - "version": "==3.0.4" - }, "urllib3": { "extras": [ "secure" @@ -1659,7 +1791,6 @@ "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "index": "pypi", - "markers": null, "version": "==1.25.10" }, "websocket-client": { diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index d8ce2b8d3..35bd99735 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: chrome: - image: selenium/standalone-chrome-debug:3.141.59-20200525 + image: selenium/standalone-chrome-debug:3.141.59-20200719 volumes: - /dev/shm:/dev/shm network_mode: host diff --git a/e2e/test_flows_enroll.py b/e2e/test_flows_enroll.py index e0e767532..9af3647fa 100644 --- a/e2e/test_flows_enroll.py +++ b/e2e/test_flows_enroll.py @@ -1,13 +1,12 @@ """Test Enroll flow""" -from time import sleep +from sys import platform +from typing import Any, Dict, Optional +from unittest.case import skipUnless from django.test import override_settings -from docker import DockerClient, from_env -from docker.models.containers import Container from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as ec -from structlog import get_logger from e2e.utils import USER, SeleniumTestCase from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding @@ -18,41 +17,23 @@ from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage from passbook.stages.user_login.models import UserLoginStage from passbook.stages.user_write.models import UserWriteStage -LOGGER = get_logger() - +@skipUnless(platform.startswith("linux"), "requires local docker") class TestFlowsEnroll(SeleniumTestCase): """Test Enroll flow""" - def setUp(self): - self.container = self.setup_client() - super().setUp() - - def setup_client(self) -> Container: - """Setup test IdP container""" - client: DockerClient = from_env() - container = client.containers.run( - image="mailhog/mailhog:v1.0.1", - detach=True, - network_mode="host", - auto_remove=True, - healthcheck=Healthcheck( + def get_container_specs(self) -> Optional[Dict[str, Any]]: + return { + "image": "mailhog/mailhog:v1.0.1", + "detach": True, + "network_mode": "host", + "auto_remove": True, + "healthcheck": Healthcheck( test=["CMD", "wget", "--spider", "http://localhost:8025"], interval=5 * 100 * 1000000, start_period=1 * 100 * 1000000, ), - ) - while True: - container.reload() - status = container.attrs.get("State", {}).get("Health", {}).get("Status") - if status == "healthy": - return container - LOGGER.info("Container failed healthcheck") - sleep(1) - - def tearDown(self): - self.container.kill() - super().tearDown() + } def test_enroll_2_step(self): """Test 2-step enroll flow""" @@ -220,21 +201,25 @@ class TestFlowsEnroll(SeleniumTestCase): self.driver.find_element(By.ID, "id_name").send_keys("some name") self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz") self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() - sleep(3) + # Wait for the success message so we know the email is sent + self.wait.until( + ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-form > p")) + ) # Open Mailhog self.driver.get("http://localhost:8025") # Click on first message + self.wait.until( + ec.presence_of_element_located((By.CLASS_NAME, "msglist-message")) + ) self.driver.find_element(By.CLASS_NAME, "msglist-message").click() - sleep(3) self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane")) self.driver.find_element(By.ID, "confirm").click() self.driver.close() self.driver.switch_to.window(self.driver.window_handles[0]) # We're now logged in - sleep(3) self.wait.until( ec.presence_of_element_located( (By.XPATH, "//a[contains(@href, '/-/user/')]") diff --git a/e2e/test_flows_login.py b/e2e/test_flows_login.py index c92b3cbc6..6c7325b14 100644 --- a/e2e/test_flows_login.py +++ b/e2e/test_flows_login.py @@ -1,10 +1,14 @@ """test default login flow""" +from sys import platform +from unittest.case import skipUnless + from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from e2e.utils import USER, SeleniumTestCase +@skipUnless(platform.startswith("linux"), "requires local docker") class TestFlowsLogin(SeleniumTestCase): """test default login flow""" diff --git a/e2e/test_flows_stage_setup.py b/e2e/test_flows_stage_setup.py index 8145ecaf2..be8088520 100644 --- a/e2e/test_flows_stage_setup.py +++ b/e2e/test_flows_stage_setup.py @@ -1,7 +1,6 @@ """test stage setup flows (password change)""" -import string -from random import SystemRandom -from time import sleep +from sys import platform +from unittest.case import skipUnless from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys @@ -9,9 +8,11 @@ from selenium.webdriver.common.keys import Keys from e2e.utils import USER, SeleniumTestCase from passbook.core.models import User from passbook.flows.models import Flow, FlowDesignation +from passbook.providers.oauth2.generators import generate_client_secret from passbook.stages.password.models import PasswordStage +@skipUnless(platform.startswith("linux"), "requires local docker") class TestFlowsStageSetup(SeleniumTestCase): """test stage setup flows""" @@ -27,10 +28,7 @@ class TestFlowsStageSetup(SeleniumTestCase): stage.change_flow = flow stage.save() - new_password = "".join( - SystemRandom().choice(string.ascii_uppercase + string.digits) - for _ in range(8) - ) + new_password = generate_client_secret() self.driver.get( f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F" @@ -48,7 +46,7 @@ class TestFlowsStageSetup(SeleniumTestCase): self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password) self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() - sleep(2) + self.wait_for_url(self.url("passbook_core:user-settings")) # Because USER() is cached, we need to get the user manually here user = User.objects.get(username=USER().username) self.assertTrue(user.check_password(new_password)) diff --git a/e2e/test_provider_oauth2_github.py b/e2e/test_provider_oauth2_github.py index 756ef7fa8..cfa7459b1 100644 --- a/e2e/test_provider_oauth2_github.py +++ b/e2e/test_provider_oauth2_github.py @@ -1,12 +1,11 @@ """test OAuth Provider flow""" -from time import sleep +from sys import platform +from typing import Any, Dict, Optional +from unittest.case import skipUnless -from docker import DockerClient, from_env -from docker.models.containers import Container from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys -from structlog import get_logger from e2e.utils import USER, SeleniumTestCase from passbook.core.models import Application @@ -19,32 +18,29 @@ from passbook.providers.oauth2.generators import ( ) from passbook.providers.oauth2.models import ClientTypes, OAuth2Provider, ResponseTypes -LOGGER = get_logger() - +@skipUnless(platform.startswith("linux"), "requires local docker") class TestProviderOAuth2Github(SeleniumTestCase): """test OAuth Provider flow""" def setUp(self): self.client_id = generate_client_id() self.client_secret = generate_client_secret() - self.container = self.setup_client() super().setUp() - def setup_client(self) -> Container: + def get_container_specs(self) -> Optional[Dict[str, Any]]: """Setup client grafana container which we test OAuth against""" - client: DockerClient = from_env() - container = client.containers.run( - image="grafana/grafana:7.1.0", - detach=True, - network_mode="host", - auto_remove=True, - healthcheck=Healthcheck( + return { + "image": "grafana/grafana:7.1.0", + "detach": True, + "network_mode": "host", + "auto_remove": True, + "healthcheck": Healthcheck( test=["CMD", "wget", "--spider", "http://localhost:3000"], interval=5 * 100 * 1000000, start_period=1 * 100 * 1000000, ), - environment={ + "environment": { "GF_AUTH_GITHUB_ENABLED": "true", "GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true", "GF_AUTH_GITHUB_CLIENT_ID": self.client_id, @@ -61,22 +57,10 @@ class TestProviderOAuth2Github(SeleniumTestCase): ), "GF_LOG_LEVEL": "debug", }, - ) - while True: - container.reload() - status = container.attrs.get("State", {}).get("Health", {}).get("Status") - if status == "healthy": - return container - LOGGER.info("Container failed healthcheck") - sleep(1) - - def tearDown(self): - self.container.kill() - super().tearDown() + } def test_authorization_consent_implied(self): """test OAuth Provider flow (default authorization flow with implied consent)""" - sleep(1) # Bootstrap all needed objects authorization_flow = Flow.objects.get( slug="default-provider-authorization-implicit-consent" @@ -129,7 +113,6 @@ class TestProviderOAuth2Github(SeleniumTestCase): def test_authorization_consent_explicit(self): """test OAuth Provider flow (default authorization flow with explicit consent)""" - sleep(1) # Bootstrap all needed objects authorization_flow = Flow.objects.get( slug="default-provider-authorization-explicit-consent" @@ -167,8 +150,13 @@ class TestProviderOAuth2Github(SeleniumTestCase): By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]" ).text, ) - sleep(1) - self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() + self.driver.find_element( + By.CSS_SELECTOR, + ( + "form[action='/flows/b/default-provider-authorization-explicit-consent/'] " + "[type=submit]" + ), + ).click() self.wait_for_url("http://localhost:3000/?orgId=1") self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() @@ -197,7 +185,6 @@ class TestProviderOAuth2Github(SeleniumTestCase): def test_denied(self): """test OAuth Provider flow (default authorization flow, denied)""" - sleep(1) # Bootstrap all needed objects authorization_flow = Flow.objects.get( slug="default-provider-authorization-explicit-consent" diff --git a/e2e/test_provider_oauth2_oidc.py b/e2e/test_provider_oauth2_oidc.py index 829ba5e3b..317f32977 100644 --- a/e2e/test_provider_oauth2_oidc.py +++ b/e2e/test_provider_oauth2_oidc.py @@ -1,8 +1,9 @@ """test OAuth2 OpenID Provider flow""" +from sys import platform from time import sleep +from typing import Any, Dict, Optional +from unittest.case import skipUnless -from docker import DockerClient, from_env -from docker.models.containers import Container from docker.types import Healthcheck from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys @@ -34,29 +35,27 @@ from passbook.providers.oauth2.models import ( LOGGER = get_logger() +@skipUnless(platform.startswith("linux"), "requires local docker") class TestProviderOAuth2OIDC(SeleniumTestCase): """test OAuth with OpenID Provider flow""" def setUp(self): self.client_id = generate_client_id() self.client_secret = generate_client_secret() - self.container = self.setup_client() super().setUp() - def setup_client(self) -> Container: - """Setup client grafana container which we test OIDC against""" - client: DockerClient = from_env() - container = client.containers.run( - image="grafana/grafana:7.1.0", - detach=True, - network_mode="host", - auto_remove=True, - healthcheck=Healthcheck( + def get_container_specs(self) -> Optional[Dict[str, Any]]: + return { + "image": "grafana/grafana:7.1.0", + "detach": True, + "network_mode": "host", + "auto_remove": True, + "healthcheck": Healthcheck( test=["CMD", "wget", "--spider", "http://localhost:3000"], interval=5 * 100 * 1000000, start_period=1 * 100 * 1000000, ), - environment={ + "environment": { "GF_AUTH_GENERIC_OAUTH_ENABLED": "true", "GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id, "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret, @@ -72,18 +71,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): ), "GF_LOG_LEVEL": "debug", }, - ) - while True: - container.reload() - status = container.attrs.get("State", {}).get("Health", {}).get("Status") - if status == "healthy": - return container - LOGGER.info("Container failed healthcheck") - sleep(1) - - def tearDown(self): - self.container.kill() - super().tearDown() + } def test_redirect_uri_error(self): """test OpenID Provider flow (invalid redirect URI, check error message)""" diff --git a/e2e/test_provider_saml.py b/e2e/test_provider_saml.py index 1cf9fac6a..2272a2956 100644 --- a/e2e/test_provider_saml.py +++ b/e2e/test_provider_saml.py @@ -1,5 +1,7 @@ """test SAML Provider flow""" +from sys import platform from time import sleep +from unittest.case import skipUnless from docker import DockerClient, from_env from docker.models.containers import Container @@ -23,6 +25,7 @@ from passbook.providers.saml.models import ( LOGGER = get_logger() +@skipUnless(platform.startswith("linux"), "requires local docker") class TestProviderSAML(SeleniumTestCase): """test SAML Provider flow""" @@ -60,10 +63,6 @@ class TestProviderSAML(SeleniumTestCase): LOGGER.info("Container failed healthcheck") sleep(1) - def tearDown(self): - self.container.kill() - super().tearDown() - def test_sp_initiated_implicit(self): """test SAML Provider flow SP-initiated flow (implicit consent)""" # Bootstrap all needed objects diff --git a/e2e/test_source_saml.py b/e2e/test_source_saml.py index fea1ca95d..a2122a3bb 100644 --- a/e2e/test_source_saml.py +++ b/e2e/test_source_saml.py @@ -1,5 +1,7 @@ """test SAML Source""" +from sys import platform from time import sleep +from unittest.case import skipUnless from docker import DockerClient, from_env from docker.models.containers import Container @@ -68,6 +70,7 @@ Sm75WXsflOxuTn08LbgGc4s= -----END PRIVATE KEY-----""" +@skipUnless(platform.startswith("linux"), "requires local docker") class TestSourceSAML(SeleniumTestCase): """test SAML Source flow""" @@ -103,10 +106,6 @@ class TestSourceSAML(SeleniumTestCase): LOGGER.info("Container failed healthcheck") sleep(1) - def tearDown(self): - self.container.kill() - super().tearDown() - def test_idp_redirect(self): """test SAML Source With redirect binding""" sleep(1) diff --git a/e2e/test_sources_oauth.py b/e2e/test_sources_oauth.py index 2db1fbea1..a6da73d36 100644 --- a/e2e/test_sources_oauth.py +++ b/e2e/test_sources_oauth.py @@ -1,8 +1,9 @@ """test OAuth Source""" from os.path import abspath -from time import sleep +from sys import platform +from typing import Any, Dict, Optional +from unittest.case import skipUnless -from docker import DockerClient, from_env from docker.models.containers import Container from docker.types import Healthcheck from selenium.webdriver.common.by import By @@ -21,6 +22,7 @@ CONFIG_PATH = "/tmp/dex.yml" LOGGER = get_logger() +@skipUnless(platform.startswith("linux"), "requires local docker") class TestSourceOAuth(SeleniumTestCase): """test OAuth Source flow""" @@ -28,7 +30,7 @@ class TestSourceOAuth(SeleniumTestCase): def setUp(self): self.client_secret = generate_client_secret() - self.container = self.setup_client() + self.prepare_dex_config() super().setUp() def prepare_dex_config(self): @@ -66,34 +68,23 @@ class TestSourceOAuth(SeleniumTestCase): with open(CONFIG_PATH, "w+") as _file: safe_dump(config, _file) - def setup_client(self) -> Container: - """Setup test Dex container""" - self.prepare_dex_config() - client: DockerClient = from_env() - container = client.containers.run( - image="quay.io/dexidp/dex:v2.24.0", - detach=True, - network_mode="host", - auto_remove=True, - command="serve /config.yml", - healthcheck=Healthcheck( + def get_container_specs(self) -> Optional[Dict[str, Any]]: + return { + "image": "quay.io/dexidp/dex:v2.24.0", + "detach": True, + "network_mode": "host", + "auto_remove": True, + "command": "serve /config.yml", + "healthcheck": Healthcheck( test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"], interval=5 * 100 * 1000000, start_period=1 * 100 * 1000000, ), - volumes={abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}}, - ) - while True: - container.reload() - status = container.attrs.get("State", {}).get("Health", {}).get("Status") - if status == "healthy": - return container - LOGGER.info("Container failed healthcheck") - sleep(1) + "volumes": {abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}}, + } def create_objects(self): """Create required objects""" - sleep(1) # Bootstrap all needed objects authentication_flow = Flow.objects.get(slug="default-source-authentication") enrollment_flow = Flow.objects.get(slug="default-source-enrollment") @@ -111,10 +102,6 @@ class TestSourceOAuth(SeleniumTestCase): consumer_secret=self.client_secret, ) - def tearDown(self): - self.container.kill() - super().tearDown() - def test_oauth_enroll(self): """test OAuth Source With With OIDC""" self.create_objects() @@ -141,6 +128,7 @@ class TestSourceOAuth(SeleniumTestCase): ) self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click() + self.wait.until(ec.presence_of_element_located((By.NAME, "username"))) # At this point we've been redirected back # and we're asked for the username self.driver.find_element(By.NAME, "username").click() diff --git a/e2e/utils.py b/e2e/utils.py index a4db8dd3b..5896c127e 100644 --- a/e2e/utils.py +++ b/e2e/utils.py @@ -4,13 +4,16 @@ from glob import glob from importlib.util import module_from_spec, spec_from_file_location from inspect import getmembers, isfunction from os import environ, makedirs -from time import time +from time import sleep, time +from typing import Any, Dict, Optional from django.apps import apps from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.db import connection, transaction from django.db.utils import IntegrityError from django.shortcuts import reverse +from docker import DockerClient, from_env +from docker.models.containers import Container from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.remote.webdriver import WebDriver @@ -30,15 +33,35 @@ def USER() -> User: # noqa class SeleniumTestCase(StaticLiveServerTestCase): """StaticLiveServerTestCase which automatically creates a Webdriver instance""" + container: Optional[Container] = None + def setUp(self): super().setUp() makedirs("selenium_screenshots/", exist_ok=True) self.driver = self._get_driver() self.driver.maximize_window() - self.driver.implicitly_wait(30) - self.wait = WebDriverWait(self.driver, 50) + self.driver.implicitly_wait(10) + self.wait = WebDriverWait(self.driver, 30) self.apply_default_data() self.logger = get_logger() + if specs := self.get_container_specs(): + self.container = self._start_container(specs) + + def _start_container(self, specs: Dict[str, Any]) -> Container: + client: DockerClient = from_env() + container = client.containers.run(**specs) + while True: + container.reload() + status = container.attrs.get("State", {}).get("Health", {}).get("Status") + if status == "healthy": + return container + self.logger.info("Container failed healthcheck") + sleep(1) + + def get_container_specs(self) -> Optional[Dict[str, Any]]: + """Optionally get container specs which will launched on setup, wait for the container to + be healthy, and deleted again on tearDown""" + return None def _get_driver(self) -> WebDriver: return webdriver.Remote( @@ -57,6 +80,8 @@ class SeleniumTestCase(StaticLiveServerTestCase): self.logger.warning( line["message"], source=line["source"], level=line["level"] ) + if self.container: + self.container.kill() self.driver.quit() super().tearDown() diff --git a/lifecycle/gunicorn.conf.py b/lifecycle/gunicorn.conf.py index 17ea97fca..d16ed83be 100644 --- a/lifecycle/gunicorn.conf.py +++ b/lifecycle/gunicorn.conf.py @@ -1,9 +1,10 @@ """Gunicorn config""" +from multiprocessing import cpu_count +from pathlib import Path + import structlog bind = "0.0.0.0:8000" -workers = 2 -threads = 4 user = "passbook" group = "passbook" @@ -40,3 +41,11 @@ logconfig_dict = { "gunicorn": {"handlers": ["console"], "level": "INFO", "propagate": False}, }, } + +# if we're running in kubernetes, use fixed workers because we can scale with more pods +# otherwise (assume docker-compose), use as much as we can +if Path("/var/run/secrets/kubernetes.io").exists(): + workers = 2 +else: + worker = cpu_count() +threads = 4 diff --git a/passbook/admin/views/applications.py b/passbook/admin/views/applications.py index 741cf0e50..2a5bc85da 100644 --- a/passbook/admin/views/applications.py +++ b/passbook/admin/views/applications.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/certificate_key_pair.py b/passbook/admin/views/certificate_key_pair.py index 79e1c5a47..2ab02eeb6 100644 --- a/passbook/admin/views/certificate_key_pair.py +++ b/passbook/admin/views/certificate_key_pair.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/flows.py b/passbook/admin/views/flows.py index 207eecfda..3fb977d38 100644 --- a/passbook/admin/views/flows.py +++ b/passbook/admin/views/flows.py @@ -7,7 +7,7 @@ from django.contrib.auth.mixins import ( from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpRequest, HttpResponse, JsonResponse from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import DetailView, FormView, ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/groups.py b/passbook/admin/views/groups.py index cfc99c18c..267a88075 100644 --- a/passbook/admin/views/groups.py +++ b/passbook/admin/views/groups.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/outposts.py b/passbook/admin/views/outposts.py index b3ecd555e..87c1721f9 100644 --- a/passbook/admin/views/outposts.py +++ b/passbook/admin/views/outposts.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/policies.py b/passbook/admin/views/policies.py index ac04ef537..523c7650a 100644 --- a/passbook/admin/views/policies.py +++ b/passbook/admin/views/policies.py @@ -10,7 +10,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.db.models import QuerySet from django.http import HttpResponse from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import FormView from django.views.generic.detail import DetailView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/policies_bindings.py b/passbook/admin/views/policies_bindings.py index f90df5a48..f2ebd77e3 100644 --- a/passbook/admin/views/policies_bindings.py +++ b/passbook/admin/views/policies_bindings.py @@ -6,7 +6,7 @@ from django.contrib.auth.mixins import ( from django.contrib.messages.views import SuccessMessageMixin from django.db.models import QuerySet from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/property_mapping.py b/passbook/admin/views/property_mapping.py index 3ba8ad47e..790d2f912 100644 --- a/passbook/admin/views/property_mapping.py +++ b/passbook/admin/views/property_mapping.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from passbook.admin.views.utils import ( diff --git a/passbook/admin/views/providers.py b/passbook/admin/views/providers.py index e4dffa264..8cfc003c0 100644 --- a/passbook/admin/views/providers.py +++ b/passbook/admin/views/providers.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from passbook.admin.views.utils import ( diff --git a/passbook/admin/views/sources.py b/passbook/admin/views/sources.py index 41a87aa68..f7a318061 100644 --- a/passbook/admin/views/sources.py +++ b/passbook/admin/views/sources.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from passbook.admin.views.utils import ( diff --git a/passbook/admin/views/stages.py b/passbook/admin/views/stages.py index 9451cb2d0..cbcbf5a5b 100644 --- a/passbook/admin/views/stages.py +++ b/passbook/admin/views/stages.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from passbook.admin.views.utils import ( diff --git a/passbook/admin/views/stages_bindings.py b/passbook/admin/views/stages_bindings.py index c673b1aa9..5ff5c418d 100644 --- a/passbook/admin/views/stages_bindings.py +++ b/passbook/admin/views/stages_bindings.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/stages_invitations.py b/passbook/admin/views/stages_invitations.py index f81bb28b9..2a1be1c8a 100644 --- a/passbook/admin/views/stages_invitations.py +++ b/passbook/admin/views/stages_invitations.py @@ -6,7 +6,7 @@ from django.contrib.auth.mixins import ( from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpResponseRedirect from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/stages_prompts.py b/passbook/admin/views/stages_prompts.py index 74b3c3aff..8ebd2d336 100644 --- a/passbook/admin/views/stages_prompts.py +++ b/passbook/admin/views/stages_prompts.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/tokens.py b/passbook/admin/views/tokens.py index 7b8e1c4b2..204bc1012 100644 --- a/passbook/admin/views/tokens.py +++ b/passbook/admin/views/tokens.py @@ -1,7 +1,7 @@ """passbook Token administration""" from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import ListView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin diff --git a/passbook/admin/views/users.py b/passbook/admin/views/users.py index 0e0d262d1..38aaa2e95 100644 --- a/passbook/admin/views/users.py +++ b/passbook/admin/views/users.py @@ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect from django.urls import reverse, reverse_lazy from django.utils.http import urlencode -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import DetailView, ListView, UpdateView from guardian.mixins import ( PermissionListMixin, diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py index 2acadd0c3..a6afa49ae 100644 --- a/passbook/api/v2/urls.py +++ b/passbook/api/v2/urls.py @@ -1,6 +1,5 @@ """api v2 urls""" -from django.conf.urls import url -from django.urls import path +from django.urls import path, re_path from drf_yasg import openapi from drf_yasg.views import get_schema_view from rest_framework import routers @@ -119,7 +118,7 @@ SchemaView = get_schema_view( ) urlpatterns = [ - url( + re_path( r"^swagger(?P\.json|\.yaml)$", SchemaView.without_ui(cache_timeout=0), name="schema-json", diff --git a/passbook/core/views/utils.py b/passbook/core/views/utils.py index cdc338787..7ad222334 100644 --- a/passbook/core/views/utils.py +++ b/passbook/core/views/utils.py @@ -1,5 +1,5 @@ """passbook core utils view""" -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import TemplateView diff --git a/passbook/flows/migrations/0009_source_flows.py b/passbook/flows/migrations/0009_source_flows.py index a39e17258..c713bcfd9 100644 --- a/passbook/flows/migrations/0009_source_flows.py +++ b/passbook/flows/migrations/0009_source_flows.py @@ -52,7 +52,7 @@ def create_default_source_enrollment_flow( # PromptStage to ask user for their username prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create( - name="default-source-enrollment-username-prompt", + name="Welcome to passbook! Please select a username.", ) prompt, _ = Prompt.objects.using(db_alias).update_or_create( field_key="username", diff --git a/passbook/flows/templates/flows/shell.html b/passbook/flows/templates/flows/shell.html index 2ec813f3f..cea54d9a6 100644 --- a/passbook/flows/templates/flows/shell.html +++ b/passbook/flows/templates/flows/shell.html @@ -115,11 +115,12 @@ const updateFormAction = (form) => { for (let index = 0; index < form.elements.length; index++) { const element = form.elements[index]; if (element.value === form.action) { - console.log("Found Form action URL in form elements, not changing form action."); + console.log("pb-flow: Found Form action URL in form elements, not changing form action."); return false; } } form.action = flowBodyUrl; + console.log(`pb-flow: updated form.action ${flowBodyUrl}`); return true; }; const checkAutosubmit = (form) => { @@ -129,11 +130,11 @@ const checkAutosubmit = (form) => { }; const setFormSubmitHandlers = () => { document.querySelectorAll("#flow-body form").forEach(form => { - console.log(`Checking for autosubmit attribute ${form}`); + console.log(`pb-flow: Checking for autosubmit attribute ${form}`); checkAutosubmit(form); - console.log(`Setting action for form ${form}`); + console.log(`pb-flow: Setting action for form ${form}`); updateFormAction(form); - console.log(`Adding handler for form ${form}`); + console.log(`pb-flow: Adding handler for form ${form}`); form.addEventListener('submit', (e) => { e.preventDefault(); let formData = new FormData(form); @@ -145,6 +146,7 @@ const setFormSubmitHandlers = () => { updateCard(data); }); }); + form.classList.add("pb-flow-wrapped"); }); }; diff --git a/passbook/flows/tests/test_views.py b/passbook/flows/tests/test_views.py index 0e3de1abe..f72f92e62 100644 --- a/passbook/flows/tests/test_views.py +++ b/passbook/flows/tests/test_views.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, PropertyMock, patch from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException from passbook.flows.markers import ReevaluateMarker, StageMarker @@ -247,7 +247,7 @@ class TestFlowExecutor(TestCase): response = self.client.post(exec_url) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) @@ -293,7 +293,7 @@ class TestFlowExecutor(TestCase): # First request, run the planner response = self.client.get(exec_url) self.assertEqual(response.status_code, 200) - self.assertIn("dummy1", force_text(response.content)) + self.assertIn("dummy1", force_str(response.content)) plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] @@ -316,13 +316,13 @@ class TestFlowExecutor(TestCase): # but it won't save it, hence we cant' check the plan response = self.client.get(exec_url) self.assertEqual(response.status_code, 200) - self.assertIn("dummy4", force_text(response.content)) + self.assertIn("dummy4", force_str(response.content)) # fourth request, this confirms the last stage (dummy4) # We do this request without the patch, so the policy results in false response = self.client.post(exec_url) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) diff --git a/passbook/providers/oauth2/models.py b/passbook/providers/oauth2/models.py index 868f62508..2f335ac85 100644 --- a/passbook/providers/oauth2/models.py +++ b/passbook/providers/oauth2/models.py @@ -14,7 +14,7 @@ from django.forms import ModelForm from django.http import HttpRequest from django.shortcuts import reverse from django.utils import dateformat, timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key from jwkest.jws import JWS diff --git a/passbook/providers/oauth2/utils.py b/passbook/providers/oauth2/utils.py index 0736d4586..6d5a18e30 100644 --- a/passbook/providers/oauth2/utils.py +++ b/passbook/providers/oauth2/utils.py @@ -82,7 +82,7 @@ def extract_client_auth(request: HttpRequest) -> Tuple[str, str]: b64_user_pass = auth_header.split()[1] try: user_pass = b64decode(b64_user_pass).decode("utf-8").split(":") - client_id, client_secret = tuple(user_pass) + client_id, client_secret = user_pass except (ValueError, Error): client_id = client_secret = "" else: diff --git a/passbook/providers/oauth2/views/userinfo.py b/passbook/providers/oauth2/views/userinfo.py index 379e86feb..d281751fa 100644 --- a/passbook/providers/oauth2/views/userinfo.py +++ b/passbook/providers/oauth2/views/userinfo.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List from django.http import HttpRequest, HttpResponse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.views import View from structlog import get_logger diff --git a/passbook/providers/saml/models.py b/passbook/providers/saml/models.py index c86f37d47..cecbecfff 100644 --- a/passbook/providers/saml/models.py +++ b/passbook/providers/saml/models.py @@ -5,7 +5,7 @@ from django.db import models from django.forms import ModelForm from django.http import HttpRequest from django.shortcuts import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from structlog import get_logger from passbook.core.models import PropertyMapping, Provider diff --git a/passbook/root/settings.py b/passbook/root/settings.py index c74bd475c..5907124aa 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/2.1/ref/settings/ import importlib import os -import sys from json import dumps import structlog @@ -156,6 +155,7 @@ DJANGO_REDIS_IGNORE_EXCEPTIONS = True DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" +SESSION_COOKIE_SAMESITE = "lax" MIDDLEWARE = [ "django_prometheus.middleware.PrometheusBeforeMiddleware", @@ -372,15 +372,9 @@ LOGGING = { } TEST = False -TEST_RUNNER = "xmlrunner.extra.djangotestrunner.XMLTestRunner" +TEST_RUNNER = "passbook.root.test_runner.PytestTestRunner" LOG_LEVEL = CONFIG.y("log_level").upper() -TEST_OUTPUT_FILE_NAME = "unittest.xml" - -if len(sys.argv) >= 2 and sys.argv[1] == "test": - LOG_LEVEL = "DEBUG" - TEST = True - CELERY_TASK_ALWAYS_EAGER = True _LOGGING_HANDLER_MAP = { "": LOG_LEVEL, @@ -431,7 +425,6 @@ for _app in INSTALLED_APPS: pass if DEBUG: - SESSION_COOKIE_SAMESITE = None INSTALLED_APPS.append("debug_toolbar") MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") diff --git a/passbook/root/test_runner.py b/passbook/root/test_runner.py new file mode 100644 index 000000000..17d4bbf07 --- /dev/null +++ b/passbook/root/test_runner.py @@ -0,0 +1,35 @@ +"""Integrate ./manage.py test with pytest""" +from django.conf import settings + + +class PytestTestRunner: + """Runs pytest to discover and run tests.""" + + def __init__(self, verbosity=1, failfast=False, keepdb=False, **_): + self.verbosity = verbosity + self.failfast = failfast + self.keepdb = keepdb + settings.TEST = True + settings.CELERY_TASK_ALWAYS_EAGER = True + + def run_tests(self, test_labels): + """Run pytest and return the exitcode. + + It translates some of Django's test command option to pytest's. + """ + import pytest + + argv = [] + if self.verbosity == 0: + argv.append("--quiet") + if self.verbosity == 2: + argv.append("--verbose") + if self.verbosity == 3: + argv.append("-vv") + if self.failfast: + argv.append("--exitfirst") + if self.keepdb: + argv.append("--reuse-db") + + argv.extend(test_labels) + return pytest.main(argv) diff --git a/passbook/root/urls.py b/passbook/root/urls.py index bf63a5024..c77f5a3b7 100644 --- a/passbook/root/urls.py +++ b/passbook/root/urls.py @@ -15,7 +15,7 @@ admin.site.login = RedirectView.as_view( pattern_name="passbook_flows:default-authentication" ) admin.site.logout = RedirectView.as_view( - pattern_name="passbook_flows:default-invalidate" + pattern_name="passbook_flows:default-invalidation" ) handler400 = error.BadRequestView.as_view() diff --git a/passbook/sources/oauth/clients.py b/passbook/sources/oauth/clients.py index 0b3cb85f8..c5f210ccb 100644 --- a/passbook/sources/oauth/clients.py +++ b/passbook/sources/oauth/clients.py @@ -5,7 +5,7 @@ from urllib.parse import parse_qs, urlencode from django.http import HttpRequest from django.utils.crypto import constant_time_compare, get_random_string -from django.utils.encoding import force_text +from django.utils.encoding import force_str from requests import Session from requests.exceptions import RequestException from requests_oauthlib import OAuth1 @@ -111,7 +111,7 @@ class OAuthClient(BaseOAuthClient): def get_request_token(self, request, callback): "Fetch the OAuth request token. Only required for OAuth 1.0." - callback = force_text(request.build_absolute_uri(callback)) + callback = force_str(request.build_absolute_uri(callback)) try: response = self.session.request( "post", @@ -128,7 +128,7 @@ class OAuthClient(BaseOAuthClient): def get_redirect_args(self, request, callback): "Get request parameters for redirect url." - callback = force_text(request.build_absolute_uri(callback)) + callback = force_str(request.build_absolute_uri(callback)) raw_token = self.get_request_token(request, callback) token, secret = self.parse_raw_token(raw_token) if token is not None and secret is not None: diff --git a/passbook/sources/oauth/views/callback.py b/passbook/sources/oauth/views/callback.py index a6a2e9c34..8d6058794 100644 --- a/passbook/sources/oauth/views/callback.py +++ b/passbook/sources/oauth/views/callback.py @@ -6,7 +6,7 @@ from django.contrib import messages from django.http import Http404, HttpRequest, HttpResponse from django.shortcuts import redirect from django.urls import reverse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import View from structlog import get_logger diff --git a/passbook/sources/oauth/views/user.py b/passbook/sources/oauth/views/user.py index d92d14998..0aa0fba02 100644 --- a/passbook/sources/oauth/views/user.py +++ b/passbook/sources/oauth/views/user.py @@ -6,7 +6,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.generic import TemplateView, View from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection diff --git a/passbook/stages/captcha/tests.py b/passbook/stages/captcha/tests.py index 465deead2..c82abdd95 100644 --- a/passbook/stages/captcha/tests.py +++ b/passbook/stages/captcha/tests.py @@ -2,7 +2,7 @@ from django.conf import settings from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.markers import StageMarker @@ -50,6 +50,6 @@ class TestCaptchaStage(TestCase): ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) diff --git a/passbook/stages/consent/tests.py b/passbook/stages/consent/tests.py index 536b41c6b..2521c7f1c 100644 --- a/passbook/stages/consent/tests.py +++ b/passbook/stages/consent/tests.py @@ -3,7 +3,7 @@ from time import sleep from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import Application, User from passbook.core.tasks import clean_expired_models @@ -49,7 +49,7 @@ class TestConsentStage(TestCase): ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) self.assertFalse(UserConsent.objects.filter(user=self.user).exists()) @@ -80,7 +80,7 @@ class TestConsentStage(TestCase): ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) self.assertTrue( @@ -117,7 +117,7 @@ class TestConsentStage(TestCase): ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) self.assertTrue( diff --git a/passbook/stages/dummy/tests.py b/passbook/stages/dummy/tests.py index e0a84c07d..ddc6f7550 100644 --- a/passbook/stages/dummy/tests.py +++ b/passbook/stages/dummy/tests.py @@ -1,7 +1,7 @@ """dummy tests""" from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding @@ -44,7 +44,7 @@ class TestDummyStage(TestCase): response = self.client.post(url, {}) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) diff --git a/passbook/stages/email/tests.py b/passbook/stages/email/tests.py index ff4db0cd4..110dedeeb 100644 --- a/passbook/stages/email/tests.py +++ b/passbook/stages/email/tests.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from django.core import mail from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import Token, User from passbook.flows.markers import StageMarker @@ -114,7 +114,7 @@ class TestEmailStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) diff --git a/passbook/stages/identification/tests.py b/passbook/stages/identification/tests.py index 313090050..d4ff8a4b9 100644 --- a/passbook/stages/identification/tests.py +++ b/passbook/stages/identification/tests.py @@ -1,7 +1,7 @@ """identification tests""" from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding @@ -56,7 +56,7 @@ class TestIdentificationStage(TestCase): response = self.client.post(url, form_data) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) @@ -101,7 +101,7 @@ class TestIdentificationStage(TestCase): ), ) self.assertEqual(response.status_code, 200) - self.assertIn(flow.slug, force_text(response.content)) + self.assertIn(flow.slug, force_str(response.content)) def test_recovery_flow(self): """Test that recovery flow is linked correctly""" @@ -122,4 +122,4 @@ class TestIdentificationStage(TestCase): ), ) self.assertEqual(response.status_code, 200) - self.assertIn(flow.slug, force_text(response.content)) + self.assertIn(flow.slug, force_str(response.content)) diff --git a/passbook/stages/invitation/tests.py b/passbook/stages/invitation/tests.py index c805f77a6..6e454792c 100644 --- a/passbook/stages/invitation/tests.py +++ b/passbook/stages/invitation/tests.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from guardian.shortcuts import get_anonymous_user from passbook.core.models import User @@ -59,7 +59,7 @@ class TestUserLoginStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_flows:denied")}, ) @@ -86,7 +86,7 @@ class TestUserLoginStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) @@ -125,6 +125,6 @@ class TestUserLoginStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) diff --git a/passbook/stages/otp_time/stage.py b/passbook/stages/otp_time/stage.py index 74e517a2a..039cc8e0e 100644 --- a/passbook/stages/otp_time/stage.py +++ b/passbook/stages/otp_time/stage.py @@ -2,7 +2,7 @@ from typing import Any, Dict from django.http import HttpRequest, HttpResponse -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.views.generic import FormView from django_otp.plugins.otp_totp.models import TOTPDevice from lxml.etree import tostring # nosec @@ -35,7 +35,7 @@ class OTPTimeStageView(FormView, StageView): """Get QR Code SVG as string based on `device`""" qr_code = QRCode(image_factory=SvgFillImage) qr_code.add_data(device.config_url) - return force_text(tostring(qr_code.make_image().get_image())) + return force_str(tostring(qr_code.make_image().get_image())) def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) diff --git a/passbook/stages/password/tests.py b/passbook/stages/password/tests.py index f52b45449..d11b61329 100644 --- a/passbook/stages/password/tests.py +++ b/passbook/stages/password/tests.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch from django.core.exceptions import PermissionDenied from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.markers import StageMarker @@ -61,7 +61,7 @@ class TestPasswordStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_flows:denied")}, ) @@ -84,7 +84,7 @@ class TestPasswordStage(TestCase): ), ) self.assertEqual(response.status_code, 200) - self.assertIn(flow.slug, force_text(response.content)) + self.assertIn(flow.slug, force_str(response.content)) def test_valid_password(self): """Test with a valid pending user and valid password""" @@ -106,7 +106,7 @@ class TestPasswordStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) @@ -154,6 +154,6 @@ class TestPasswordStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_flows:denied")}, ) diff --git a/passbook/stages/prompt/tests.py b/passbook/stages/prompt/tests.py index bc8e3dd2f..3b823c62e 100644 --- a/passbook/stages/prompt/tests.py +++ b/passbook/stages/prompt/tests.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.markers import StageMarker @@ -110,9 +110,9 @@ class TestPromptStage(TestCase): ) self.assertEqual(response.status_code, 200) for prompt in self.stage.fields.all(): - self.assertIn(prompt.field_key, force_text(response.content)) - self.assertIn(prompt.label, force_text(response.content)) - self.assertIn(prompt.placeholder, force_text(response.content)) + self.assertIn(prompt.field_key, force_str(response.content)) + self.assertIn(prompt.label, force_str(response.content)) + self.assertIn(prompt.placeholder, force_str(response.content)) def test_valid_form_with_policy(self) -> PromptForm: """Test form validation""" @@ -164,7 +164,7 @@ class TestPromptStage(TestCase): ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) diff --git a/passbook/stages/user_delete/tests.py b/passbook/stages/user_delete/tests.py index 1c0648a70..319038d58 100644 --- a/passbook/stages/user_delete/tests.py +++ b/passbook/stages/user_delete/tests.py @@ -1,7 +1,7 @@ """delete tests""" from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.markers import StageMarker @@ -44,7 +44,7 @@ class TestUserDeleteStage(TestCase): ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_flows:denied")}, ) @@ -83,7 +83,7 @@ class TestUserDeleteStage(TestCase): ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) diff --git a/passbook/stages/user_login/tests.py b/passbook/stages/user_login/tests.py index 26212a355..978ae64d7 100644 --- a/passbook/stages/user_login/tests.py +++ b/passbook/stages/user_login/tests.py @@ -1,7 +1,7 @@ """login tests""" from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.markers import StageMarker @@ -50,7 +50,7 @@ class TestUserLoginStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) @@ -71,7 +71,7 @@ class TestUserLoginStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_flows:denied")}, ) @@ -93,7 +93,7 @@ class TestUserLoginStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_flows:denied")}, ) diff --git a/passbook/stages/user_logout/tests.py b/passbook/stages/user_logout/tests.py index 394093509..54b35760c 100644 --- a/passbook/stages/user_logout/tests.py +++ b/passbook/stages/user_logout/tests.py @@ -1,7 +1,7 @@ """logout tests""" from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.markers import StageMarker @@ -50,7 +50,7 @@ class TestUserLogoutStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) diff --git a/passbook/stages/user_write/tests.py b/passbook/stages/user_write/tests.py index 2c3b84aa2..8ead85bdb 100644 --- a/passbook/stages/user_write/tests.py +++ b/passbook/stages/user_write/tests.py @@ -4,7 +4,7 @@ from random import SystemRandom from django.shortcuts import reverse from django.test import Client, TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from passbook.core.models import User from passbook.flows.markers import StageMarker @@ -59,7 +59,7 @@ class TestUserWriteStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) user_qs = User.objects.filter( @@ -97,7 +97,7 @@ class TestUserWriteStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_core:overview")}, ) user_qs = User.objects.filter( @@ -124,7 +124,7 @@ class TestUserWriteStage(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual( - force_text(response.content), + force_str(response.content), {"type": "redirect", "to": reverse("passbook_flows:denied")}, ) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..56d9dad2f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +DJANGO_SETTINGS_MODULE = passbook.root.settings +# -- recommended but optional: +python_files = tests.py test_*.py *_tests.py +junit_family = xunit2 +addopts = -p no:celery --junitxml=unittest.xml