From 3e7ea01a421ff62baba855b77f95ae974541a8d7 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 11:00:55 +0000 Subject: [PATCH 01/81] test(tokens-repo): make empty_keys fixture (and derived) shareable --- .../empty_keys.json | 9 -------- .../test_repository/test_tokens_repository.py | 23 +++++++++++++++---- .../test_tokens_repository/empty_keys.json | 9 -------- 3 files changed, 19 insertions(+), 22 deletions(-) delete mode 100644 tests/test_graphql/test_repository/test_json_tokens_repository/empty_keys.json delete mode 100644 tests/test_graphql/test_repository/test_tokens_repository/empty_keys.json diff --git a/tests/test_graphql/test_repository/test_json_tokens_repository/empty_keys.json b/tests/test_graphql/test_repository/test_json_tokens_repository/empty_keys.json deleted file mode 100644 index 2131ddf..0000000 --- a/tests/test_graphql/test_repository/test_json_tokens_repository/empty_keys.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tokens": [ - { - "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", - "name": "primary_token", - "date": "2022-07-15 17:41:31.675698" - } - ] -} diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 020a868..8b8b089 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -32,22 +32,37 @@ ORIGINAL_DEVICE_NAMES = [ "forth_token", ] +EMPTY_KEYS_JSON = """ +{ + "tokens": [ + { + "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", + "name": "primary_token", + "date": "2022-07-15 17:41:31.675698" + } + ] +} +""" + def mnemonic_from_hex(hexkey): return Mnemonic(language="english").to_mnemonic(bytes.fromhex(hexkey)) @pytest.fixture -def empty_keys(mocker, datadir): - mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=datadir / "empty_keys.json") - assert read_json(datadir / "empty_keys.json")["tokens"] == [ +def empty_keys(mocker, tmpdir): + tokens_file = tmpdir / "empty_keys.json" + with open(tokens_file, "w") as file: + file.write(EMPTY_KEYS_JSON) + mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=tokens_file) + assert read_json(tokens_file)["tokens"] == [ { "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", "name": "primary_token", "date": "2022-07-15 17:41:31.675698", } ] - return datadir + return tmpdir @pytest.fixture diff --git a/tests/test_graphql/test_repository/test_tokens_repository/empty_keys.json b/tests/test_graphql/test_repository/test_tokens_repository/empty_keys.json deleted file mode 100644 index 2131ddf..0000000 --- a/tests/test_graphql/test_repository/test_tokens_repository/empty_keys.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tokens": [ - { - "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", - "name": "primary_token", - "date": "2022-07-15 17:41:31.675698" - } - ] -} From 8065921862fa986a5aec387f76bea950b851e7c2 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 11:09:24 +0000 Subject: [PATCH 02/81] test(tokens-repo): make empty_tokens fixture, even more minimal --- .../test_repository/test_tokens_repository.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 8b8b089..ee1b9e0 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -44,6 +44,8 @@ EMPTY_KEYS_JSON = """ } """ +EMPTY_TOKENS_JSON = ' {"tokens": []}' + def mnemonic_from_hex(hexkey): return Mnemonic(language="english").to_mnemonic(bytes.fromhex(hexkey)) @@ -65,6 +67,16 @@ def empty_keys(mocker, tmpdir): return tmpdir +@pytest.fixture +def empty_tokens(mocker, tmpdir): + tokens_file = tmpdir / "empty_tokens.json" + with open(tokens_file, "w") as file: + file.write(EMPTY_TOKENS_JSON) + mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=tokens_file) + assert read_json(tokens_file)["tokens"] == [] + return tmpdir + + @pytest.fixture def mock_new_device_key_generate(mocker): mock = mocker.patch( @@ -153,7 +165,7 @@ def mock_recovery_key_generate(mocker): @pytest.fixture -def empty_json_repo(empty_keys): +def empty_json_repo(empty_tokens): repo = JsonTokensRepository() for token in repo.get_tokens(): repo.delete_token(token) From 889c7eee6a29daf5a230fd5abaa8ce9a100386b7 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 11:14:36 +0000 Subject: [PATCH 03/81] test(tokens-repo): offload empty_keys fixture to json tests --- .../test_json_tokens_repository.py | 29 ++++++++++++++++++- .../test_repository/test_tokens_repository.py | 27 ----------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/test_graphql/test_repository/test_json_tokens_repository.py b/tests/test_graphql/test_repository/test_json_tokens_repository.py index af8c844..23df9df 100644 --- a/tests/test_graphql/test_repository/test_json_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_json_tokens_repository.py @@ -25,7 +25,6 @@ from test_tokens_repository import ( mock_recovery_key_generate, mock_generate_token, mock_new_device_key_generate, - empty_keys, ) ORIGINAL_TOKEN_CONTENT = [ @@ -51,6 +50,18 @@ ORIGINAL_TOKEN_CONTENT = [ }, ] +EMPTY_KEYS_JSON = """ +{ + "tokens": [ + { + "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", + "name": "primary_token", + "date": "2022-07-15 17:41:31.675698" + } + ] +} +""" + @pytest.fixture def tokens(mocker, datadir): @@ -59,6 +70,22 @@ def tokens(mocker, datadir): return datadir +@pytest.fixture +def empty_keys(mocker, tmpdir): + tokens_file = tmpdir / "empty_keys.json" + with open(tokens_file, "w") as file: + file.write(EMPTY_KEYS_JSON) + mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=tokens_file) + assert read_json(tokens_file)["tokens"] == [ + { + "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", + "name": "primary_token", + "date": "2022-07-15 17:41:31.675698", + } + ] + return tmpdir + + @pytest.fixture def null_keys(mocker, datadir): mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=datadir / "null_keys.json") diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index ee1b9e0..b172f13 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -32,17 +32,6 @@ ORIGINAL_DEVICE_NAMES = [ "forth_token", ] -EMPTY_KEYS_JSON = """ -{ - "tokens": [ - { - "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", - "name": "primary_token", - "date": "2022-07-15 17:41:31.675698" - } - ] -} -""" EMPTY_TOKENS_JSON = ' {"tokens": []}' @@ -51,22 +40,6 @@ def mnemonic_from_hex(hexkey): return Mnemonic(language="english").to_mnemonic(bytes.fromhex(hexkey)) -@pytest.fixture -def empty_keys(mocker, tmpdir): - tokens_file = tmpdir / "empty_keys.json" - with open(tokens_file, "w") as file: - file.write(EMPTY_KEYS_JSON) - mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=tokens_file) - assert read_json(tokens_file)["tokens"] == [ - { - "token": "KG9ni-B-CMPk327Zv1qC7YBQaUGaBUcgdkvMvQ2atFI", - "name": "primary_token", - "date": "2022-07-15 17:41:31.675698", - } - ] - return tmpdir - - @pytest.fixture def empty_tokens(mocker, tmpdir): tokens_file = tmpdir / "empty_tokens.json" From e125f3a4b18f28e20bc292cd53779fea20decaec Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 12:44:51 +0000 Subject: [PATCH 04/81] test(tokens-repo): remove test tokens.json files except for one which will temporarily remain gitkeeps are to prevent shared_datadir from erroring out in a freshly cloned repo. for now huey database and jobs fixtures use shared_datadir --- tests/conftest.py | 15 +++++++++++---- tests/test_graphql/data/gitkeep | 0 tests/test_graphql/data/tokens.json | 14 -------------- tests/test_rest_endpoints/data/tokens.json | 14 -------------- .../test_rest_endpoints/services/data/tokens.json | 9 --------- tests/test_rest_endpoints/services/gitkeep | 0 6 files changed, 11 insertions(+), 41 deletions(-) create mode 100644 tests/test_graphql/data/gitkeep delete mode 100644 tests/test_graphql/data/tokens.json delete mode 100644 tests/test_rest_endpoints/data/tokens.json delete mode 100644 tests/test_rest_endpoints/services/data/tokens.json create mode 100644 tests/test_rest_endpoints/services/gitkeep diff --git a/tests/conftest.py b/tests/conftest.py index ea7a66a..4b65d20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,18 +4,25 @@ import os import pytest from fastapi.testclient import TestClient +from shutil import copy +import os.path as path def pytest_generate_tests(metafunc): os.environ["TEST_MODE"] = "true" +def global_data_dir(): + return path.join(path.dirname(__file__), "data") + + @pytest.fixture -def tokens_file(mocker, shared_datadir): +def tokens_file(mocker, tmpdir): """Mock tokens file.""" - mock = mocker.patch( - "selfprivacy_api.utils.TOKENS_FILE", shared_datadir / "tokens.json" - ) + tmp_file = tmpdir / "tokens.json" + source_file = path.join(global_data_dir(), "tokens.json") + copy(source_file, tmp_file) + mock = mocker.patch("selfprivacy_api.utils.TOKENS_FILE", tmp_file) return mock diff --git a/tests/test_graphql/data/gitkeep b/tests/test_graphql/data/gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_graphql/data/tokens.json b/tests/test_graphql/data/tokens.json deleted file mode 100644 index 9be9d02..0000000 --- a/tests/test_graphql/data/tokens.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "test_token", - "date": "2022-01-14 08:31:10.789314" - }, - { - "token": "TEST_TOKEN2", - "name": "test_token2", - "date": "2022-01-14 08:31:10.789314" - } - ] -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/data/tokens.json b/tests/test_rest_endpoints/data/tokens.json deleted file mode 100644 index 9be9d02..0000000 --- a/tests/test_rest_endpoints/data/tokens.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "test_token", - "date": "2022-01-14 08:31:10.789314" - }, - { - "token": "TEST_TOKEN2", - "name": "test_token2", - "date": "2022-01-14 08:31:10.789314" - } - ] -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/data/tokens.json b/tests/test_rest_endpoints/services/data/tokens.json deleted file mode 100644 index 9d35420..0000000 --- a/tests/test_rest_endpoints/services/data/tokens.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "Test Token", - "date": "2022-01-14 08:31:10.789314" - } - ] -} \ No newline at end of file diff --git a/tests/test_rest_endpoints/services/gitkeep b/tests/test_rest_endpoints/services/gitkeep new file mode 100644 index 0000000..e69de29 From f542c1e6c78f63de07cfcc0c6d9fb4976d27644c Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 14:51:25 +0000 Subject: [PATCH 05/81] test(tokens-repo): break out assert_original() in rest --- tests/test_rest_endpoints/test_auth.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 12de0cf..bb322e9 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -37,6 +37,10 @@ DATE_FORMATS = [ ] +def assert_original(filename): + assert read_json(filename) == TOKENS_FILE_CONTETS + + def test_get_tokens_info(authorized_client, tokens_file): response = authorized_client.get("/auth/tokens") assert response.status_code == 200 @@ -58,7 +62,7 @@ def test_get_tokens_unauthorized(client, tokens_file): def test_delete_token_unauthorized(client, tokens_file): response = client.delete("/auth/tokens") assert response.status_code == 401 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_delete_token(authorized_client, tokens_file): @@ -82,7 +86,7 @@ def test_delete_self_token(authorized_client, tokens_file): "/auth/tokens", json={"token_name": "test_token"} ) assert response.status_code == 400 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_delete_nonexistent_token(authorized_client, tokens_file): @@ -90,13 +94,13 @@ def test_delete_nonexistent_token(authorized_client, tokens_file): "/auth/tokens", json={"token_name": "test_token3"} ) assert response.status_code == 404 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_refresh_token_unauthorized(client, tokens_file): response = client.post("/auth/tokens") assert response.status_code == 401 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_refresh_token(authorized_client, tokens_file): @@ -112,7 +116,7 @@ def test_refresh_token(authorized_client, tokens_file): def test_get_new_device_auth_token_unauthorized(client, tokens_file): response = client.post("/auth/new_device") assert response.status_code == 401 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_get_new_device_auth_token(authorized_client, tokens_file): @@ -133,13 +137,13 @@ def test_get_and_delete_new_device_token(authorized_client, tokens_file): "/auth/new_device", json={"token": response.json()["token"]} ) assert response.status_code == 200 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_delete_token_unauthenticated(client, tokens_file): response = client.delete("/auth/new_device") assert response.status_code == 401 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_get_and_authorize_new_device(client, authorized_client, tokens_file): @@ -163,7 +167,7 @@ def test_authorize_new_device_with_invalid_token(client, tokens_file): json={"token": "invalid_token", "device": "new_device"}, ) assert response.status_code == 404 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_get_and_authorize_used_token(client, authorized_client, tokens_file): @@ -214,7 +218,7 @@ def test_authorize_without_token(client, tokens_file): json={"device": "new_device"}, ) assert response.status_code == 422 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) # Recovery tokens @@ -243,7 +247,7 @@ def test_authorize_without_token(client, tokens_file): def test_get_recovery_token_status_unauthorized(client, tokens_file): response = client.get("/auth/recovery_token") assert response.status_code == 401 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_get_recovery_token_when_none_exists(authorized_client, tokens_file): @@ -256,7 +260,7 @@ def test_get_recovery_token_when_none_exists(authorized_client, tokens_file): "expiration": None, "uses_left": None, } - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(tokens_file) def test_generate_recovery_token(authorized_client, client, tokens_file): From 7e0e6015cf2412cf9017f2acd6bc6dc2c2181add Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 15:16:58 +0000 Subject: [PATCH 06/81] test(tokens-repo): break out rest_get_token_info() --- tests/test_rest_endpoints/test_auth.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index bb322e9..bd4efae 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -41,10 +41,14 @@ def assert_original(filename): assert read_json(filename) == TOKENS_FILE_CONTETS -def test_get_tokens_info(authorized_client, tokens_file): - response = authorized_client.get("/auth/tokens") +def rest_get_tokens_info(client): + response = client.get("/auth/tokens") assert response.status_code == 200 - assert response.json() == [ + return response.json() + + +def test_get_tokens_info(authorized_client, tokens_file): + assert rest_get_tokens_info(authorized_client) == [ {"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True}, { "name": "test_token2", From 270e569af22e07512e7177f59e26fae0ae479d1d Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 15:26:42 +0000 Subject: [PATCH 07/81] test(tokens-repo): use rest token info in token deletion test --- tests/test_rest_endpoints/test_auth.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index bd4efae..6199265 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -74,15 +74,9 @@ def test_delete_token(authorized_client, tokens_file): "/auth/tokens", json={"token_name": "test_token2"} ) assert response.status_code == 200 - assert read_json(tokens_file) == { - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "test_token", - "date": "2022-01-14 08:31:10.789314", - } - ] - } + assert rest_get_tokens_info(authorized_client) == [ + {"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True} + ] def test_delete_self_token(authorized_client, tokens_file): From 07fe2f8a558b6ada7896d157844e4f5273f40fcb Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 15:43:43 +0000 Subject: [PATCH 08/81] test(tokens-repo): check refreshed token validity by trying to auth --- tests/test_rest_endpoints/test_auth.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 6199265..44b543d 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -5,11 +5,6 @@ import datetime import pytest from mnemonic import Mnemonic -from selfprivacy_api.repositories.tokens.json_tokens_repository import ( - JsonTokensRepository, -) - -TOKEN_REPO = JsonTokensRepository() from tests.common import read_json, write_json @@ -105,7 +100,8 @@ def test_refresh_token(authorized_client, tokens_file): response = authorized_client.post("/auth/tokens") assert response.status_code == 200 new_token = response.json()["token"] - assert TOKEN_REPO.get_token_by_token_string(new_token) is not None + authorized_client.headers.update({"Authorization": "Bearer " + new_token}) + assert rest_get_tokens_info(authorized_client) is not None # new device From 1d6275b75bbfc88d5a08a9136986db9ab939f803 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 15:48:40 +0000 Subject: [PATCH 09/81] test(tokens-repo): delete standalone get new device test At rest api level, we can only check the existence of new device token by using it, and this test already exists. --- tests/test_rest_endpoints/test_auth.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 44b543d..3d6b256 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -113,14 +113,6 @@ def test_get_new_device_auth_token_unauthorized(client, tokens_file): assert_original(tokens_file) -def test_get_new_device_auth_token(authorized_client, tokens_file): - response = authorized_client.post("/auth/new_device") - assert response.status_code == 200 - assert "token" in response.json() - token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex() - assert read_json(tokens_file)["new_device"]["token"] == token - - def test_get_and_delete_new_device_token(authorized_client, tokens_file): response = authorized_client.post("/auth/new_device") assert response.status_code == 200 From 179078aed2060f36dce6770fe2bfd1d74a899bd8 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 16:17:06 +0000 Subject: [PATCH 10/81] test(tokens-repo): break out getting new device token --- tests/test_rest_endpoints/test_auth.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 3d6b256..b8e1292 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -132,15 +132,20 @@ def test_delete_token_unauthenticated(client, tokens_file): assert_original(tokens_file) -def test_get_and_authorize_new_device(client, authorized_client, tokens_file): - response = authorized_client.post("/auth/new_device") +def rest_get_new_device_token(client): + response = client.post("/auth/new_device") assert response.status_code == 200 assert "token" in response.json() - token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex() - assert read_json(tokens_file)["new_device"]["token"] == token + return response.json()["token"] + + +def test_get_and_authorize_new_device(client, authorized_client, tokens_file): response = client.post( "/auth/new_device/authorize", - json={"token": response.json()["token"], "device": "new_device"}, + json={ + "token": rest_get_new_device_token(authorized_client), + "device": "new_device", + }, ) assert response.status_code == 200 assert read_json(tokens_file)["tokens"][2]["token"] == response.json()["token"] @@ -157,21 +162,17 @@ def test_authorize_new_device_with_invalid_token(client, tokens_file): def test_get_and_authorize_used_token(client, authorized_client, tokens_file): - response = authorized_client.post("/auth/new_device") - assert response.status_code == 200 - assert "token" in response.json() - token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex() - assert read_json(tokens_file)["new_device"]["token"] == token + token_to_be_used_2_times = rest_get_new_device_token(authorized_client) response = client.post( "/auth/new_device/authorize", - json={"token": response.json()["token"], "device": "new_device"}, + json={"token": token_to_be_used_2_times, "device": "new_device"}, ) assert response.status_code == 200 assert read_json(tokens_file)["tokens"][2]["token"] == response.json()["token"] assert read_json(tokens_file)["tokens"][2]["name"] == "new_device" response = client.post( "/auth/new_device/authorize", - json={"token": response.json()["token"], "device": "new_device"}, + json={"token": token_to_be_used_2_times, "device": "new_device"}, ) assert response.status_code == 404 From bfcec3d51de58746ab765595d26d3eb6795119f3 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 16:27:08 +0000 Subject: [PATCH 11/81] test(tokens-repo): break out checking token validity --- tests/test_rest_endpoints/test_auth.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index b8e1292..9467f49 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -36,6 +36,11 @@ def assert_original(filename): assert read_json(filename) == TOKENS_FILE_CONTETS +def assert_token_valid(client, token): + client.headers.update({"Authorization": "Bearer " + token}) + assert rest_get_tokens_info(client) is not None + + def rest_get_tokens_info(client): response = client.get("/auth/tokens") assert response.status_code == 200 @@ -100,8 +105,7 @@ def test_refresh_token(authorized_client, tokens_file): response = authorized_client.post("/auth/tokens") assert response.status_code == 200 new_token = response.json()["token"] - authorized_client.headers.update({"Authorization": "Bearer " + new_token}) - assert rest_get_tokens_info(authorized_client) is not None + assert_token_valid(authorized_client, new_token) # new device @@ -148,8 +152,7 @@ def test_get_and_authorize_new_device(client, authorized_client, tokens_file): }, ) assert response.status_code == 200 - assert read_json(tokens_file)["tokens"][2]["token"] == response.json()["token"] - assert read_json(tokens_file)["tokens"][2]["name"] == "new_device" + assert_token_valid(authorized_client, response.json()["token"]) def test_authorize_new_device_with_invalid_token(client, tokens_file): @@ -168,8 +171,7 @@ def test_get_and_authorize_used_token(client, authorized_client, tokens_file): json={"token": token_to_be_used_2_times, "device": "new_device"}, ) assert response.status_code == 200 - assert read_json(tokens_file)["tokens"][2]["token"] == response.json()["token"] - assert read_json(tokens_file)["tokens"][2]["name"] == "new_device" + assert_token_valid(authorized_client, response.json()["token"]) response = client.post( "/auth/new_device/authorize", json={"token": token_to_be_used_2_times, "device": "new_device"}, From 458c4fd28aeeb116b73fe85885be4eb0e26ca2b0 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 16:37:34 +0000 Subject: [PATCH 12/81] test(tokens-repo): make new device tests a bit more readable --- tests/test_rest_endpoints/test_auth.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 9467f49..93be5ee 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -118,14 +118,8 @@ def test_get_new_device_auth_token_unauthorized(client, tokens_file): def test_get_and_delete_new_device_token(authorized_client, tokens_file): - response = authorized_client.post("/auth/new_device") - assert response.status_code == 200 - assert "token" in response.json() - token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex() - assert read_json(tokens_file)["new_device"]["token"] == token - response = authorized_client.delete( - "/auth/new_device", json={"token": response.json()["token"]} - ) + token = rest_get_new_device_token(authorized_client) + response = authorized_client.delete("/auth/new_device", json={"token": token}) assert response.status_code == 200 assert_original(tokens_file) @@ -144,10 +138,11 @@ def rest_get_new_device_token(client): def test_get_and_authorize_new_device(client, authorized_client, tokens_file): + token = rest_get_new_device_token(authorized_client) response = client.post( "/auth/new_device/authorize", json={ - "token": rest_get_new_device_token(authorized_client), + "token": token, "device": "new_device", }, ) @@ -172,6 +167,7 @@ def test_get_and_authorize_used_token(client, authorized_client, tokens_file): ) assert response.status_code == 200 assert_token_valid(authorized_client, response.json()["token"]) + response = client.post( "/auth/new_device/authorize", json={"token": token_to_be_used_2_times, "device": "new_device"}, @@ -182,11 +178,7 @@ def test_get_and_authorize_used_token(client, authorized_client, tokens_file): def test_get_and_authorize_token_after_12_minutes( client, authorized_client, tokens_file ): - response = authorized_client.post("/auth/new_device") - assert response.status_code == 200 - assert "token" in response.json() - token = Mnemonic(language="english").to_entropy(response.json()["token"]).hex() - assert read_json(tokens_file)["new_device"]["token"] == token + token = rest_get_new_device_token(authorized_client) file_data = read_json(tokens_file) file_data["new_device"]["expiration"] = str( @@ -196,7 +188,7 @@ def test_get_and_authorize_token_after_12_minutes( response = client.post( "/auth/new_device/authorize", - json={"token": response.json()["token"], "device": "new_device"}, + json={"token": token, "device": "new_device"}, ) assert response.status_code == 404 From 0bf18603d4f3b49c2be6c5648717428d76532cab Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 28 Dec 2022 17:09:19 +0000 Subject: [PATCH 13/81] test(tokens-repo): travel in time to check expiration --- tests/test_rest_endpoints/test_auth.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 93be5ee..f428904 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -31,6 +31,9 @@ DATE_FORMATS = [ "%Y-%m-%d %H:%M:%S.%f", ] +# for expiration tests. If headache, consider freezegun +DEVICE_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.new_device_key.datetime" + def assert_original(filename): assert read_json(filename) == TOKENS_FILE_CONTETS @@ -176,15 +179,19 @@ def test_get_and_authorize_used_token(client, authorized_client, tokens_file): def test_get_and_authorize_token_after_12_minutes( - client, authorized_client, tokens_file + client, authorized_client, tokens_file, mocker ): token = rest_get_new_device_token(authorized_client) - file_data = read_json(tokens_file) - file_data["new_device"]["expiration"] = str( - datetime.datetime.now() - datetime.timedelta(minutes=13) - ) - write_json(tokens_file, file_data) + # TARDIS sounds + new_time = datetime.datetime.now() + datetime.timedelta(minutes=13) + + class warped_spacetime(datetime.datetime): + @classmethod + def now(cls): + return new_time + + mock = mocker.patch(DEVICE_KEY_VALIDATION_DATETIME, warped_spacetime) response = client.post( "/auth/new_device/authorize", From 74777c4343840c7421426d8a8c3631ce54de6ea2 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 10:13:31 +0000 Subject: [PATCH 14/81] test(tokens-repo): fix typo in contets --- tests/test_rest_endpoints/test_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index f428904..80cc2eb 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -9,7 +9,7 @@ from mnemonic import Mnemonic from tests.common import read_json, write_json -TOKENS_FILE_CONTETS = { +TOKENS_FILE_CONTENTS = { "tokens": [ { "token": "TEST_TOKEN", @@ -36,7 +36,7 @@ DEVICE_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.new_device_key.d def assert_original(filename): - assert read_json(filename) == TOKENS_FILE_CONTETS + assert read_json(filename) == TOKENS_FILE_CONTENTS def assert_token_valid(client, token): From 0239f3174eab652e1e7fe7f6f7d33ee9976c3a53 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 10:27:51 +0000 Subject: [PATCH 15/81] test(tokens-repo): do not read json in generate recovery test --- tests/test_rest_endpoints/test_auth.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 80cc2eb..7d1c88f 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -257,21 +257,19 @@ def test_generate_recovery_token(authorized_client, client, tokens_file): assert response.status_code == 200 assert "token" in response.json() mnemonic_token = response.json()["token"] - token = Mnemonic(language="english").to_entropy(mnemonic_token).hex() - assert read_json(tokens_file)["recovery_token"]["token"] == token - time_generated = read_json(tokens_file)["recovery_token"]["date"] - assert time_generated is not None + # Try to get token status + response = authorized_client.get("/auth/recovery_token") + assert response.status_code == 200 + assert "date" in response.json() + time_generated = response.json()["date"] + # Assert that the token was generated near the current time assert ( datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - datetime.timedelta(seconds=5) < datetime.datetime.now() ) - - # Try to get token status - response = authorized_client.get("/auth/recovery_token") - assert response.status_code == 200 assert response.json() == { "exists": True, "valid": True, @@ -287,8 +285,7 @@ def test_generate_recovery_token(authorized_client, client, tokens_file): ) assert recovery_response.status_code == 200 new_token = recovery_response.json()["token"] - assert read_json(tokens_file)["tokens"][2]["token"] == new_token - assert read_json(tokens_file)["tokens"][2]["name"] == "recovery_device" + assert_token_valid(authorized_client, new_token) # Try to use token again recovery_response = client.post( @@ -297,8 +294,7 @@ def test_generate_recovery_token(authorized_client, client, tokens_file): ) assert recovery_response.status_code == 200 new_token = recovery_response.json()["token"] - assert read_json(tokens_file)["tokens"][3]["token"] == new_token - assert read_json(tokens_file)["tokens"][3]["name"] == "recovery_device2" + assert_token_valid(authorized_client, new_token) @pytest.mark.parametrize("timeformat", DATE_FORMATS) From 548f47963ad8416a9023733b72c48f667a3cf22d Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 10:52:00 +0000 Subject: [PATCH 16/81] test(tokens-repo): break out obtaining recovery tokens --- tests/test_rest_endpoints/test_auth.py | 36 ++++++++++++++------------ 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 7d1c88f..49a1f3b 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -251,12 +251,25 @@ def test_get_recovery_token_when_none_exists(authorized_client, tokens_file): assert_original(tokens_file) -def test_generate_recovery_token(authorized_client, client, tokens_file): - # Generate token without expiration and uses_left - response = authorized_client.post("/auth/recovery_token") +def rest_make_recovery_token(client, expires_at=None, timeformat=None): + if expires_at is None: + response = client.post("/auth/recovery_token") + else: + assert timeformat is not None + expires_at_str = expires_at.strftime(timeformat) + response = client.post( + "/auth/recovery_token", + json={"expiration": expires_at_str}, + ) + assert response.status_code == 200 assert "token" in response.json() - mnemonic_token = response.json()["token"] + return response.json()["token"] + + +def test_generate_recovery_token(authorized_client, client, tokens_file): + # Generate token without expiration and uses_left + mnemonic_token = rest_make_recovery_token(authorized_client) # Try to get token status response = authorized_client.get("/auth/recovery_token") @@ -304,20 +317,9 @@ def test_generate_recovery_token_with_expiration_date( # Generate token with expiration date # Generate expiration date in the future expiration_date = datetime.datetime.now() + datetime.timedelta(minutes=5) - expiration_date_str = expiration_date.strftime(timeformat) - response = authorized_client.post( - "/auth/recovery_token", - json={"expiration": expiration_date_str}, + mnemonic_token = rest_make_recovery_token( + authorized_client, expires_at=expiration_date, timeformat=timeformat ) - assert response.status_code == 200 - assert "token" in response.json() - mnemonic_token = response.json()["token"] - token = Mnemonic(language="english").to_entropy(mnemonic_token).hex() - assert read_json(tokens_file)["recovery_token"]["token"] == token - assert datetime.datetime.strptime( - read_json(tokens_file)["recovery_token"]["expiration"], "%Y-%m-%dT%H:%M:%S.%f" - ) == datetime.datetime.strptime(expiration_date_str, timeformat) - time_generated = read_json(tokens_file)["recovery_token"]["date"] assert time_generated is not None # Assert that the token was generated near the current time From ac4d4e012767c1e535ac426c5dd24728ef5bc173 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 11:14:34 +0000 Subject: [PATCH 17/81] test(tokens-repo): break out recovery time operations --- tests/test_rest_endpoints/test_auth.py | 47 ++++++++++++++------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 49a1f3b..1a3093c 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -267,23 +267,34 @@ def rest_make_recovery_token(client, expires_at=None, timeformat=None): return response.json()["token"] -def test_generate_recovery_token(authorized_client, client, tokens_file): - # Generate token without expiration and uses_left - mnemonic_token = rest_make_recovery_token(authorized_client) - - # Try to get token status - response = authorized_client.get("/auth/recovery_token") +def rest_get_recovery_status(client): + response = client.get("/auth/recovery_token") assert response.status_code == 200 - assert "date" in response.json() - time_generated = response.json()["date"] + return response.json() - # Assert that the token was generated near the current time + +def rest_get_recovery_date(client): + status = rest_get_recovery_status(client) + assert "date" in status + return status["date"] + + +def assert_recovery_recent(time_generated): assert ( datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - datetime.timedelta(seconds=5) < datetime.datetime.now() ) - assert response.json() == { + + +def test_generate_recovery_token(authorized_client, client, tokens_file): + # Generate token without expiration and uses_left + mnemonic_token = rest_make_recovery_token(authorized_client) + + time_generated = rest_get_recovery_date(authorized_client) + assert_recovery_recent(time_generated) + + assert rest_get_recovery_status(authorized_client) == { "exists": True, "valid": True, "date": time_generated, @@ -320,19 +331,11 @@ def test_generate_recovery_token_with_expiration_date( mnemonic_token = rest_make_recovery_token( authorized_client, expires_at=expiration_date, timeformat=timeformat ) - time_generated = read_json(tokens_file)["recovery_token"]["date"] - assert time_generated is not None - # Assert that the token was generated near the current time - assert ( - datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - - datetime.timedelta(seconds=5) - < datetime.datetime.now() - ) - # Try to get token status - response = authorized_client.get("/auth/recovery_token") - assert response.status_code == 200 - assert response.json() == { + time_generated = rest_get_recovery_date(authorized_client) + assert_recovery_recent(time_generated) + + assert rest_get_recovery_status(authorized_client) == { "exists": True, "valid": True, "date": time_generated, From 203940096c262be37d7d5f439c49b5862a0bc70a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 11:51:52 +0000 Subject: [PATCH 18/81] test(tokens-repo): break out recovery token use --- tests/test_rest_endpoints/test_auth.py | 31 ++++++++++++-------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 1a3093c..c426d54 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -287,6 +287,17 @@ def assert_recovery_recent(time_generated): ) +def rest_recover_with_mnemonic(client, mnemonic_token, device_name): + recovery_response = client.post( + "/auth/recovery_token/use", + json={"token": mnemonic_token, "device": device_name}, + ) + assert recovery_response.status_code == 200 + new_token = recovery_response.json()["token"] + assert_token_valid(client, new_token) + return new_token + + def test_generate_recovery_token(authorized_client, client, tokens_file): # Generate token without expiration and uses_left mnemonic_token = rest_make_recovery_token(authorized_client) @@ -302,23 +313,9 @@ def test_generate_recovery_token(authorized_client, client, tokens_file): "uses_left": None, } - # Try to use the token - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": "recovery_device"}, - ) - assert recovery_response.status_code == 200 - new_token = recovery_response.json()["token"] - assert_token_valid(authorized_client, new_token) - - # Try to use token again - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": "recovery_device2"}, - ) - assert recovery_response.status_code == 200 - new_token = recovery_response.json()["token"] - assert_token_valid(authorized_client, new_token) + rest_recover_with_mnemonic(client, mnemonic_token, "recover_device") + # And again + rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2") @pytest.mark.parametrize("timeformat", DATE_FORMATS) From e0bd6efcb2b5a546225e6f6c5f3cbc61b9a4929a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 12:01:04 +0000 Subject: [PATCH 19/81] test(tokens-repo): use new recovery functions --- tests/test_rest_endpoints/test_auth.py | 42 +++----------------------- 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index c426d54..bdfb579 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -340,25 +340,9 @@ def test_generate_recovery_token_with_expiration_date( "uses_left": None, } - # Try to use the token - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": "recovery_device"}, - ) - assert recovery_response.status_code == 200 - new_token = recovery_response.json()["token"] - assert read_json(tokens_file)["tokens"][2]["token"] == new_token - assert read_json(tokens_file)["tokens"][2]["name"] == "recovery_device" - - # Try to use token again - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": "recovery_device2"}, - ) - assert recovery_response.status_code == 200 - new_token = recovery_response.json()["token"] - assert read_json(tokens_file)["tokens"][3]["token"] == new_token - assert read_json(tokens_file)["tokens"][3]["name"] == "recovery_device2" + rest_recover_with_mnemonic(client, mnemonic_token, "recover_device") + # And again + rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2") # Try to use token after expiration date new_data = read_json(tokens_file) @@ -450,16 +434,7 @@ def test_generate_recovery_token_with_limited_uses( } # Try to use the token - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": "recovery_device"}, - ) - assert recovery_response.status_code == 200 - new_token = recovery_response.json()["token"] - assert read_json(tokens_file)["tokens"][2]["token"] == new_token - assert read_json(tokens_file)["tokens"][2]["name"] == "recovery_device" - - assert read_json(tokens_file)["recovery_token"]["uses_left"] == 1 + rest_recover_with_mnemonic(client, mnemonic_token, "recover_device") # Get the status of the token response = authorized_client.get("/auth/recovery_token") @@ -473,14 +448,7 @@ def test_generate_recovery_token_with_limited_uses( } # Try to use token again - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": "recovery_device2"}, - ) - assert recovery_response.status_code == 200 - new_token = recovery_response.json()["token"] - assert read_json(tokens_file)["tokens"][3]["token"] == new_token - assert read_json(tokens_file)["tokens"][3]["name"] == "recovery_device2" + rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2") # Get the status of the token response = authorized_client.get("/auth/recovery_token") From 3aa3d197e2af8180439c3adcea2e839b96f813f8 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 12:33:21 +0000 Subject: [PATCH 20/81] test(tokens-repo): use mock time for recovery tokens expiration test --- tests/test_rest_endpoints/test_auth.py | 44 ++++++++++---------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index bdfb579..309cc6c 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -32,9 +32,16 @@ DATE_FORMATS = [ ] # for expiration tests. If headache, consider freezegun +RECOVERY_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.recovery_key.datetime" DEVICE_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.new_device_key.datetime" +class NearFuture(datetime.datetime): + @classmethod + def now(cls): + return datetime.datetime.now() + datetime.timedelta(minutes=13) + + def assert_original(filename): assert read_json(filename) == TOKENS_FILE_CONTENTS @@ -184,14 +191,7 @@ def test_get_and_authorize_token_after_12_minutes( token = rest_get_new_device_token(authorized_client) # TARDIS sounds - new_time = datetime.datetime.now() + datetime.timedelta(minutes=13) - - class warped_spacetime(datetime.datetime): - @classmethod - def now(cls): - return new_time - - mock = mocker.patch(DEVICE_KEY_VALIDATION_DATETIME, warped_spacetime) + mock = mocker.patch(DEVICE_KEY_VALIDATION_DATETIME, NearFuture) response = client.post( "/auth/new_device/authorize", @@ -320,7 +320,7 @@ def test_generate_recovery_token(authorized_client, client, tokens_file): @pytest.mark.parametrize("timeformat", DATE_FORMATS) def test_generate_recovery_token_with_expiration_date( - authorized_client, client, tokens_file, timeformat + authorized_client, client, tokens_file, timeformat, mocker ): # Generate token with expiration date # Generate expiration date in the future @@ -345,29 +345,17 @@ def test_generate_recovery_token_with_expiration_date( rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2") # Try to use token after expiration date - new_data = read_json(tokens_file) - new_data["recovery_token"]["expiration"] = datetime.datetime.now().strftime( - "%Y-%m-%dT%H:%M:%S.%f" - ) - write_json(tokens_file, new_data) + mock = mocker.patch(RECOVERY_KEY_VALIDATION_DATETIME, NearFuture) + device_name = "recovery_device3" recovery_response = client.post( "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": "recovery_device3"}, + json={"token": mnemonic_token, "device": device_name}, ) assert recovery_response.status_code == 404 - # Assert that the token was not created in JSON - assert read_json(tokens_file)["tokens"] == new_data["tokens"] - - # Get the status of the token - response = authorized_client.get("/auth/recovery_token") - assert response.status_code == 200 - assert response.json() == { - "exists": True, - "valid": False, - "date": time_generated, - "expiration": new_data["recovery_token"]["expiration"], - "uses_left": None, - } + # Assert that the token was not created + assert device_name not in [ + token["name"] for token in rest_get_tokens_info(authorized_client) + ] @pytest.mark.parametrize("timeformat", DATE_FORMATS) From 42fa5fe524c174abd29223ceabb8e38bd4df377b Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 12:52:12 +0000 Subject: [PATCH 21/81] test(tokens-repo): allow ading uses in a helper recovery function --- tests/test_rest_endpoints/test_auth.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 309cc6c..33eb76a 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -251,15 +251,23 @@ def test_get_recovery_token_when_none_exists(authorized_client, tokens_file): assert_original(tokens_file) -def rest_make_recovery_token(client, expires_at=None, timeformat=None): - if expires_at is None: - response = client.post("/auth/recovery_token") - else: +def rest_make_recovery_token(client, expires_at=None, timeformat=None, uses=None): + json = {} + + if expires_at is not None: assert timeformat is not None expires_at_str = expires_at.strftime(timeformat) + json["expiration"] = expires_at_str + + if uses is not None: + json["uses"] = uses + + if json == {}: + response = client.post("/auth/recovery_token") + else: response = client.post( "/auth/recovery_token", - json={"expiration": expires_at_str}, + json=json, ) assert response.status_code == 200 From 02bfffa75fdb578058ab6269a6d8d8692c2dd221 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 13:04:57 +0000 Subject: [PATCH 22/81] test(tokens-repo): refactor the rest of auth tests --- tests/test_rest_endpoints/test_auth.py | 46 ++++++-------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 33eb76a..7e55900 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -378,7 +378,7 @@ def test_generate_recovery_token_with_expiration_in_the_past( json={"expiration": expiration_date_str}, ) assert response.status_code == 400 - assert "recovery_token" not in read_json(tokens_file) + assert not rest_get_recovery_status(authorized_client)["exists"] def test_generate_recovery_token_with_invalid_time_format( @@ -391,37 +391,19 @@ def test_generate_recovery_token_with_invalid_time_format( json={"expiration": expiration_date}, ) assert response.status_code == 422 - assert "recovery_token" not in read_json(tokens_file) + assert not rest_get_recovery_status(authorized_client)["exists"] def test_generate_recovery_token_with_limited_uses( authorized_client, client, tokens_file ): # Generate token with limited uses - response = authorized_client.post( - "/auth/recovery_token", - json={"uses": 2}, - ) - assert response.status_code == 200 - assert "token" in response.json() - mnemonic_token = response.json()["token"] - token = Mnemonic(language="english").to_entropy(mnemonic_token).hex() - assert read_json(tokens_file)["recovery_token"]["token"] == token - assert read_json(tokens_file)["recovery_token"]["uses_left"] == 2 + mnemonic_token = rest_make_recovery_token(authorized_client, uses=2) - # Get the date of the token - time_generated = read_json(tokens_file)["recovery_token"]["date"] - assert time_generated is not None - assert ( - datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - - datetime.timedelta(seconds=5) - < datetime.datetime.now() - ) + time_generated = rest_get_recovery_date(authorized_client) + assert_recovery_recent(time_generated) - # Try to get token status - response = authorized_client.get("/auth/recovery_token") - assert response.status_code == 200 - assert response.json() == { + assert rest_get_recovery_status(authorized_client) == { "exists": True, "valid": True, "date": time_generated, @@ -432,10 +414,7 @@ def test_generate_recovery_token_with_limited_uses( # Try to use the token rest_recover_with_mnemonic(client, mnemonic_token, "recover_device") - # Get the status of the token - response = authorized_client.get("/auth/recovery_token") - assert response.status_code == 200 - assert response.json() == { + assert rest_get_recovery_status(authorized_client) == { "exists": True, "valid": True, "date": time_generated, @@ -446,10 +425,7 @@ def test_generate_recovery_token_with_limited_uses( # Try to use token again rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2") - # Get the status of the token - response = authorized_client.get("/auth/recovery_token") - assert response.status_code == 200 - assert response.json() == { + assert rest_get_recovery_status(authorized_client) == { "exists": True, "valid": False, "date": time_generated, @@ -464,8 +440,6 @@ def test_generate_recovery_token_with_limited_uses( ) assert recovery_response.status_code == 404 - assert read_json(tokens_file)["recovery_token"]["uses_left"] == 0 - def test_generate_recovery_token_with_negative_uses( authorized_client, client, tokens_file @@ -476,7 +450,7 @@ def test_generate_recovery_token_with_negative_uses( json={"uses": -2}, ) assert response.status_code == 400 - assert "recovery_token" not in read_json(tokens_file) + assert not rest_get_recovery_status(authorized_client)["exists"] def test_generate_recovery_token_with_zero_uses(authorized_client, client, tokens_file): @@ -486,4 +460,4 @@ def test_generate_recovery_token_with_zero_uses(authorized_client, client, token json={"uses": 0}, ) assert response.status_code == 400 - assert "recovery_token" not in read_json(tokens_file) + assert not rest_get_recovery_status(authorized_client)["exists"] From e55a55ef6f80c0df355a58f75857276c61029e5b Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 30 Dec 2022 13:11:10 +0000 Subject: [PATCH 23/81] test(tokens-repo): beautify test_auth.py --- tests/test_rest_endpoints/test_auth.py | 115 +++++++++++++------------ 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 7e55900..17585fb 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -57,6 +57,64 @@ def rest_get_tokens_info(client): return response.json() +def rest_make_recovery_token(client, expires_at=None, timeformat=None, uses=None): + json = {} + + if expires_at is not None: + assert timeformat is not None + expires_at_str = expires_at.strftime(timeformat) + json["expiration"] = expires_at_str + + if uses is not None: + json["uses"] = uses + + if json == {}: + response = client.post("/auth/recovery_token") + else: + response = client.post( + "/auth/recovery_token", + json=json, + ) + + assert response.status_code == 200 + assert "token" in response.json() + return response.json()["token"] + + +def rest_get_recovery_status(client): + response = client.get("/auth/recovery_token") + assert response.status_code == 200 + return response.json() + + +def rest_get_recovery_date(client): + status = rest_get_recovery_status(client) + assert "date" in status + return status["date"] + + +def assert_recovery_recent(time_generated): + assert ( + datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") + - datetime.timedelta(seconds=5) + < datetime.datetime.now() + ) + + +def rest_recover_with_mnemonic(client, mnemonic_token, device_name): + recovery_response = client.post( + "/auth/recovery_token/use", + json={"token": mnemonic_token, "device": device_name}, + ) + assert recovery_response.status_code == 200 + new_token = recovery_response.json()["token"] + assert_token_valid(client, new_token) + return new_token + + +# Tokens + + def test_get_tokens_info(authorized_client, tokens_file): assert rest_get_tokens_info(authorized_client) == [ {"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True}, @@ -118,7 +176,7 @@ def test_refresh_token(authorized_client, tokens_file): assert_token_valid(authorized_client, new_token) -# new device +# New device def test_get_new_device_auth_token_unauthorized(client, tokens_file): @@ -251,61 +309,6 @@ def test_get_recovery_token_when_none_exists(authorized_client, tokens_file): assert_original(tokens_file) -def rest_make_recovery_token(client, expires_at=None, timeformat=None, uses=None): - json = {} - - if expires_at is not None: - assert timeformat is not None - expires_at_str = expires_at.strftime(timeformat) - json["expiration"] = expires_at_str - - if uses is not None: - json["uses"] = uses - - if json == {}: - response = client.post("/auth/recovery_token") - else: - response = client.post( - "/auth/recovery_token", - json=json, - ) - - assert response.status_code == 200 - assert "token" in response.json() - return response.json()["token"] - - -def rest_get_recovery_status(client): - response = client.get("/auth/recovery_token") - assert response.status_code == 200 - return response.json() - - -def rest_get_recovery_date(client): - status = rest_get_recovery_status(client) - assert "date" in status - return status["date"] - - -def assert_recovery_recent(time_generated): - assert ( - datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - - datetime.timedelta(seconds=5) - < datetime.datetime.now() - ) - - -def rest_recover_with_mnemonic(client, mnemonic_token, device_name): - recovery_response = client.post( - "/auth/recovery_token/use", - json={"token": mnemonic_token, "device": device_name}, - ) - assert recovery_response.status_code == 200 - new_token = recovery_response.json()["token"] - assert_token_valid(client, new_token) - return new_token - - def test_generate_recovery_token(authorized_client, client, tokens_file): # Generate token without expiration and uses_left mnemonic_token = rest_make_recovery_token(authorized_client) From f45567b87b9d99caef0214cb38cb25edebb0f46c Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 2 Jan 2023 14:33:48 +0000 Subject: [PATCH 24/81] fix(tokens-repo): readd gitkeep to services data folder after rebase --- tests/test_rest_endpoints/services/{ => data}/gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/test_rest_endpoints/services/{ => data}/gitkeep (100%) diff --git a/tests/test_rest_endpoints/services/gitkeep b/tests/test_rest_endpoints/services/data/gitkeep similarity index 100% rename from tests/test_rest_endpoints/services/gitkeep rename to tests/test_rest_endpoints/services/data/gitkeep From 8f645113e2fe272f6a4ac0581140d215ebda2fad Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 2 Jan 2023 15:49:55 +0000 Subject: [PATCH 25/81] test(tokens-repo): new assert_original(), no more json --- tests/test_rest_endpoints/test_auth.py | 118 ++++++++++++++----------- 1 file changed, 65 insertions(+), 53 deletions(-) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 17585fb..40960f0 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -3,10 +3,6 @@ # pylint: disable=missing-function-docstring import datetime import pytest -from mnemonic import Mnemonic - - -from tests.common import read_json, write_json TOKENS_FILE_CONTENTS = { @@ -14,12 +10,12 @@ TOKENS_FILE_CONTENTS = { { "token": "TEST_TOKEN", "name": "test_token", - "date": "2022-01-14 08:31:10.789314", + "date": datetime.datetime(2022, 1, 14, 8, 31, 10, 789314), }, { "token": "TEST_TOKEN2", "name": "test_token2", - "date": "2022-01-14 08:31:10.789314", + "date": datetime.datetime(2022, 1, 14, 8, 31, 10, 789314), }, ] } @@ -42,8 +38,17 @@ class NearFuture(datetime.datetime): return datetime.datetime.now() + datetime.timedelta(minutes=13) -def assert_original(filename): - assert read_json(filename) == TOKENS_FILE_CONTENTS +def assert_original(client): + new_tokens = rest_get_tokens_info(client) + + for token in TOKENS_FILE_CONTENTS["tokens"]: + assert_token_valid(client, token["token"]) + for new_token in new_tokens: + if new_token["name"] == token["name"]: + assert ( + datetime.datetime.fromisoformat(new_token["date"]) == token["date"] + ) + assert_no_recovery(client) def assert_token_valid(client, token): @@ -57,6 +62,17 @@ def rest_get_tokens_info(client): return response.json() +def rest_try_authorize_new_device(client, token, device_name): + response = client.post( + "/auth/new_device/authorize", + json={ + "token": token, + "device": device_name, + }, + ) + return response + + def rest_make_recovery_token(client, expires_at=None, timeformat=None, uses=None): json = {} @@ -101,6 +117,10 @@ def assert_recovery_recent(time_generated): ) +def assert_no_recovery(client): + assert not rest_get_recovery_status(client)["exists"] + + def rest_recover_with_mnemonic(client, mnemonic_token, device_name): recovery_response = client.post( "/auth/recovery_token/use", @@ -131,10 +151,10 @@ def test_get_tokens_unauthorized(client, tokens_file): assert response.status_code == 401 -def test_delete_token_unauthorized(client, tokens_file): +def test_delete_token_unauthorized(client, authorized_client, tokens_file): response = client.delete("/auth/tokens") assert response.status_code == 401 - assert_original(tokens_file) + assert_original(authorized_client) def test_delete_token(authorized_client, tokens_file): @@ -152,7 +172,7 @@ def test_delete_self_token(authorized_client, tokens_file): "/auth/tokens", json={"token_name": "test_token"} ) assert response.status_code == 400 - assert_original(tokens_file) + assert_original(authorized_client) def test_delete_nonexistent_token(authorized_client, tokens_file): @@ -160,13 +180,13 @@ def test_delete_nonexistent_token(authorized_client, tokens_file): "/auth/tokens", json={"token_name": "test_token3"} ) assert response.status_code == 404 - assert_original(tokens_file) + assert_original(authorized_client) -def test_refresh_token_unauthorized(client, tokens_file): +def test_refresh_token_unauthorized(client, authorized_client, tokens_file): response = client.post("/auth/tokens") assert response.status_code == 401 - assert_original(tokens_file) + assert_original(authorized_client) def test_refresh_token(authorized_client, tokens_file): @@ -179,23 +199,26 @@ def test_refresh_token(authorized_client, tokens_file): # New device -def test_get_new_device_auth_token_unauthorized(client, tokens_file): +def test_get_new_device_auth_token_unauthorized(client, authorized_client, tokens_file): response = client.post("/auth/new_device") assert response.status_code == 401 - assert_original(tokens_file) + assert "token" not in response.json() + assert "detail" in response.json() + # We only can check existence of a token we know. -def test_get_and_delete_new_device_token(authorized_client, tokens_file): +def test_get_and_delete_new_device_token(client, authorized_client, tokens_file): token = rest_get_new_device_token(authorized_client) response = authorized_client.delete("/auth/new_device", json={"token": token}) assert response.status_code == 200 - assert_original(tokens_file) + assert rest_try_authorize_new_device(client, token, "new_device").status_code == 404 -def test_delete_token_unauthenticated(client, tokens_file): - response = client.delete("/auth/new_device") +def test_delete_token_unauthenticated(client, authorized_client, tokens_file): + token = rest_get_new_device_token(authorized_client) + response = client.delete("/auth/new_device", json={"token": token}) assert response.status_code == 401 - assert_original(tokens_file) + assert rest_try_authorize_new_device(client, token, "new_device").status_code == 200 def rest_get_new_device_token(client): @@ -207,38 +230,29 @@ def rest_get_new_device_token(client): def test_get_and_authorize_new_device(client, authorized_client, tokens_file): token = rest_get_new_device_token(authorized_client) - response = client.post( - "/auth/new_device/authorize", - json={ - "token": token, - "device": "new_device", - }, - ) + response = rest_try_authorize_new_device(client, token, "new_device") assert response.status_code == 200 assert_token_valid(authorized_client, response.json()["token"]) -def test_authorize_new_device_with_invalid_token(client, tokens_file): - response = client.post( - "/auth/new_device/authorize", - json={"token": "invalid_token", "device": "new_device"}, - ) +def test_authorize_new_device_with_invalid_token( + client, authorized_client, tokens_file +): + response = rest_try_authorize_new_device(client, "invalid_token", "new_device") assert response.status_code == 404 - assert_original(tokens_file) + assert_original(authorized_client) def test_get_and_authorize_used_token(client, authorized_client, tokens_file): token_to_be_used_2_times = rest_get_new_device_token(authorized_client) - response = client.post( - "/auth/new_device/authorize", - json={"token": token_to_be_used_2_times, "device": "new_device"}, + response = rest_try_authorize_new_device( + client, token_to_be_used_2_times, "new_device" ) assert response.status_code == 200 assert_token_valid(authorized_client, response.json()["token"]) - response = client.post( - "/auth/new_device/authorize", - json={"token": token_to_be_used_2_times, "device": "new_device"}, + response = rest_try_authorize_new_device( + client, token_to_be_used_2_times, "new_device" ) assert response.status_code == 404 @@ -251,20 +265,18 @@ def test_get_and_authorize_token_after_12_minutes( # TARDIS sounds mock = mocker.patch(DEVICE_KEY_VALIDATION_DATETIME, NearFuture) - response = client.post( - "/auth/new_device/authorize", - json={"token": token, "device": "new_device"}, - ) + response = rest_try_authorize_new_device(client, token, "new_device") assert response.status_code == 404 + assert_original(authorized_client) -def test_authorize_without_token(client, tokens_file): +def test_authorize_without_token(client, authorized_client, tokens_file): response = client.post( "/auth/new_device/authorize", json={"device": "new_device"}, ) assert response.status_code == 422 - assert_original(tokens_file) + assert_original(authorized_client) # Recovery tokens @@ -290,10 +302,10 @@ def test_authorize_without_token(client, tokens_file): # - if request is invalid, returns 400 -def test_get_recovery_token_status_unauthorized(client, tokens_file): +def test_get_recovery_token_status_unauthorized(client, authorized_client, tokens_file): response = client.get("/auth/recovery_token") assert response.status_code == 401 - assert_original(tokens_file) + assert_original(authorized_client) def test_get_recovery_token_when_none_exists(authorized_client, tokens_file): @@ -306,7 +318,7 @@ def test_get_recovery_token_when_none_exists(authorized_client, tokens_file): "expiration": None, "uses_left": None, } - assert_original(tokens_file) + assert_original(authorized_client) def test_generate_recovery_token(authorized_client, client, tokens_file): @@ -381,7 +393,7 @@ def test_generate_recovery_token_with_expiration_in_the_past( json={"expiration": expiration_date_str}, ) assert response.status_code == 400 - assert not rest_get_recovery_status(authorized_client)["exists"] + assert_no_recovery(authorized_client) def test_generate_recovery_token_with_invalid_time_format( @@ -394,7 +406,7 @@ def test_generate_recovery_token_with_invalid_time_format( json={"expiration": expiration_date}, ) assert response.status_code == 422 - assert not rest_get_recovery_status(authorized_client)["exists"] + assert_no_recovery(authorized_client) def test_generate_recovery_token_with_limited_uses( @@ -453,7 +465,7 @@ def test_generate_recovery_token_with_negative_uses( json={"uses": -2}, ) assert response.status_code == 400 - assert not rest_get_recovery_status(authorized_client)["exists"] + assert_no_recovery(authorized_client) def test_generate_recovery_token_with_zero_uses(authorized_client, client, tokens_file): @@ -463,4 +475,4 @@ def test_generate_recovery_token_with_zero_uses(authorized_client, client, token json={"uses": 0}, ) assert response.status_code == 400 - assert not rest_get_recovery_status(authorized_client)["exists"] + assert_no_recovery(authorized_client) From 824b018487fe353fd3f64d2581015de821e83f3a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 2 Jan 2023 17:22:18 +0000 Subject: [PATCH 26/81] test(tokens-repo): make shared test token state use token repo api for loading --- tests/conftest.py | 69 ++++++++++++++++--- tests/test_graphql/test_api_devices.py | 6 +- .../test_repository/test_tokens_repository.py | 26 ------- 3 files changed, 64 insertions(+), 37 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4b65d20..bba3915 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,8 +4,34 @@ import os import pytest from fastapi.testclient import TestClient -from shutil import copy import os.path as path +import datetime + +# from selfprivacy_api.actions.api_tokens import TOKEN_REPO +from selfprivacy_api.models.tokens.token import Token +from selfprivacy_api.repositories.tokens.json_tokens_repository import ( + JsonTokensRepository, +) + +from tests.common import read_json + +EMPTY_TOKENS_JSON = ' {"tokens": []}' + + +TOKENS_FILE_CONTENTS = { + "tokens": [ + { + "token": "TEST_TOKEN", + "name": "test_token", + "date": datetime.datetime(2022, 1, 14, 8, 31, 10, 789314), + }, + { + "token": "TEST_TOKEN2", + "name": "test_token2", + "date": datetime.datetime(2022, 1, 14, 8, 31, 10, 789314), + }, + ] +} def pytest_generate_tests(metafunc): @@ -17,13 +43,40 @@ def global_data_dir(): @pytest.fixture -def tokens_file(mocker, tmpdir): - """Mock tokens file.""" - tmp_file = tmpdir / "tokens.json" - source_file = path.join(global_data_dir(), "tokens.json") - copy(source_file, tmp_file) - mock = mocker.patch("selfprivacy_api.utils.TOKENS_FILE", tmp_file) - return mock +def empty_tokens(mocker, tmpdir): + tokenfile = tmpdir / "empty_tokens.json" + with open(tokenfile, "w") as file: + file.write(EMPTY_TOKENS_JSON) + mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=tokenfile) + assert read_json(tokenfile)["tokens"] == [] + return tmpdir + + +@pytest.fixture +def empty_json_repo(empty_tokens): + repo = JsonTokensRepository() + for token in repo.get_tokens(): + repo.delete_token(token) + assert repo.get_tokens() == [] + return repo + + +@pytest.fixture +def tokens_file(empty_json_repo, tmpdir): + """A state with tokens""" + for token in TOKENS_FILE_CONTENTS["tokens"]: + empty_json_repo._store_token( + Token( + token=token["token"], + device_name=token["name"], + created_at=token["date"], + ) + ) + # temporary return for compatibility with older tests + + tokenfile = tmpdir / "empty_tokens.json" + assert path.exists(tokenfile) + return tokenfile @pytest.fixture diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 07cf42a..c546238 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -17,12 +17,12 @@ TOKENS_FILE_CONTETS = { { "token": "TEST_TOKEN", "name": "test_token", - "date": "2022-01-14 08:31:10.789314", + "date": "2022-01-14T08:31:10.789314", }, { "token": "TEST_TOKEN2", "name": "test_token2", - "date": "2022-01-14 08:31:10.789314", + "date": "2022-01-14T08:31:10.789314", }, ] } @@ -118,7 +118,7 @@ def test_graphql_delete_token(authorized_client, tokens_file): { "token": "TEST_TOKEN", "name": "test_token", - "date": "2022-01-14 08:31:10.789314", + "date": "2022-01-14T08:31:10.789314", } ] } diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index b172f13..a2dbb7a 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -16,13 +16,9 @@ from selfprivacy_api.repositories.tokens.exceptions import ( TokenNotFound, NewDeviceKeyNotFound, ) -from selfprivacy_api.repositories.tokens.json_tokens_repository import ( - JsonTokensRepository, -) from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( RedisTokensRepository, ) -from tests.common import read_json ORIGINAL_DEVICE_NAMES = [ @@ -33,23 +29,10 @@ ORIGINAL_DEVICE_NAMES = [ ] -EMPTY_TOKENS_JSON = ' {"tokens": []}' - - def mnemonic_from_hex(hexkey): return Mnemonic(language="english").to_mnemonic(bytes.fromhex(hexkey)) -@pytest.fixture -def empty_tokens(mocker, tmpdir): - tokens_file = tmpdir / "empty_tokens.json" - with open(tokens_file, "w") as file: - file.write(EMPTY_TOKENS_JSON) - mocker.patch("selfprivacy_api.utils.TOKENS_FILE", new=tokens_file) - assert read_json(tokens_file)["tokens"] == [] - return tmpdir - - @pytest.fixture def mock_new_device_key_generate(mocker): mock = mocker.patch( @@ -137,15 +120,6 @@ def mock_recovery_key_generate(mocker): return mock -@pytest.fixture -def empty_json_repo(empty_tokens): - repo = JsonTokensRepository() - for token in repo.get_tokens(): - repo.delete_token(token) - assert repo.get_tokens() == [] - return repo - - @pytest.fixture def empty_redis_repo(): repo = RedisTokensRepository() From 00ba76c074567481b38e23243546881144f2b39c Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 2 Jan 2023 18:22:04 +0000 Subject: [PATCH 27/81] refactor(tokens-repo): delete a stray comment --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index bba3915..891e4e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,6 @@ from fastapi.testclient import TestClient import os.path as path import datetime -# from selfprivacy_api.actions.api_tokens import TOKEN_REPO from selfprivacy_api.models.tokens.token import Token from selfprivacy_api.repositories.tokens.json_tokens_repository import ( JsonTokensRepository, From 2f707cc0cc8a7b672a3115d1c07c67fe184dbbe1 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 11:23:53 +0000 Subject: [PATCH 28/81] test(tokens-repo): delete extraneous test token content copies --- tests/test_graphql/test_api.py | 15 --------------- tests/test_graphql/test_api_recovery.py | 15 --------------- tests/test_rest_endpoints/test_auth.py | 16 +--------------- 3 files changed, 1 insertion(+), 45 deletions(-) diff --git a/tests/test_graphql/test_api.py b/tests/test_graphql/test_api.py index 16c7c4d..695dd8e 100644 --- a/tests/test_graphql/test_api.py +++ b/tests/test_graphql/test_api.py @@ -7,21 +7,6 @@ from tests.test_graphql.test_api_devices import API_DEVICES_QUERY from tests.test_graphql.test_api_recovery import API_RECOVERY_QUERY from tests.test_graphql.test_api_version import API_VERSION_QUERY -TOKENS_FILE_CONTETS = { - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "test_token", - "date": "2022-01-14 08:31:10.789314", - }, - { - "token": "TEST_TOKEN2", - "name": "test_token2", - "date": "2022-01-14 08:31:10.789314", - }, - ] -} - def test_graphql_get_entire_api_data(authorized_client, tokens_file): response = authorized_client.post( diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index c5e229e..2cb824f 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -5,21 +5,6 @@ import datetime from tests.common import generate_api_query, mnemonic_to_hex, read_json, write_json -TOKENS_FILE_CONTETS = { - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "test_token", - "date": "2022-01-14 08:31:10.789314", - }, - { - "token": "TEST_TOKEN2", - "name": "test_token2", - "date": "2022-01-14 08:31:10.789314", - }, - ] -} - API_RECOVERY_QUERY = """ recoveryKey { exists diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 40960f0..1872203 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -4,21 +4,7 @@ import datetime import pytest - -TOKENS_FILE_CONTENTS = { - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "test_token", - "date": datetime.datetime(2022, 1, 14, 8, 31, 10, 789314), - }, - { - "token": "TEST_TOKEN2", - "name": "test_token2", - "date": datetime.datetime(2022, 1, 14, 8, 31, 10, 789314), - }, - ] -} +from tests.conftest import TOKENS_FILE_CONTENTS DATE_FORMATS = [ "%Y-%m-%dT%H:%M:%S.%fZ", From d26d115172cad8c1542dc72120181d60a1246531 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 12:31:24 +0000 Subject: [PATCH 29/81] test(tokens-repo): break out assert_original() in graphql device tests --- tests/conftest.py | 6 ++- tests/test_graphql/test_api_devices.py | 63 +++++++++++--------------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 891e4e9..212b6da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,8 @@ TOKENS_FILE_CONTENTS = { ] } +DEVICE_WE_AUTH_TESTS_WITH = TOKENS_FILE_CONTENTS["tokens"][0] + def pytest_generate_tests(metafunc): os.environ["TEST_MODE"] = "true" @@ -107,7 +109,9 @@ def authorized_client(tokens_file, huey_database, jobs_file): from selfprivacy_api.app import app client = TestClient(app) - client.headers.update({"Authorization": "Bearer TEST_TOKEN"}) + client.headers.update( + {"Authorization": "Bearer " + DEVICE_WE_AUTH_TESTS_WITH["token"]} + ) return client diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index c546238..f91b4f1 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -11,21 +11,7 @@ from selfprivacy_api.repositories.tokens.json_tokens_repository import ( from selfprivacy_api.models.tokens.token import Token from tests.common import generate_api_query, read_json, write_json - -TOKENS_FILE_CONTETS = { - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "test_token", - "date": "2022-01-14T08:31:10.789314", - }, - { - "token": "TEST_TOKEN2", - "name": "test_token2", - "date": "2022-01-14T08:31:10.789314", - }, - ] -} +from tests.conftest import DEVICE_WE_AUTH_TESTS_WITH, TOKENS_FILE_CONTENTS API_DEVICES_QUERY = """ devices { @@ -41,27 +27,30 @@ def token_repo(): return JsonTokensRepository() -def test_graphql_tokens_info(authorized_client, tokens_file): - response = authorized_client.post( +def assert_original(client): + response = client.post( "/graphql", json={"query": generate_api_query([API_DEVICES_QUERY])}, ) assert response.status_code == 200 assert response.json().get("data") is not None - assert response.json()["data"]["api"]["devices"] is not None - assert len(response.json()["data"]["api"]["devices"]) == 2 - assert ( - response.json()["data"]["api"]["devices"][0]["creationDate"] - == "2022-01-14T08:31:10.789314" - ) - assert response.json()["data"]["api"]["devices"][0]["isCaller"] is True - assert response.json()["data"]["api"]["devices"][0]["name"] == "test_token" - assert ( - response.json()["data"]["api"]["devices"][1]["creationDate"] - == "2022-01-14T08:31:10.789314" - ) - assert response.json()["data"]["api"]["devices"][1]["isCaller"] is False - assert response.json()["data"]["api"]["devices"][1]["name"] == "test_token2" + devices = response.json()["data"]["api"]["devices"] + assert devices is not None + original_devices = TOKENS_FILE_CONTENTS["tokens"] + assert len(devices) == len(original_devices) + for original_device in original_devices: + assert original_device["name"] in [device["name"] for device in devices] + for device in devices: + if device["name"] == DEVICE_WE_AUTH_TESTS_WITH["name"]: + assert device["isCaller"] is True + else: + assert device["isCaller"] is False + if device["name"] == original_device["name"]: + assert device["creationDate"] == original_device["date"].isoformat() + + +def test_graphql_tokens_info(authorized_client, tokens_file): + assert_original(authorized_client) def test_graphql_tokens_info_unauthorized(client, tokens_file): @@ -139,7 +128,7 @@ def test_graphql_delete_self_token(authorized_client, tokens_file): assert response.json()["data"]["deleteDeviceApiToken"]["success"] is False assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 400 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(authorized_client) def test_graphql_delete_nonexistent_token(authorized_client, tokens_file): @@ -157,7 +146,7 @@ def test_graphql_delete_nonexistent_token(authorized_client, tokens_file): assert response.json()["data"]["deleteDeviceApiToken"]["success"] is False assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 404 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(authorized_client) REFRESH_TOKEN_MUTATION = """ @@ -294,7 +283,7 @@ def test_graphql_get_and_delete_new_device_key(authorized_client, tokens_file): assert response.json()["data"]["invalidateNewDeviceApiKey"]["success"] is True assert response.json()["data"]["invalidateNewDeviceApiKey"]["message"] is not None assert response.json()["data"]["invalidateNewDeviceApiKey"]["code"] == 200 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(authorized_client) AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION = """ @@ -347,7 +336,9 @@ def test_graphql_get_and_authorize_new_device(client, authorized_client, tokens_ assert read_json(tokens_file)["tokens"][2]["name"] == "new_device" -def test_graphql_authorize_new_device_with_invalid_key(client, tokens_file): +def test_graphql_authorize_new_device_with_invalid_key( + client, authorized_client, tokens_file +): response = client.post( "/graphql", json={ @@ -367,7 +358,7 @@ def test_graphql_authorize_new_device_with_invalid_key(client, tokens_file): response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None ) assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404 - assert read_json(tokens_file) == TOKENS_FILE_CONTETS + assert_original(authorized_client) def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_file): From d8c78cc14c87c2de2d97b63a75580652434fa853 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 13:18:38 +0000 Subject: [PATCH 30/81] test(tokens-repo): untie token deletion tests from json --- tests/test_graphql/test_api_devices.py | 51 ++++++++++++++++---------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index f91b4f1..437470a 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -13,6 +13,8 @@ from selfprivacy_api.models.tokens.token import Token from tests.common import generate_api_query, read_json, write_json from tests.conftest import DEVICE_WE_AUTH_TESTS_WITH, TOKENS_FILE_CONTENTS +ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] + API_DEVICES_QUERY = """ devices { creationDate @@ -27,7 +29,7 @@ def token_repo(): return JsonTokensRepository() -def assert_original(client): +def graphql_get_devices(client): response = client.post( "/graphql", json={"query": generate_api_query([API_DEVICES_QUERY])}, @@ -36,19 +38,30 @@ def assert_original(client): assert response.json().get("data") is not None devices = response.json()["data"]["api"]["devices"] assert devices is not None - original_devices = TOKENS_FILE_CONTENTS["tokens"] - assert len(devices) == len(original_devices) - for original_device in original_devices: - assert original_device["name"] in [device["name"] for device in devices] - for device in devices: - if device["name"] == DEVICE_WE_AUTH_TESTS_WITH["name"]: - assert device["isCaller"] is True - else: - assert device["isCaller"] is False + return devices + + +def assert_same(graphql_devices, abstract_devices): + """Orderless comparison""" + assert len(graphql_devices) == len(abstract_devices) + for original_device in abstract_devices: + assert original_device["name"] in [device["name"] for device in graphql_devices] + for device in graphql_devices: if device["name"] == original_device["name"]: assert device["creationDate"] == original_device["date"].isoformat() +def assert_original(client): + devices = graphql_get_devices(client) + assert_same(devices, ORIGINAL_DEVICES) + + for device in devices: + if device["name"] == DEVICE_WE_AUTH_TESTS_WITH["name"]: + assert device["isCaller"] is True + else: + assert device["isCaller"] is False + + def test_graphql_tokens_info(authorized_client, tokens_file): assert_original(authorized_client) @@ -88,12 +101,16 @@ def test_graphql_delete_token_unauthorized(client, tokens_file): def test_graphql_delete_token(authorized_client, tokens_file): + test_devices = ORIGINAL_DEVICES.copy() + device_to_delete = test_devices.pop(1) + assert device_to_delete != DEVICE_WE_AUTH_TESTS_WITH + response = authorized_client.post( "/graphql", json={ "query": DELETE_TOKEN_MUTATION, "variables": { - "device": "test_token2", + "device": device_to_delete["name"], }, }, ) @@ -102,15 +119,9 @@ def test_graphql_delete_token(authorized_client, tokens_file): assert response.json()["data"]["deleteDeviceApiToken"]["success"] is True assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 200 - assert read_json(tokens_file) == { - "tokens": [ - { - "token": "TEST_TOKEN", - "name": "test_token", - "date": "2022-01-14T08:31:10.789314", - } - ] - } + + devices = graphql_get_devices(authorized_client) + assert_same(devices, test_devices) def test_graphql_delete_self_token(authorized_client, tokens_file): From 7f5236701e023a675191f1dc960a5413e631db0a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 14:01:23 +0000 Subject: [PATCH 31/81] test(tokens-repo): break out assert_ok() and assert_errorcode() in graphql --- tests/test_graphql/test_api_devices.py | 28 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 437470a..5f88079 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -62,6 +62,22 @@ def assert_original(client): assert device["isCaller"] is False +def assert_ok(response, request): + assert response.status_code == 200 + assert response.json().get("data") is not None + assert response.json()["data"][request]["success"] is True + assert response.json()["data"][request]["message"] is not None + assert response.json()["data"][request]["code"] == 200 + + +def assert_errorcode(response, request, code): + assert response.status_code == 200 + assert response.json().get("data") is not None + assert response.json()["data"][request]["success"] is False + assert response.json()["data"][request]["message"] is not None + assert response.json()["data"][request]["code"] == code + + def test_graphql_tokens_info(authorized_client, tokens_file): assert_original(authorized_client) @@ -114,11 +130,7 @@ def test_graphql_delete_token(authorized_client, tokens_file): }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["deleteDeviceApiToken"]["success"] is True - assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None - assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 200 + assert_ok(response, "deleteDeviceApiToken") devices = graphql_get_devices(authorized_client) assert_same(devices, test_devices) @@ -134,11 +146,7 @@ def test_graphql_delete_self_token(authorized_client, tokens_file): }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["deleteDeviceApiToken"]["success"] is False - assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None - assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 400 + assert_errorcode(response, "deleteDeviceApiToken", 400) assert_original(authorized_client) From 5a1b48fa3d00f129f568ac9b563348c7fe1e76c3 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 14:15:12 +0000 Subject: [PATCH 32/81] test(tokens-repo): break out assert_empty() --- tests/test_graphql/test_api_devices.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 5f88079..90f1685 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -78,6 +78,11 @@ def assert_errorcode(response, request, code): assert response.json()["data"][request]["code"] == code +def assert_empty(response): + assert response.status_code == 200 + assert response.json().get("data") is None + + def test_graphql_tokens_info(authorized_client, tokens_file): assert_original(authorized_client) @@ -87,8 +92,7 @@ def test_graphql_tokens_info_unauthorized(client, tokens_file): "/graphql", json={"query": generate_api_query([API_DEVICES_QUERY])}, ) - assert response.status_code == 200 - assert response.json()["data"] is None + assert_empty(response) DELETE_TOKEN_MUTATION = """ @@ -112,8 +116,7 @@ def test_graphql_delete_token_unauthorized(client, tokens_file): }, }, ) - assert response.status_code == 200 - assert response.json()["data"] is None + assert_empty(response) def test_graphql_delete_token(authorized_client, tokens_file): @@ -185,8 +188,7 @@ def test_graphql_refresh_token_unauthorized(client, tokens_file): "/graphql", json={"query": REFRESH_TOKEN_MUTATION}, ) - assert response.status_code == 200 - assert response.json()["data"] is None + assert_empty(response) def test_graphql_refresh_token(authorized_client, tokens_file, token_repo): @@ -224,8 +226,7 @@ def test_graphql_get_new_device_auth_key_unauthorized(client, tokens_file): "/graphql", json={"query": NEW_DEVICE_KEY_MUTATION}, ) - assert response.status_code == 200 - assert response.json()["data"] is None + assert_empty(response) def test_graphql_get_new_device_auth_key(authorized_client, tokens_file): @@ -270,8 +271,7 @@ def test_graphql_invalidate_new_device_token_unauthorized(client, tokens_file): }, }, ) - assert response.status_code == 200 - assert response.json()["data"] is None + assert_empty(response) def test_graphql_get_and_delete_new_device_key(authorized_client, tokens_file): @@ -502,5 +502,4 @@ def test_graphql_authorize_without_token(client, tokens_file): }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) From 4676e364a69f18eab960fb31b0e46fcf3d55ac4a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 14:22:14 +0000 Subject: [PATCH 33/81] test(tokens-repo): break out assert_data() --- tests/test_graphql/test_api_devices.py | 30 +++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 90f1685..3104874 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -34,9 +34,8 @@ def graphql_get_devices(client): "/graphql", json={"query": generate_api_query([API_DEVICES_QUERY])}, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - devices = response.json()["data"]["api"]["devices"] + data = assert_data(response) + devices = data["api"]["devices"] assert devices is not None return devices @@ -63,19 +62,17 @@ def assert_original(client): def assert_ok(response, request): - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"][request]["success"] is True - assert response.json()["data"][request]["message"] is not None - assert response.json()["data"][request]["code"] == 200 + data = assert_data(response) + data[request]["success"] is True + data[request]["message"] is not None + data[request]["code"] == 200 def assert_errorcode(response, request, code): - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"][request]["success"] is False - assert response.json()["data"][request]["message"] is not None - assert response.json()["data"][request]["code"] == code + data = assert_data(response) + data[request]["success"] is False + data[request]["message"] is not None + data[request]["code"] == code def assert_empty(response): @@ -83,6 +80,13 @@ def assert_empty(response): assert response.json().get("data") is None +def assert_data(response): + assert response.status_code == 200 + data = response.json().get("data") + assert data is not None + return data + + def test_graphql_tokens_info(authorized_client, tokens_file): assert_original(authorized_client) From ba5f91b00017186764767e1953a97086cedfb8c4 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 15:05:01 +0000 Subject: [PATCH 34/81] test(tokens-repo): apply assert_ok and assert_error() --- tests/test_graphql/test_api_devices.py | 96 ++++++-------------------- 1 file changed, 21 insertions(+), 75 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 3104874..f6ac3ac 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -167,11 +167,8 @@ def test_graphql_delete_nonexistent_token(authorized_client, tokens_file): }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["deleteDeviceApiToken"]["success"] is False - assert response.json()["data"]["deleteDeviceApiToken"]["message"] is not None - assert response.json()["data"]["deleteDeviceApiToken"]["code"] == 404 + assert_errorcode(response, "deleteDeviceApiToken", 404) + assert_original(authorized_client) @@ -200,11 +197,8 @@ def test_graphql_refresh_token(authorized_client, tokens_file, token_repo): "/graphql", json={"query": REFRESH_TOKEN_MUTATION}, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["refreshDeviceApiToken"]["success"] is True - assert response.json()["data"]["refreshDeviceApiToken"]["message"] is not None - assert response.json()["data"]["refreshDeviceApiToken"]["code"] == 200 + assert_ok(response, "refreshDeviceApiToken") + token = token_repo.get_token_by_name("test_token") assert token == Token( token=response.json()["data"]["refreshDeviceApiToken"]["token"], @@ -238,11 +232,8 @@ def test_graphql_get_new_device_auth_key(authorized_client, tokens_file): "/graphql", json={"query": NEW_DEVICE_KEY_MUTATION}, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True - assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None - assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200 + assert_ok(response, "getNewDeviceApiKey") + assert ( response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12 ) @@ -283,11 +274,8 @@ def test_graphql_get_and_delete_new_device_key(authorized_client, tokens_file): "/graphql", json={"query": NEW_DEVICE_KEY_MUTATION}, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True - assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None - assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200 + assert_ok(response, "getNewDeviceApiKey") + assert ( response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12 ) @@ -301,11 +289,7 @@ def test_graphql_get_and_delete_new_device_key(authorized_client, tokens_file): "/graphql", json={"query": INVALIDATE_NEW_DEVICE_KEY_MUTATION}, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["invalidateNewDeviceApiKey"]["success"] is True - assert response.json()["data"]["invalidateNewDeviceApiKey"]["message"] is not None - assert response.json()["data"]["invalidateNewDeviceApiKey"]["code"] == 200 + assert_ok(response, "invalidateNewDeviceApiKey") assert_original(authorized_client) @@ -326,15 +310,13 @@ def test_graphql_get_and_authorize_new_device(client, authorized_client, tokens_ "/graphql", json={"query": NEW_DEVICE_KEY_MUTATION}, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True - assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None - assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200 + assert_ok(response, "getNewDeviceApiKey") + mnemonic_key = response.json()["data"]["getNewDeviceApiKey"]["key"] assert mnemonic_key.split(" ").__len__() == 12 key = Mnemonic(language="english").to_entropy(mnemonic_key).hex() assert read_json(tokens_file)["new_device"]["token"] == key + response = client.post( "/graphql", json={ @@ -347,13 +329,8 @@ def test_graphql_get_and_authorize_new_device(client, authorized_client, tokens_ }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is True - assert ( - response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None - ) - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 200 + + assert_ok(response, "authorizeWithNewDeviceApiKey") token = response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"] assert read_json(tokens_file)["tokens"][2]["token"] == token assert read_json(tokens_file)["tokens"][2]["name"] == "new_device" @@ -374,13 +351,7 @@ def test_graphql_authorize_new_device_with_invalid_key( }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is False - assert ( - response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None - ) - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404 + assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) assert_original(authorized_client) @@ -389,15 +360,12 @@ def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_fi "/graphql", json={"query": NEW_DEVICE_KEY_MUTATION}, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True - assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None - assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200 + assert_ok(response, "getNewDeviceApiKey") mnemonic_key = response.json()["data"]["getNewDeviceApiKey"]["key"] assert mnemonic_key.split(" ").__len__() == 12 key = Mnemonic(language="english").to_entropy(mnemonic_key).hex() assert read_json(tokens_file)["new_device"]["token"] == key + response = client.post( "/graphql", json={ @@ -410,13 +378,7 @@ def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_fi }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is True - assert ( - response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None - ) - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 200 + assert_ok(response, "authorizeWithNewDeviceApiKey") assert ( read_json(tokens_file)["tokens"][2]["token"] == response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"] @@ -435,13 +397,7 @@ def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_fi }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is False - assert ( - response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None - ) - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404 + assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) assert read_json(tokens_file)["tokens"].__len__() == 3 @@ -452,11 +408,7 @@ def test_graphql_get_and_authorize_key_after_12_minutes( "/graphql", json={"query": NEW_DEVICE_KEY_MUTATION}, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewDeviceApiKey"]["success"] is True - assert response.json()["data"]["getNewDeviceApiKey"]["message"] is not None - assert response.json()["data"]["getNewDeviceApiKey"]["code"] == 200 + assert_ok(response, "getNewDeviceApiKey") assert ( response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12 ) @@ -485,13 +437,7 @@ def test_graphql_get_and_authorize_key_after_12_minutes( }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["success"] is False - assert ( - response.json()["data"]["authorizeWithNewDeviceApiKey"]["message"] is not None - ) - assert response.json()["data"]["authorizeWithNewDeviceApiKey"]["code"] == 404 + assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) def test_graphql_authorize_without_token(client, tokens_file): From 469f9d292d8d9d3297ba3d28bc05f64a9a1f5737 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 15:08:15 +0000 Subject: [PATCH 35/81] test(tokens-repo): make sure we try to delete the token we authed with --- tests/test_graphql/test_api_devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index f6ac3ac..6ee1ab4 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -149,7 +149,7 @@ def test_graphql_delete_self_token(authorized_client, tokens_file): json={ "query": DELETE_TOKEN_MUTATION, "variables": { - "device": "test_token", + "device": DEVICE_WE_AUTH_TESTS_WITH["name"], }, }, ) From 6eb5800e4e0d63517cbe622ee6c93c49e9d7bffb Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 15:37:48 +0000 Subject: [PATCH 36/81] test(tokens-repo): untie refresh token testing from token repo --- tests/test_graphql/test_api_devices.py | 40 +++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 6ee1ab4..780611f 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -2,13 +2,8 @@ # pylint: disable=unused-argument # pylint: disable=missing-function-docstring import datetime -import pytest from mnemonic import Mnemonic -from selfprivacy_api.repositories.tokens.json_tokens_repository import ( - JsonTokensRepository, -) -from selfprivacy_api.models.tokens.token import Token from tests.common import generate_api_query, read_json, write_json from tests.conftest import DEVICE_WE_AUTH_TESTS_WITH, TOKENS_FILE_CONTENTS @@ -24,11 +19,6 @@ devices { """ -@pytest.fixture -def token_repo(): - return JsonTokensRepository() - - def graphql_get_devices(client): response = client.post( "/graphql", @@ -40,6 +30,13 @@ def graphql_get_devices(client): return devices +def graphql_get_caller_token_info(client): + devices = graphql_get_devices(client) + for device in devices: + if device["isCaller"] is True: + return device + + def assert_same(graphql_devices, abstract_devices): """Orderless comparison""" assert len(graphql_devices) == len(abstract_devices) @@ -87,6 +84,15 @@ def assert_data(response): return data +def set_client_token(client, token): + client.headers.update({"Authorization": "Bearer " + token}) + + +def assert_token_valid(client, token): + set_client_token(client, token) + assert graphql_get_devices(client) is not None + + def test_graphql_tokens_info(authorized_client, tokens_file): assert_original(authorized_client) @@ -192,19 +198,19 @@ def test_graphql_refresh_token_unauthorized(client, tokens_file): assert_empty(response) -def test_graphql_refresh_token(authorized_client, tokens_file, token_repo): +def test_graphql_refresh_token(authorized_client, client, tokens_file): + caller_name_and_date = graphql_get_caller_token_info(authorized_client) response = authorized_client.post( "/graphql", json={"query": REFRESH_TOKEN_MUTATION}, ) assert_ok(response, "refreshDeviceApiToken") - token = token_repo.get_token_by_name("test_token") - assert token == Token( - token=response.json()["data"]["refreshDeviceApiToken"]["token"], - device_name="test_token", - created_at=datetime.datetime(2022, 1, 14, 8, 31, 10, 789314), - ) + new_token = response.json()["data"]["refreshDeviceApiToken"]["token"] + assert_token_valid(client, new_token) + + set_client_token(client, new_token) + assert graphql_get_caller_token_info(client) == caller_name_and_date NEW_DEVICE_KEY_MUTATION = """ From 102d6b1c5c3eaebd75397bf26feed44919bc2df9 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 15:41:45 +0000 Subject: [PATCH 37/81] test(tokens-repo): delete get new device key standalone test we can only see if device key is valid by using it or deleting it. another test does it --- tests/test_graphql/test_api_devices.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 780611f..ea926ea 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -233,24 +233,6 @@ def test_graphql_get_new_device_auth_key_unauthorized(client, tokens_file): assert_empty(response) -def test_graphql_get_new_device_auth_key(authorized_client, tokens_file): - response = authorized_client.post( - "/graphql", - json={"query": NEW_DEVICE_KEY_MUTATION}, - ) - assert_ok(response, "getNewDeviceApiKey") - - assert ( - response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12 - ) - token = ( - Mnemonic(language="english") - .to_entropy(response.json()["data"]["getNewDeviceApiKey"]["key"]) - .hex() - ) - assert read_json(tokens_file)["new_device"]["token"] == token - - INVALIDATE_NEW_DEVICE_KEY_MUTATION = """ mutation InvalidateNewDeviceKey { invalidateNewDeviceApiKey { From e739921835fbbdfc517ca9c9f4a8c78b79f7b148 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 16:08:40 +0000 Subject: [PATCH 38/81] test(tokens-repo): untie get and delete new device from json --- tests/test_graphql/test_api_devices.py | 61 ++++++++++++++------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index ea926ea..51d729c 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -93,6 +93,33 @@ def assert_token_valid(client, token): assert graphql_get_devices(client) is not None +def graphql_get_new_device_key(authorized_client) -> str: + response = authorized_client.post( + "/graphql", + json={"query": NEW_DEVICE_KEY_MUTATION}, + ) + assert_ok(response, "getNewDeviceApiKey") + + key = response.json()["data"]["getNewDeviceApiKey"]["key"] + assert key.split(" ").__len__() == 12 + return key + + +def graphql_try_auth_new_device(client, mnemonic_key, device_name): + return client.post( + "/graphql", + json={ + "query": AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION, + "variables": { + "input": { + "key": mnemonic_key, + "deviceName": device_name, + } + }, + }, + ) + + def test_graphql_tokens_info(authorized_client, tokens_file): assert_original(authorized_client) @@ -257,28 +284,17 @@ def test_graphql_invalidate_new_device_token_unauthorized(client, tokens_file): assert_empty(response) -def test_graphql_get_and_delete_new_device_key(authorized_client, tokens_file): - response = authorized_client.post( - "/graphql", - json={"query": NEW_DEVICE_KEY_MUTATION}, - ) - assert_ok(response, "getNewDeviceApiKey") +def test_graphql_get_and_delete_new_device_key(client, authorized_client, tokens_file): + mnemonic_key = graphql_get_new_device_key(authorized_client) - assert ( - response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12 - ) - token = ( - Mnemonic(language="english") - .to_entropy(response.json()["data"]["getNewDeviceApiKey"]["key"]) - .hex() - ) - assert read_json(tokens_file)["new_device"]["token"] == token response = authorized_client.post( "/graphql", json={"query": INVALIDATE_NEW_DEVICE_KEY_MUTATION}, ) assert_ok(response, "invalidateNewDeviceApiKey") - assert_original(authorized_client) + + response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") + assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION = """ @@ -305,18 +321,7 @@ def test_graphql_get_and_authorize_new_device(client, authorized_client, tokens_ key = Mnemonic(language="english").to_entropy(mnemonic_key).hex() assert read_json(tokens_file)["new_device"]["token"] == key - response = client.post( - "/graphql", - json={ - "query": AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION, - "variables": { - "input": { - "key": mnemonic_key, - "deviceName": "new_device", - } - }, - }, - ) + response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") assert_ok(response, "authorizeWithNewDeviceApiKey") token = response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"] From 367ba51c9d0c444a6f0c15111c301a33c7c7f3f6 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 16:26:59 +0000 Subject: [PATCH 39/81] test(tokens-repo): untie authorize new device from json --- tests/test_graphql/test_api_devices.py | 28 ++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 51d729c..4b792db 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -120,6 +120,13 @@ def graphql_try_auth_new_device(client, mnemonic_key, device_name): ) +def graphql_authorize_new_device(client, mnemonic_key, device_name) -> str: + response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") + assert_ok(response, "authorizeWithNewDeviceApiKey") + token = response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"] + assert_token_valid(client, token) + + def test_graphql_tokens_info(authorized_client, tokens_file): assert_original(authorized_client) @@ -310,23 +317,14 @@ mutation AuthorizeWithNewDeviceKey($input: UseNewDeviceKeyInput!) { def test_graphql_get_and_authorize_new_device(client, authorized_client, tokens_file): - response = authorized_client.post( - "/graphql", - json={"query": NEW_DEVICE_KEY_MUTATION}, - ) - assert_ok(response, "getNewDeviceApiKey") + mnemonic_key = graphql_get_new_device_key(authorized_client) + old_devices = graphql_get_devices(authorized_client) - mnemonic_key = response.json()["data"]["getNewDeviceApiKey"]["key"] - assert mnemonic_key.split(" ").__len__() == 12 - key = Mnemonic(language="english").to_entropy(mnemonic_key).hex() - assert read_json(tokens_file)["new_device"]["token"] == key + graphql_authorize_new_device(client, mnemonic_key, "new_device") + new_devices = graphql_get_devices(authorized_client) - response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") - - assert_ok(response, "authorizeWithNewDeviceApiKey") - token = response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"] - assert read_json(tokens_file)["tokens"][2]["token"] == token - assert read_json(tokens_file)["tokens"][2]["name"] == "new_device" + assert len(new_devices) == len(old_devices) + 1 + assert "new_device" in [device["name"] for device in new_devices] def test_graphql_authorize_new_device_with_invalid_key( From 592d62f53f0857be7e9883cd72c1852bf05fbc45 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 16:37:27 +0000 Subject: [PATCH 40/81] test(tokens-repo): untie double new device auth from json --- tests/test_graphql/test_api_devices.py | 46 ++++---------------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 4b792db..a88493c 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -347,49 +347,15 @@ def test_graphql_authorize_new_device_with_invalid_key( def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_file): - response = authorized_client.post( - "/graphql", - json={"query": NEW_DEVICE_KEY_MUTATION}, - ) - assert_ok(response, "getNewDeviceApiKey") - mnemonic_key = response.json()["data"]["getNewDeviceApiKey"]["key"] - assert mnemonic_key.split(" ").__len__() == 12 - key = Mnemonic(language="english").to_entropy(mnemonic_key).hex() - assert read_json(tokens_file)["new_device"]["token"] == key + mnemonic_key = graphql_get_new_device_key(authorized_client) - response = client.post( - "/graphql", - json={ - "query": AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION, - "variables": { - "input": { - "key": mnemonic_key, - "deviceName": "new_token", - } - }, - }, - ) - assert_ok(response, "authorizeWithNewDeviceApiKey") - assert ( - read_json(tokens_file)["tokens"][2]["token"] - == response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"] - ) - assert read_json(tokens_file)["tokens"][2]["name"] == "new_token" + graphql_authorize_new_device(client, mnemonic_key, "new_device") + devices = graphql_get_devices(authorized_client) - response = client.post( - "/graphql", - json={ - "query": AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION, - "variables": { - "input": { - "key": mnemonic_key, - "deviceName": "test_token2", - } - }, - }, - ) + response = graphql_try_auth_new_device(client, mnemonic_key, "new_device2") assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) - assert read_json(tokens_file)["tokens"].__len__() == 3 + + assert graphql_get_devices(authorized_client) == devices def test_graphql_get_and_authorize_key_after_12_minutes( From 0aaa90f54a6a8ac38ce74bae126eef0d8a8e6fcf Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 16:42:50 +0000 Subject: [PATCH 41/81] test(tokens-repo): shrink invalid new device test --- tests/test_graphql/test_api_devices.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index a88493c..37d81af 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -330,19 +330,9 @@ def test_graphql_get_and_authorize_new_device(client, authorized_client, tokens_ def test_graphql_authorize_new_device_with_invalid_key( client, authorized_client, tokens_file ): - response = client.post( - "/graphql", - json={ - "query": AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION, - "variables": { - "input": { - "key": "invalid_token", - "deviceName": "test_token", - } - }, - }, - ) + response = graphql_try_auth_new_device(client, "invalid_token", "new_device") assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) + assert_original(authorized_client) From f5faf84a2b58012f1444e530f033ac17edb95f2a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 16:49:40 +0000 Subject: [PATCH 42/81] test(tokens-repo): move timewarping to test commons --- tests/common.py | 11 +++++++++++ tests/test_rest_endpoints/test_auth.py | 15 +++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/common.py b/tests/common.py index 18e065c..95488cc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,6 +1,17 @@ import json +import datetime from mnemonic import Mnemonic +# for expiration tests. If headache, consider freezegun +RECOVERY_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.recovery_key.datetime" +DEVICE_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.new_device_key.datetime" + + +class NearFuture(datetime.datetime): + @classmethod + def now(cls): + return datetime.datetime.now() + datetime.timedelta(minutes=13) + def read_json(file_path): with open(file_path, "r", encoding="utf-8") as file: diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 1872203..1632e22 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -5,6 +5,11 @@ import datetime import pytest from tests.conftest import TOKENS_FILE_CONTENTS +from tests.common import ( + RECOVERY_KEY_VALIDATION_DATETIME, + DEVICE_KEY_VALIDATION_DATETIME, + NearFuture, +) DATE_FORMATS = [ "%Y-%m-%dT%H:%M:%S.%fZ", @@ -13,16 +18,6 @@ DATE_FORMATS = [ "%Y-%m-%d %H:%M:%S.%f", ] -# for expiration tests. If headache, consider freezegun -RECOVERY_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.recovery_key.datetime" -DEVICE_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.new_device_key.datetime" - - -class NearFuture(datetime.datetime): - @classmethod - def now(cls): - return datetime.datetime.now() + datetime.timedelta(minutes=13) - def assert_original(client): new_tokens = rest_get_tokens_info(client) From 1305144112e1851ab09ddc58b0b2978e65ba651e Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 4 Jan 2023 16:58:27 +0000 Subject: [PATCH 43/81] test(tokens-repo): complete untying graphql device tests from json --- tests/test_graphql/test_api_devices.py | 48 ++++++-------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 37d81af..fce99f7 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -1,11 +1,12 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument # pylint: disable=missing-function-docstring -import datetime -from mnemonic import Mnemonic - - -from tests.common import generate_api_query, read_json, write_json +from tests.common import ( + RECOVERY_KEY_VALIDATION_DATETIME, + DEVICE_KEY_VALIDATION_DATETIME, + NearFuture, + generate_api_query +) from tests.conftest import DEVICE_WE_AUTH_TESTS_WITH, TOKENS_FILE_CONTENTS ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] @@ -349,41 +350,12 @@ def test_graphql_get_and_authorize_used_key(client, authorized_client, tokens_fi def test_graphql_get_and_authorize_key_after_12_minutes( - client, authorized_client, tokens_file + client, authorized_client, tokens_file, mocker ): - response = authorized_client.post( - "/graphql", - json={"query": NEW_DEVICE_KEY_MUTATION}, - ) - assert_ok(response, "getNewDeviceApiKey") - assert ( - response.json()["data"]["getNewDeviceApiKey"]["key"].split(" ").__len__() == 12 - ) - key = ( - Mnemonic(language="english") - .to_entropy(response.json()["data"]["getNewDeviceApiKey"]["key"]) - .hex() - ) - assert read_json(tokens_file)["new_device"]["token"] == key + mnemonic_key = graphql_get_new_device_key(authorized_client) + mock = mocker.patch(DEVICE_KEY_VALIDATION_DATETIME, NearFuture) - file_data = read_json(tokens_file) - file_data["new_device"]["expiration"] = str( - datetime.datetime.now() - datetime.timedelta(minutes=13) - ) - write_json(tokens_file, file_data) - - response = client.post( - "/graphql", - json={ - "query": AUTHORIZE_WITH_NEW_DEVICE_KEY_MUTATION, - "variables": { - "input": { - "key": key, - "deviceName": "test_token", - } - }, - }, - ) + response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") assert_errorcode(response, "authorizeWithNewDeviceApiKey", 404) From d09cd1bbe1a83fb4d79fe0c9928101bce49fda1b Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 09:54:07 +0000 Subject: [PATCH 44/81] test(tokens-repo): use assert_empty consistently --- tests/test_graphql/common.py | 24 +++++++++++++++++ tests/test_graphql/test_api_devices.py | 34 +++++-------------------- tests/test_graphql/test_api_recovery.py | 7 +++-- tests/test_graphql/test_ssh.py | 7 +++-- tests/test_graphql/test_system.py | 22 ++++++---------- tests/test_graphql/test_users.py | 16 +++++------- 6 files changed, 51 insertions(+), 59 deletions(-) create mode 100644 tests/test_graphql/common.py diff --git a/tests/test_graphql/common.py b/tests/test_graphql/common.py new file mode 100644 index 0000000..f2cc54d --- /dev/null +++ b/tests/test_graphql/common.py @@ -0,0 +1,24 @@ +def assert_ok(response, request): + data = assert_data(response) + data[request]["success"] is True + data[request]["message"] is not None + data[request]["code"] == 200 + + +def assert_errorcode(response, request, code): + data = assert_data(response) + data[request]["success"] is False + data[request]["message"] is not None + data[request]["code"] == code + + +def assert_empty(response): + assert response.status_code == 200 + assert response.json().get("data") is None + + +def assert_data(response): + assert response.status_code == 200 + data = response.json().get("data") + assert data is not None + return data diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index fce99f7..3db8647 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -5,9 +5,15 @@ from tests.common import ( RECOVERY_KEY_VALIDATION_DATETIME, DEVICE_KEY_VALIDATION_DATETIME, NearFuture, - generate_api_query + generate_api_query, ) from tests.conftest import DEVICE_WE_AUTH_TESTS_WITH, TOKENS_FILE_CONTENTS +from tests.test_graphql.common import ( + assert_data, + assert_empty, + assert_ok, + assert_errorcode, +) ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] @@ -59,32 +65,6 @@ def assert_original(client): assert device["isCaller"] is False -def assert_ok(response, request): - data = assert_data(response) - data[request]["success"] is True - data[request]["message"] is not None - data[request]["code"] == 200 - - -def assert_errorcode(response, request, code): - data = assert_data(response) - data[request]["success"] is False - data[request]["message"] is not None - data[request]["code"] == code - - -def assert_empty(response): - assert response.status_code == 200 - assert response.json().get("data") is None - - -def assert_data(response): - assert response.status_code == 200 - data = response.json().get("data") - assert data is not None - return data - - def set_client_token(client, token): client.headers.update({"Authorization": "Bearer " + token}) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 2cb824f..f34f12a 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -4,6 +4,7 @@ import datetime from tests.common import generate_api_query, mnemonic_to_hex, read_json, write_json +from tests.test_graphql.common import assert_empty API_RECOVERY_QUERY = """ recoveryKey { @@ -21,8 +22,7 @@ def test_graphql_recovery_key_status_unauthorized(client, tokens_file): "/graphql", json={"query": generate_api_query([API_RECOVERY_QUERY])}, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_recovery_key_status_when_none_exists(authorized_client, tokens_file): @@ -354,8 +354,7 @@ def test_graphql_generate_recovery_key_with_invalid_time_format( }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) assert "recovery_token" not in read_json(tokens_file) diff --git a/tests/test_graphql/test_ssh.py b/tests/test_graphql/test_ssh.py index 4831692..38c40f1 100644 --- a/tests/test_graphql/test_ssh.py +++ b/tests/test_graphql/test_ssh.py @@ -3,6 +3,7 @@ import pytest from tests.common import read_json +from tests.test_graphql.common import assert_empty class ProcessMock: @@ -70,8 +71,7 @@ def test_graphql_add_ssh_key_unauthorized(client, some_users, mock_subprocess_po }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_add_ssh_key(authorized_client, some_users, mock_subprocess_popen): @@ -227,8 +227,7 @@ def test_graphql_remove_ssh_key_unauthorized(client, some_users, mock_subprocess }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_remove_ssh_key(authorized_client, some_users, mock_subprocess_popen): diff --git a/tests/test_graphql/test_system.py b/tests/test_graphql/test_system.py index a021a16..5fdc06a 100644 --- a/tests/test_graphql/test_system.py +++ b/tests/test_graphql/test_system.py @@ -5,6 +5,7 @@ import os import pytest from tests.common import generate_system_query, read_json +from tests.test_graphql.common import assert_empty @pytest.fixture @@ -144,8 +145,7 @@ def test_graphql_get_python_version_wrong_auth( "query": generate_system_query([API_PYTHON_VERSION_INFO]), }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_get_python_version(authorized_client, mock_subprocess_check_output): @@ -181,8 +181,7 @@ def test_graphql_get_system_version_unauthorized( }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) assert mock_subprocess_check_output.call_count == 0 @@ -348,8 +347,7 @@ def test_graphql_get_timezone_unauthorized(client, turned_on): "query": generate_system_query([API_GET_TIMEZONE]), }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_get_timezone(authorized_client, turned_on): @@ -403,8 +401,7 @@ def test_graphql_change_timezone_unauthorized(client, turned_on): }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_change_timezone(authorized_client, turned_on): @@ -507,8 +504,7 @@ def test_graphql_get_auto_upgrade_unauthorized(client, turned_on): "query": generate_system_query([API_GET_AUTO_UPGRADE_SETTINGS_QUERY]), }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_get_auto_upgrade(authorized_client, turned_on): @@ -614,8 +610,7 @@ def test_graphql_change_auto_upgrade_unauthorized(client, turned_on): }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_change_auto_upgrade(authorized_client, turned_on): @@ -843,8 +838,7 @@ def test_graphql_pull_system_configuration_unauthorized(client, mock_subprocess_ }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) assert mock_subprocess_popen.call_count == 0 diff --git a/tests/test_graphql/test_users.py b/tests/test_graphql/test_users.py index 7a65736..503335d 100644 --- a/tests/test_graphql/test_users.py +++ b/tests/test_graphql/test_users.py @@ -6,6 +6,7 @@ from tests.common import ( generate_users_query, read_json, ) +from tests.test_graphql.common import assert_empty invalid_usernames = [ "messagebus", @@ -125,8 +126,7 @@ def test_graphql_get_users_unauthorized(client, some_users, mock_subprocess_pope "query": generate_users_query([API_USERS_INFO]), }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_get_some_users(authorized_client, some_users, mock_subprocess_popen): @@ -192,8 +192,7 @@ def test_graphql_get_one_user_unauthorized(client, one_user, mock_subprocess_pop }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_get_one_user(authorized_client, one_user, mock_subprocess_popen): @@ -321,8 +320,7 @@ def test_graphql_add_user_unauthorize(client, one_user, mock_subprocess_popen): }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_add_user(authorized_client, one_user, mock_subprocess_popen): @@ -570,8 +568,7 @@ def test_graphql_delete_user_unauthorized(client, some_users, mock_subprocess_po "variables": {"username": "user1"}, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_delete_user(authorized_client, some_users, mock_subprocess_popen): @@ -675,8 +672,7 @@ def test_graphql_update_user_unauthorized(client, some_users, mock_subprocess_po }, }, ) - assert response.status_code == 200 - assert response.json().get("data") is None + assert_empty(response) def test_graphql_update_user(authorized_client, some_users, mock_subprocess_popen): From 503c9c99effcbee13264616e87dd838b86a2a268 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 10:34:52 +0000 Subject: [PATCH 45/81] test(tokens-repo): break out getting status --- tests/test_graphql/test_api_recovery.py | 37 +++++++++++++++---------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index f34f12a..9e12c0e 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -4,7 +4,7 @@ import datetime from tests.common import generate_api_query, mnemonic_to_hex, read_json, write_json -from tests.test_graphql.common import assert_empty +from tests.test_graphql.common import assert_empty, assert_data API_RECOVERY_QUERY = """ recoveryKey { @@ -17,27 +17,34 @@ recoveryKey { """ -def test_graphql_recovery_key_status_unauthorized(client, tokens_file): - response = client.post( +def request_recovery_status(client): + return client.post( "/graphql", json={"query": generate_api_query([API_RECOVERY_QUERY])}, ) + + +def graphql_recovery_status(client): + response = request_recovery_status(client) + data = assert_data(response) + + status = data["api"]["recoveryKey"] + assert status is not None + return status + + +def test_graphql_recovery_key_status_unauthorized(client, tokens_file): + response = request_recovery_status(client) assert_empty(response) def test_graphql_recovery_key_status_when_none_exists(authorized_client, tokens_file): - response = authorized_client.post( - "/graphql", - json={"query": generate_api_query([API_RECOVERY_QUERY])}, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["api"]["recoveryKey"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["exists"] is False - assert response.json()["data"]["api"]["recoveryKey"]["valid"] is False - assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is None - assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None - assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None + status = graphql_recovery_status(authorized_client) + assert status["exists"] is False + assert status["valid"] is False + assert status["creationDate"] is None + assert status["expirationDate"] is None + assert status["usesLeft"] is None API_RECOVERY_KEY_GENERATE_MUTATION = """ From 851d90b30c6d0de73c37e44972b698f42d494f7d Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 10:48:59 +0000 Subject: [PATCH 46/81] test(tokens-repo): break out getting recovery key --- tests/test_graphql/test_api_recovery.py | 35 ++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 9e12c0e..dd7d3e4 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -4,7 +4,7 @@ import datetime from tests.common import generate_api_query, mnemonic_to_hex, read_json, write_json -from tests.test_graphql.common import assert_empty, assert_data +from tests.test_graphql.common import assert_empty, assert_data, assert_ok API_RECOVERY_QUERY = """ recoveryKey { @@ -33,6 +33,20 @@ def graphql_recovery_status(client): return status +def graphql_get_new_recovery_key(client): + response = client.post( + "/graphql", + json={ + "query": API_RECOVERY_KEY_GENERATE_MUTATION, + }, + ) + assert_ok(response, "getNewRecoveryApiKey") + key = response.json()["data"]["getNewRecoveryApiKey"]["key"] + assert key is not None + assert key.split(" ").__len__() == 18 + return key + + def test_graphql_recovery_key_status_unauthorized(client, tokens_file): response = request_recovery_status(client) assert_empty(response) @@ -71,26 +85,11 @@ mutation TestUseRecoveryKey($input: UseRecoveryKeyInput!) { def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_GENERATE_MUTATION, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is True - assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is not None - assert ( - response.json()["data"]["getNewRecoveryApiKey"]["key"].split(" ").__len__() - == 18 - ) + key = graphql_get_new_recovery_key(authorized_client) + assert read_json(tokens_file)["recovery_token"] is not None time_generated = read_json(tokens_file)["recovery_token"]["date"] assert time_generated is not None - key = response.json()["data"]["getNewRecoveryApiKey"]["key"] assert ( datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - datetime.timedelta(seconds=5) From 6cb9cc6d03d8d71d83c6f4bfdb138a406904b364 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 10:59:59 +0000 Subject: [PATCH 47/81] test(tokens-repo): use assert recent --- tests/common.py | 8 ++++++++ tests/test_graphql/test_api_recovery.py | 15 ++++++++------- tests/test_rest_endpoints/test_auth.py | 9 +-------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/common.py b/tests/common.py index 95488cc..a49885a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -37,3 +37,11 @@ def generate_users_query(query_array): def mnemonic_to_hex(mnemonic): return Mnemonic(language="english").to_entropy(mnemonic).hex() + + +def assert_recovery_recent(time_generated): + assert ( + datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") + - datetime.timedelta(seconds=5) + < datetime.datetime.now() + ) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index dd7d3e4..2f97513 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -3,7 +3,13 @@ # pylint: disable=missing-function-docstring import datetime -from tests.common import generate_api_query, mnemonic_to_hex, read_json, write_json +from tests.common import ( + generate_api_query, + mnemonic_to_hex, + read_json, + write_json, + assert_recovery_recent, +) from tests.test_graphql.common import assert_empty, assert_data, assert_ok API_RECOVERY_QUERY = """ @@ -90,12 +96,7 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): assert read_json(tokens_file)["recovery_token"] is not None time_generated = read_json(tokens_file)["recovery_token"]["date"] assert time_generated is not None - assert ( - datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - - datetime.timedelta(seconds=5) - < datetime.datetime.now() - ) - + assert_recovery_recent(time_generated) # Try to get token status response = authorized_client.post( "/graphql", diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 1632e22..ff161fb 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -9,6 +9,7 @@ from tests.common import ( RECOVERY_KEY_VALIDATION_DATETIME, DEVICE_KEY_VALIDATION_DATETIME, NearFuture, + assert_recovery_recent, ) DATE_FORMATS = [ @@ -90,14 +91,6 @@ def rest_get_recovery_date(client): return status["date"] -def assert_recovery_recent(time_generated): - assert ( - datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - - datetime.timedelta(seconds=5) - < datetime.datetime.now() - ) - - def assert_no_recovery(client): assert not rest_get_recovery_status(client)["exists"] From 92b2a674799fc965ac6154ab428d89c3c7ecfbec Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 11:08:53 +0000 Subject: [PATCH 48/81] test(tokens-repo): use get recovery status in test of recovery use --- tests/test_graphql/test_api_recovery.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 2f97513..aacc96c 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -93,25 +93,12 @@ mutation TestUseRecoveryKey($input: UseRecoveryKeyInput!) { def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): key = graphql_get_new_recovery_key(authorized_client) - assert read_json(tokens_file)["recovery_token"] is not None - time_generated = read_json(tokens_file)["recovery_token"]["date"] - assert time_generated is not None - assert_recovery_recent(time_generated) - # Try to get token status - response = authorized_client.post( - "/graphql", - json={"query": generate_api_query([API_RECOVERY_QUERY])}, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["api"]["recoveryKey"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True - assert response.json()["data"]["api"]["recoveryKey"]["valid"] is True - assert response.json()["data"]["api"]["recoveryKey"][ - "creationDate" - ] == time_generated.replace("Z", "") - assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None - assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None + status = graphql_recovery_status(authorized_client) + assert status["exists"] is True + assert status["valid"] is True + assert_recovery_recent(status["creationDate"]) + assert status["expirationDate"] is None + assert status["usesLeft"] is None # Try to use token response = client.post( From 137ae58b421d5e8734a463aa66e43aead61dc11c Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 11:25:53 +0000 Subject: [PATCH 49/81] test(tokens-repo): break out using recovery key --- tests/test_graphql/test_api_recovery.py | 44 ++++++++++++------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index aacc96c..20204df 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -53,6 +53,25 @@ def graphql_get_new_recovery_key(client): return key +def graphql_use_recovery_key(client, key, device_name): + response = client.post( + "/graphql", + json={ + "query": API_RECOVERY_KEY_USE_MUTATION, + "variables": { + "input": { + "key": key, + "deviceName": device_name, + }, + }, + }, + ) + assert_ok(response, "useRecoveryApiKey") + token = response.json()["data"]["useRecoveryApiKey"]["token"] + assert token is not None + return token + + def test_graphql_recovery_key_status_unauthorized(client, tokens_file): response = request_recovery_status(client) assert_empty(response) @@ -100,29 +119,8 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): assert status["expirationDate"] is None assert status["usesLeft"] is None - # Try to use token - response = client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_USE_MUTATION, - "variables": { - "input": { - "key": key, - "deviceName": "new_test_token", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["useRecoveryApiKey"]["success"] is True - assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None - assert ( - response.json()["data"]["useRecoveryApiKey"]["token"] - == read_json(tokens_file)["tokens"][2]["token"] - ) + token = graphql_use_recovery_key(client, key, "new_test_token") + assert token == read_json(tokens_file)["tokens"][2]["token"] assert read_json(tokens_file)["tokens"][2]["name"] == "new_test_token" # Try to use token again From de2703219141b8d86bbde8b6423eb7ce35237644 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 11:46:17 +0000 Subject: [PATCH 50/81] test(tokens-repo): move token utils to graphql common --- tests/test_graphql/common.py | 36 ++++++++++++++++++++++++ tests/test_graphql/test_api_devices.py | 38 ++++---------------------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/tests/test_graphql/common.py b/tests/test_graphql/common.py index f2cc54d..7db5b35 100644 --- a/tests/test_graphql/common.py +++ b/tests/test_graphql/common.py @@ -1,3 +1,6 @@ +from tests.common import generate_api_query + + def assert_ok(response, request): data = assert_data(response) data[request]["success"] is True @@ -22,3 +25,36 @@ def assert_data(response): data = response.json().get("data") assert data is not None return data + + +API_DEVICES_QUERY = """ +devices { + creationDate + isCaller + name +} +""" + + +def request_devices(client): + return client.post( + "/graphql", + json={"query": generate_api_query([API_DEVICES_QUERY])}, + ) + + +def graphql_get_devices(client): + response = request_devices(client) + data = assert_data(response) + devices = data["api"]["devices"] + assert devices is not None + return devices + + +def set_client_token(client, token): + client.headers.update({"Authorization": "Bearer " + token}) + + +def assert_token_valid(client, token): + set_client_token(client, token) + assert graphql_get_devices(client) is not None diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 3db8647..673ed53 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -13,29 +13,15 @@ from tests.test_graphql.common import ( assert_empty, assert_ok, assert_errorcode, + assert_token_valid, + graphql_get_devices, + request_devices, + set_client_token, + API_DEVICES_QUERY, ) ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] -API_DEVICES_QUERY = """ -devices { - creationDate - isCaller - name -} -""" - - -def graphql_get_devices(client): - response = client.post( - "/graphql", - json={"query": generate_api_query([API_DEVICES_QUERY])}, - ) - data = assert_data(response) - devices = data["api"]["devices"] - assert devices is not None - return devices - def graphql_get_caller_token_info(client): devices = graphql_get_devices(client) @@ -65,15 +51,6 @@ def assert_original(client): assert device["isCaller"] is False -def set_client_token(client, token): - client.headers.update({"Authorization": "Bearer " + token}) - - -def assert_token_valid(client, token): - set_client_token(client, token) - assert graphql_get_devices(client) is not None - - def graphql_get_new_device_key(authorized_client) -> str: response = authorized_client.post( "/graphql", @@ -113,10 +90,7 @@ def test_graphql_tokens_info(authorized_client, tokens_file): def test_graphql_tokens_info_unauthorized(client, tokens_file): - response = client.post( - "/graphql", - json={"query": generate_api_query([API_DEVICES_QUERY])}, - ) + response = request_devices(client) assert_empty(response) From ce4fbdae0a90103b1c06906051885db90ce6df05 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 11:57:51 +0000 Subject: [PATCH 51/81] test(tokens-repo): check for token existense in recovery tests --- tests/test_graphql/test_api_recovery.py | 45 +++++++++---------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 20204df..04e4f6e 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -10,7 +10,14 @@ from tests.common import ( write_json, assert_recovery_recent, ) -from tests.test_graphql.common import assert_empty, assert_data, assert_ok +from tests.test_graphql.common import ( + assert_empty, + assert_data, + assert_ok, + assert_token_valid, + graphql_get_devices, + set_client_token, +) API_RECOVERY_QUERY = """ recoveryKey { @@ -69,6 +76,11 @@ def graphql_use_recovery_key(client, key, device_name): assert_ok(response, "useRecoveryApiKey") token = response.json()["data"]["useRecoveryApiKey"]["token"] assert token is not None + assert_token_valid(client, token) + set_client_token(client, token) + assert "new_test_token" in [ + device["name"] for device in graphql_get_devices(client) + ] return token @@ -119,34 +131,9 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): assert status["expirationDate"] is None assert status["usesLeft"] is None - token = graphql_use_recovery_key(client, key, "new_test_token") - assert token == read_json(tokens_file)["tokens"][2]["token"] - assert read_json(tokens_file)["tokens"][2]["name"] == "new_test_token" - - # Try to use token again - response = client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_USE_MUTATION, - "variables": { - "input": { - "key": key, - "deviceName": "new_test_token2", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["useRecoveryApiKey"]["success"] is True - assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None - assert ( - response.json()["data"]["useRecoveryApiKey"]["token"] - == read_json(tokens_file)["tokens"][3]["token"] - ) - assert read_json(tokens_file)["tokens"][3]["name"] == "new_test_token2" + graphql_use_recovery_key(client, key, "new_test_token") + # And again + graphql_use_recovery_key(client, key, "new_test_token2") def test_graphql_generate_recovery_key_with_expiration_date( From 18f5ff815c567770abbda736c9c3e34b8e8f7866 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 6 Jan 2023 13:09:54 +0000 Subject: [PATCH 52/81] test(tokens-repo): rework expiring recovery key tests --- tests/test_graphql/common.py | 24 +++ tests/test_graphql/test_api_devices.py | 26 +-- tests/test_graphql/test_api_recovery.py | 205 +++++++----------------- 3 files changed, 82 insertions(+), 173 deletions(-) diff --git a/tests/test_graphql/common.py b/tests/test_graphql/common.py index 7db5b35..03f48b7 100644 --- a/tests/test_graphql/common.py +++ b/tests/test_graphql/common.py @@ -1,4 +1,7 @@ from tests.common import generate_api_query +from tests.conftest import TOKENS_FILE_CONTENTS, DEVICE_WE_AUTH_TESTS_WITH + +ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] def assert_ok(response, request): @@ -58,3 +61,24 @@ def set_client_token(client, token): def assert_token_valid(client, token): set_client_token(client, token) assert graphql_get_devices(client) is not None + + +def assert_same(graphql_devices, abstract_devices): + """Orderless comparison""" + assert len(graphql_devices) == len(abstract_devices) + for original_device in abstract_devices: + assert original_device["name"] in [device["name"] for device in graphql_devices] + for device in graphql_devices: + if device["name"] == original_device["name"]: + assert device["creationDate"] == original_device["date"].isoformat() + + +def assert_original(client): + devices = graphql_get_devices(client) + assert_same(devices, ORIGINAL_DEVICES) + + for device in devices: + if device["name"] == DEVICE_WE_AUTH_TESTS_WITH["name"]: + assert device["isCaller"] is True + else: + assert device["isCaller"] is False diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 673ed53..b9dd808 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -14,14 +14,15 @@ from tests.test_graphql.common import ( assert_ok, assert_errorcode, assert_token_valid, + assert_original, + assert_same, graphql_get_devices, request_devices, set_client_token, API_DEVICES_QUERY, + ORIGINAL_DEVICES, ) -ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] - def graphql_get_caller_token_info(client): devices = graphql_get_devices(client) @@ -30,27 +31,6 @@ def graphql_get_caller_token_info(client): return device -def assert_same(graphql_devices, abstract_devices): - """Orderless comparison""" - assert len(graphql_devices) == len(abstract_devices) - for original_device in abstract_devices: - assert original_device["name"] in [device["name"] for device in graphql_devices] - for device in graphql_devices: - if device["name"] == original_device["name"]: - assert device["creationDate"] == original_device["date"].isoformat() - - -def assert_original(client): - devices = graphql_get_devices(client) - assert_same(devices, ORIGINAL_DEVICES) - - for device in devices: - if device["name"] == DEVICE_WE_AUTH_TESTS_WITH["name"]: - assert device["isCaller"] is True - else: - assert device["isCaller"] is False - - def graphql_get_new_device_key(authorized_client) -> str: response = authorized_client.post( "/graphql", diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 04e4f6e..47332aa 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -9,12 +9,16 @@ from tests.common import ( read_json, write_json, assert_recovery_recent, + NearFuture, + RECOVERY_KEY_VALIDATION_DATETIME, ) from tests.test_graphql.common import ( assert_empty, assert_data, assert_ok, + assert_errorcode, assert_token_valid, + assert_original, graphql_get_devices, set_client_token, ) @@ -46,13 +50,24 @@ def graphql_recovery_status(client): return status -def graphql_get_new_recovery_key(client): - response = client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_GENERATE_MUTATION, - }, - ) +def request_make_new_recovery_key(client, expires_at=None, uses=None): + json = {"query": API_RECOVERY_KEY_GENERATE_MUTATION} + limits = {} + + if expires_at is not None: + limits["expirationDate"] = expires_at.isoformat() + if uses is not None: + limits["uses"] = uses + + if limits != {}: + json["variables"] = {"limits": limits} + + response = client.post("/graphql", json=json) + return response + + +def graphql_make_new_recovery_key(client, expires_at=None, uses=None): + response = request_make_new_recovery_key(client, expires_at, uses) assert_ok(response, "getNewRecoveryApiKey") key = response.json()["data"]["getNewRecoveryApiKey"]["key"] assert key is not None @@ -60,8 +75,8 @@ def graphql_get_new_recovery_key(client): return key -def graphql_use_recovery_key(client, key, device_name): - response = client.post( +def request_recovery_auth(client, key, device_name): + return client.post( "/graphql", json={ "query": API_RECOVERY_KEY_USE_MUTATION, @@ -73,6 +88,10 @@ def graphql_use_recovery_key(client, key, device_name): }, }, ) + + +def graphql_use_recovery_key(client, key, device_name): + response = request_recovery_auth(client, key, device_name) assert_ok(response, "useRecoveryApiKey") token = response.json()["data"]["useRecoveryApiKey"]["token"] assert token is not None @@ -122,7 +141,7 @@ mutation TestUseRecoveryKey($input: UseRecoveryKeyInput!) { def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): - key = graphql_get_new_recovery_key(authorized_client) + key = graphql_make_new_recovery_key(authorized_client) status = graphql_recovery_status(authorized_client) assert status["exists"] is True @@ -140,154 +159,40 @@ def test_graphql_generate_recovery_key_with_expiration_date( client, authorized_client, tokens_file ): expiration_date = datetime.datetime.now() + datetime.timedelta(minutes=5) - expiration_date_str = expiration_date.strftime("%Y-%m-%dT%H:%M:%S.%f") - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_GENERATE_MUTATION, - "variables": { - "limits": { - "expirationDate": expiration_date_str, - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is True - assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is not None - assert ( - response.json()["data"]["getNewRecoveryApiKey"]["key"].split(" ").__len__() - == 18 - ) - assert read_json(tokens_file)["recovery_token"] is not None + key = graphql_make_new_recovery_key(authorized_client, expires_at=expiration_date) - key = response.json()["data"]["getNewRecoveryApiKey"]["key"] - assert read_json(tokens_file)["recovery_token"]["expiration"] == expiration_date_str - assert read_json(tokens_file)["recovery_token"]["token"] == mnemonic_to_hex(key) + status = graphql_recovery_status(authorized_client) + assert status["exists"] is True + assert status["valid"] is True + assert_recovery_recent(status["creationDate"]) + assert status["expirationDate"] == expiration_date.isoformat() + assert status["usesLeft"] is None - time_generated = read_json(tokens_file)["recovery_token"]["date"] - assert time_generated is not None - assert ( - datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - - datetime.timedelta(seconds=5) - < datetime.datetime.now() - ) + graphql_use_recovery_key(client, key, "new_test_token") + # And again + graphql_use_recovery_key(client, key, "new_test_token2") - # Try to get token status - response = authorized_client.post( - "/graphql", - json={"query": generate_api_query([API_RECOVERY_QUERY])}, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["api"]["recoveryKey"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True - assert response.json()["data"]["api"]["recoveryKey"]["valid"] is True - assert response.json()["data"]["api"]["recoveryKey"][ - "creationDate" - ] == time_generated.replace("Z", "") - assert ( - response.json()["data"]["api"]["recoveryKey"]["expirationDate"] - == expiration_date_str - ) - assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None - # Try to use token - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_USE_MUTATION, - "variables": { - "input": { - "key": key, - "deviceName": "new_test_token", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["useRecoveryApiKey"]["success"] is True - assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None - assert ( - response.json()["data"]["useRecoveryApiKey"]["token"] - == read_json(tokens_file)["tokens"][2]["token"] - ) +def test_graphql_use_recovery_key_after_expiration( + client, authorized_client, tokens_file, mocker +): + expiration_date = datetime.datetime.now() + datetime.timedelta(minutes=5) + key = graphql_make_new_recovery_key(authorized_client, expires_at=expiration_date) - # Try to use token again - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_USE_MUTATION, - "variables": { - "input": { - "key": key, - "deviceName": "new_test_token2", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["useRecoveryApiKey"]["success"] is True - assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None - assert ( - response.json()["data"]["useRecoveryApiKey"]["token"] - == read_json(tokens_file)["tokens"][3]["token"] - ) + # Timewarp to after it expires + mock = mocker.patch(RECOVERY_KEY_VALIDATION_DATETIME, NearFuture) - # Try to use token after expiration date - new_data = read_json(tokens_file) - new_data["recovery_token"]["expiration"] = ( - datetime.datetime.now() - datetime.timedelta(minutes=5) - ).strftime("%Y-%m-%dT%H:%M:%S.%f") - write_json(tokens_file, new_data) - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_USE_MUTATION, - "variables": { - "input": { - "key": key, - "deviceName": "new_test_token3", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["useRecoveryApiKey"]["success"] is False - assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["useRecoveryApiKey"]["code"] == 404 + response = request_recovery_auth(client, key, "new_test_token3") + assert_errorcode(response, "useRecoveryApiKey", 404) assert response.json()["data"]["useRecoveryApiKey"]["token"] is None + assert_original(authorized_client) - assert read_json(tokens_file)["tokens"] == new_data["tokens"] - - # Try to get token status - response = authorized_client.post( - "/graphql", - json={"query": generate_api_query([API_RECOVERY_QUERY])}, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["api"]["recoveryKey"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True - assert response.json()["data"]["api"]["recoveryKey"]["valid"] is False - assert ( - response.json()["data"]["api"]["recoveryKey"]["creationDate"] == time_generated - ) - assert ( - response.json()["data"]["api"]["recoveryKey"]["expirationDate"] - == new_data["recovery_token"]["expiration"] - ) - assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] is None + status = graphql_recovery_status(authorized_client) + assert status["exists"] is True + assert status["valid"] is False + assert_recovery_recent(status["creationDate"]) + assert status["expirationDate"] == expiration_date.isoformat() + assert status["usesLeft"] is None def test_graphql_generate_recovery_key_with_expiration_in_the_past( From 2d6406c8c1eb9f2cd8ae70c10d9ac69e0437d275 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 9 Jan 2023 12:17:36 +0000 Subject: [PATCH 53/81] test(tokens-repo): rework recovery expiration in the past --- tests/test_graphql/test_api_recovery.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 47332aa..a02b582 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -199,26 +199,12 @@ def test_graphql_generate_recovery_key_with_expiration_in_the_past( authorized_client, tokens_file ): expiration_date = datetime.datetime.now() - datetime.timedelta(minutes=5) - expiration_date_str = expiration_date.strftime("%Y-%m-%dT%H:%M:%S.%f") - - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_GENERATE_MUTATION, - "variables": { - "limits": { - "expirationDate": expiration_date_str, - }, - }, - }, + response = request_make_new_recovery_key( + authorized_client, expires_at=expiration_date ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is False - assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 400 + + assert_errorcode(response, "getNewRecoveryApiKey", 400) assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None - assert "recovery_token" not in read_json(tokens_file) def test_graphql_generate_recovery_key_with_invalid_time_format( From 0b28fa2637ddd7f4ec043b9afcd762053d93a7d5 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 9 Jan 2023 12:39:54 +0000 Subject: [PATCH 54/81] test(tokens-repo): rework limited uses test --- tests/test_graphql/test_api_recovery.py | 149 ++++-------------------- 1 file changed, 25 insertions(+), 124 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index a02b582..c6ccbf9 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -97,9 +97,7 @@ def graphql_use_recovery_key(client, key, device_name): assert token is not None assert_token_valid(client, token) set_client_token(client, token) - assert "new_test_token" in [ - device["name"] for device in graphql_get_devices(client) - ] + assert device_name in [device["name"] for device in graphql_get_devices(client)] return token @@ -230,135 +228,38 @@ def test_graphql_generate_recovery_key_with_invalid_time_format( def test_graphql_generate_recovery_key_with_limited_uses( - authorized_client, tokens_file + authorized_client, client, tokens_file ): - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_GENERATE_MUTATION, - "variables": { - "limits": { - "expirationDate": None, - "uses": 2, - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is True - assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is not None + mnemonic_key = graphql_make_new_recovery_key(authorized_client, uses=2) - mnemonic_key = response.json()["data"]["getNewRecoveryApiKey"]["key"] - key = mnemonic_to_hex(mnemonic_key) + status = graphql_recovery_status(authorized_client) + assert status["exists"] is True + assert status["valid"] is True + assert status["creationDate"] is not None + assert status["expirationDate"] is None + assert status["usesLeft"] == 2 - assert read_json(tokens_file)["recovery_token"]["token"] == key - assert read_json(tokens_file)["recovery_token"]["uses_left"] == 2 + graphql_use_recovery_key(client, mnemonic_key, "new_test_token1") - # Try to get token status - response = authorized_client.post( - "/graphql", - json={"query": generate_api_query([API_RECOVERY_QUERY])}, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["api"]["recoveryKey"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True - assert response.json()["data"]["api"]["recoveryKey"]["valid"] is True - assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None - assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] == 2 + status = graphql_recovery_status(authorized_client) + assert status["exists"] is True + assert status["valid"] is True + assert status["creationDate"] is not None + assert status["expirationDate"] is None + assert status["usesLeft"] == 1 - # Try to use token - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_USE_MUTATION, - "variables": { - "input": { - "key": mnemonic_key, - "deviceName": "test_token1", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["useRecoveryApiKey"]["success"] is True - assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None + graphql_use_recovery_key(client, mnemonic_key, "new_test_token2") - # Try to get token status - response = authorized_client.post( - "/graphql", - json={"query": generate_api_query([API_RECOVERY_QUERY])}, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["api"]["recoveryKey"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True - assert response.json()["data"]["api"]["recoveryKey"]["valid"] is True - assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None - assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] == 1 + status = graphql_recovery_status(authorized_client) + assert status["exists"] is True + assert status["valid"] is False + assert status["creationDate"] is not None + assert status["expirationDate"] is None + assert status["usesLeft"] == 0 - # Try to use token - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_USE_MUTATION, - "variables": { - "input": { - "key": mnemonic_key, - "deviceName": "test_token2", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["useRecoveryApiKey"]["success"] is True - assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["useRecoveryApiKey"]["code"] == 200 - assert response.json()["data"]["useRecoveryApiKey"]["token"] is not None - - # Try to get token status - response = authorized_client.post( - "/graphql", - json={"query": generate_api_query([API_RECOVERY_QUERY])}, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["api"]["recoveryKey"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["exists"] is True - assert response.json()["data"]["api"]["recoveryKey"]["valid"] is False - assert response.json()["data"]["api"]["recoveryKey"]["creationDate"] is not None - assert response.json()["data"]["api"]["recoveryKey"]["expirationDate"] is None - assert response.json()["data"]["api"]["recoveryKey"]["usesLeft"] == 0 - - # Try to use token - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_USE_MUTATION, - "variables": { - "input": { - "key": mnemonic_key, - "deviceName": "test_token3", - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["useRecoveryApiKey"]["success"] is False - assert response.json()["data"]["useRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["useRecoveryApiKey"]["code"] == 404 - assert response.json()["data"]["useRecoveryApiKey"]["token"] is None + response = request_recovery_auth(client, mnemonic_key, "new_test_token3") + assert_errorcode(response, "useRecoveryApiKey", 404) def test_graphql_generate_recovery_key_with_negative_uses( From 72fdd412d9f7fa6c8cfd59d0e2b084de9c0e1230 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 9 Jan 2023 12:44:48 +0000 Subject: [PATCH 55/81] test(tokens-repo): complete the recovery test rework --- tests/test_graphql/test_api_recovery.py | 40 ++++--------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index c6ccbf9..d7ce667 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -265,42 +265,14 @@ def test_graphql_generate_recovery_key_with_limited_uses( def test_graphql_generate_recovery_key_with_negative_uses( authorized_client, tokens_file ): - # Try to get token status - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_GENERATE_MUTATION, - "variables": { - "limits": { - "uses": -1, - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is False - assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 400 + response = request_make_new_recovery_key(authorized_client, uses=-1) + + assert_errorcode(response, "getNewRecoveryApiKey", 400) assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None def test_graphql_generate_recovery_key_with_zero_uses(authorized_client, tokens_file): - # Try to get token status - response = authorized_client.post( - "/graphql", - json={ - "query": API_RECOVERY_KEY_GENERATE_MUTATION, - "variables": { - "limits": { - "uses": 0, - }, - }, - }, - ) - assert response.status_code == 200 - assert response.json().get("data") is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["success"] is False - assert response.json()["data"]["getNewRecoveryApiKey"]["message"] is not None - assert response.json()["data"]["getNewRecoveryApiKey"]["code"] == 400 + response = request_make_new_recovery_key(authorized_client, uses=0) + + assert_errorcode(response, "getNewRecoveryApiKey", 400) assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None From e5756a0dd1e0131d8e52183e49b50029aaf061cd Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 9 Jan 2023 12:54:10 +0000 Subject: [PATCH 56/81] test(tokens-repo): cleanup recovery tests --- tests/test_graphql/test_api_recovery.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index d7ce667..9d6e671 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -5,9 +5,6 @@ import datetime from tests.common import ( generate_api_query, - mnemonic_to_hex, - read_json, - write_json, assert_recovery_recent, NearFuture, RECOVERY_KEY_VALIDATION_DATETIME, @@ -203,6 +200,7 @@ def test_graphql_generate_recovery_key_with_expiration_in_the_past( assert_errorcode(response, "getNewRecoveryApiKey", 400) assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None + assert graphql_recovery_status(authorized_client)["exists"] is False def test_graphql_generate_recovery_key_with_invalid_time_format( @@ -223,8 +221,7 @@ def test_graphql_generate_recovery_key_with_invalid_time_format( }, ) assert_empty(response) - - assert "recovery_token" not in read_json(tokens_file) + assert graphql_recovery_status(authorized_client)["exists"] is False def test_graphql_generate_recovery_key_with_limited_uses( @@ -276,3 +273,4 @@ def test_graphql_generate_recovery_key_with_zero_uses(authorized_client, tokens_ assert_errorcode(response, "getNewRecoveryApiKey", 400) assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None + assert graphql_recovery_status(authorized_client)["exists"] is False From 9cc6e304c0ff91140f20cf147a37296152384410 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 9 Jan 2023 15:29:43 +0000 Subject: [PATCH 57/81] test(tokens-repo): remove device order dependence from graphql test__api --- tests/test_graphql/common.py | 4 ++++ tests/test_graphql/test_api.py | 20 ++++++-------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/test_graphql/common.py b/tests/test_graphql/common.py index 03f48b7..d473433 100644 --- a/tests/test_graphql/common.py +++ b/tests/test_graphql/common.py @@ -75,6 +75,10 @@ def assert_same(graphql_devices, abstract_devices): def assert_original(client): devices = graphql_get_devices(client) + assert_original_devices(devices) + + +def assert_original_devices(devices): assert_same(devices, ORIGINAL_DEVICES) for device in devices: diff --git a/tests/test_graphql/test_api.py b/tests/test_graphql/test_api.py index 695dd8e..c252d44 100644 --- a/tests/test_graphql/test_api.py +++ b/tests/test_graphql/test_api.py @@ -3,6 +3,7 @@ # pylint: disable=missing-function-docstring from tests.common import generate_api_query +from tests.test_graphql.common import assert_original_devices from tests.test_graphql.test_api_devices import API_DEVICES_QUERY from tests.test_graphql.test_api_recovery import API_RECOVERY_QUERY from tests.test_graphql.test_api_version import API_VERSION_QUERY @@ -20,20 +21,11 @@ def test_graphql_get_entire_api_data(authorized_client, tokens_file): assert response.status_code == 200 assert response.json().get("data") is not None assert "version" in response.json()["data"]["api"] - assert response.json()["data"]["api"]["devices"] is not None - assert len(response.json()["data"]["api"]["devices"]) == 2 - assert ( - response.json()["data"]["api"]["devices"][0]["creationDate"] - == "2022-01-14T08:31:10.789314" - ) - assert response.json()["data"]["api"]["devices"][0]["isCaller"] is True - assert response.json()["data"]["api"]["devices"][0]["name"] == "test_token" - assert ( - response.json()["data"]["api"]["devices"][1]["creationDate"] - == "2022-01-14T08:31:10.789314" - ) - assert response.json()["data"]["api"]["devices"][1]["isCaller"] is False - assert response.json()["data"]["api"]["devices"][1]["name"] == "test_token2" + + devices = response.json()["data"]["api"]["devices"] + assert devices is not None + assert_original_devices(devices) + assert response.json()["data"]["api"]["recoveryKey"] is not None assert response.json()["data"]["api"]["recoveryKey"]["exists"] is False assert response.json()["data"]["api"]["recoveryKey"]["valid"] is False From 158c1f13a6425d726bfa0810d4f4bb58e6b7dc6a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 11 Jan 2023 17:02:01 +0000 Subject: [PATCH 58/81] refactor(tokens-repo): switch token backend to redis And use timezone-aware comparisons for expiry checks --- selfprivacy_api/actions/api_tokens.py | 20 +++++-- .../models/tokens/new_device_key.py | 10 ++-- selfprivacy_api/models/tokens/recovery_key.py | 10 +++- selfprivacy_api/models/tokens/time.py | 13 +++++ .../tokens/json_tokens_repository.py | 25 ++++++++- .../tokens/redis_tokens_repository.py | 6 +- tests/common.py | 22 +++++--- tests/conftest.py | 22 +++++--- tests/test_graphql/test_api_recovery.py | 12 ++-- .../test_repository/test_tokens_repository.py | 56 +++++++++---------- tests/test_rest_endpoints/test_auth.py | 10 ++-- 11 files changed, 136 insertions(+), 70 deletions(-) create mode 100644 selfprivacy_api/models/tokens/time.py diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 38133fd..2337224 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -1,11 +1,11 @@ """App tokens actions""" -from datetime import datetime +from datetime import datetime, timezone from typing import Optional from pydantic import BaseModel from mnemonic import Mnemonic -from selfprivacy_api.repositories.tokens.json_tokens_repository import ( - JsonTokensRepository, +from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( + RedisTokensRepository, ) from selfprivacy_api.repositories.tokens.exceptions import ( TokenNotFound, @@ -14,7 +14,7 @@ from selfprivacy_api.repositories.tokens.exceptions import ( NewDeviceKeyNotFound, ) -TOKEN_REPO = JsonTokensRepository() +TOKEN_REPO = RedisTokensRepository() class TokenInfoWithIsCaller(BaseModel): @@ -82,6 +82,14 @@ class RecoveryTokenStatus(BaseModel): uses_left: Optional[int] = None +def naive(date_time: datetime) -> datetime: + if date_time is None: + return None + if date_time.tzinfo is not None: + date_time.astimezone(timezone.utc) + return date_time.replace(tzinfo=None) + + def get_api_recovery_token_status() -> RecoveryTokenStatus: """Get the recovery token status""" token = TOKEN_REPO.get_recovery_key() @@ -91,8 +99,8 @@ def get_api_recovery_token_status() -> RecoveryTokenStatus: return RecoveryTokenStatus( exists=True, valid=is_valid, - date=token.created_at, - expiration=token.expires_at, + date=naive(token.created_at), + expiration=naive(token.expires_at), uses_left=token.uses_left, ) diff --git a/selfprivacy_api/models/tokens/new_device_key.py b/selfprivacy_api/models/tokens/new_device_key.py index dda926c..9fbd23b 100644 --- a/selfprivacy_api/models/tokens/new_device_key.py +++ b/selfprivacy_api/models/tokens/new_device_key.py @@ -1,11 +1,13 @@ """ New device key used to obtain access token. """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import secrets from pydantic import BaseModel from mnemonic import Mnemonic +from selfprivacy_api.models.tokens.time import is_past + class NewDeviceKey(BaseModel): """ @@ -22,7 +24,7 @@ class NewDeviceKey(BaseModel): """ Check if the recovery key is valid. """ - if self.expires_at < datetime.now(): + if is_past(self.expires_at): return False return True @@ -37,10 +39,10 @@ class NewDeviceKey(BaseModel): """ Factory to generate a random token. """ - creation_date = datetime.now() + creation_date = datetime.now(timezone.utc) key = secrets.token_bytes(16).hex() return NewDeviceKey( key=key, created_at=creation_date, - expires_at=datetime.now() + timedelta(minutes=10), + expires_at=creation_date + timedelta(minutes=10), ) diff --git a/selfprivacy_api/models/tokens/recovery_key.py b/selfprivacy_api/models/tokens/recovery_key.py index 098aceb..3b81398 100644 --- a/selfprivacy_api/models/tokens/recovery_key.py +++ b/selfprivacy_api/models/tokens/recovery_key.py @@ -3,12 +3,14 @@ Recovery key used to obtain access token. Recovery key has a token string, date of creation, optional date of expiration and optional count of uses left. """ -from datetime import datetime +from datetime import datetime, timezone import secrets from typing import Optional from pydantic import BaseModel from mnemonic import Mnemonic +from selfprivacy_api.models.tokens.time import is_past, ensure_timezone + class RecoveryKey(BaseModel): """ @@ -26,7 +28,7 @@ class RecoveryKey(BaseModel): """ Check if the recovery key is valid. """ - if self.expires_at is not None and self.expires_at < datetime.now(): + if self.expires_at is not None and is_past(self.expires_at): return False if self.uses_left is not None and self.uses_left <= 0: return False @@ -46,7 +48,9 @@ class RecoveryKey(BaseModel): """ Factory to generate a random token. """ - creation_date = datetime.now() + creation_date = datetime.now(timezone.utc) + if expiration is not None: + expiration = ensure_timezone(expiration) key = secrets.token_bytes(24).hex() return RecoveryKey( key=key, diff --git a/selfprivacy_api/models/tokens/time.py b/selfprivacy_api/models/tokens/time.py new file mode 100644 index 0000000..35fd992 --- /dev/null +++ b/selfprivacy_api/models/tokens/time.py @@ -0,0 +1,13 @@ +from datetime import datetime, timezone + +def is_past(dt: datetime) -> bool: + # we cannot compare a naive now() + # to dt which might be tz-aware or unaware + dt = ensure_timezone(dt) + return dt < datetime.now(timezone.utc) + +def ensure_timezone(dt:datetime) -> datetime: + if dt.tzinfo is None or dt.tzinfo.utcoffset(None) is None: + dt = dt.replace(tzinfo= timezone.utc) + return dt + diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index 77e1311..09204a8 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -2,7 +2,7 @@ temporary legacy """ from typing import Optional -from datetime import datetime +from datetime import datetime, timezone from selfprivacy_api.utils import UserDataFiles, WriteUserData, ReadUserData from selfprivacy_api.models.tokens.token import Token @@ -15,6 +15,7 @@ from selfprivacy_api.repositories.tokens.abstract_tokens_repository import ( AbstractTokensRepository, ) + DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" @@ -56,6 +57,20 @@ class JsonTokensRepository(AbstractTokensRepository): raise TokenNotFound("Token not found!") + def __key_date_from_str(self, date_string: str) -> datetime: + if date_string is None or date_string == "": + return None + # we assume that we store dates in json as naive utc + utc_no_tz = datetime.fromisoformat(date_string) + utc_with_tz = utc_no_tz.replace(tzinfo=timezone.utc) + return utc_with_tz + + def __date_from_tokens_file( + self, tokens_file: object, tokenfield: str, datefield: str + ): + date_string = tokens_file[tokenfield].get(datefield) + return self.__key_date_from_str(date_string) + def get_recovery_key(self) -> Optional[RecoveryKey]: """Get the recovery key""" with ReadUserData(UserDataFiles.TOKENS) as tokens_file: @@ -68,8 +83,12 @@ class JsonTokensRepository(AbstractTokensRepository): recovery_key = RecoveryKey( key=tokens_file["recovery_token"].get("token"), - created_at=tokens_file["recovery_token"].get("date"), - expires_at=tokens_file["recovery_token"].get("expiration"), + created_at=self.__date_from_tokens_file( + tokens_file, "recovery_token", "date" + ), + expires_at=self.__date_from_tokens_file( + tokens_file, "recovery_token", "expiration" + ), uses_left=tokens_file["recovery_token"].get("uses_left"), ) diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index c72e231..a16b79d 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -2,7 +2,7 @@ Token repository using Redis as backend. """ from typing import Optional -from datetime import datetime +from datetime import datetime, timezone from selfprivacy_api.repositories.tokens.abstract_tokens_repository import ( AbstractTokensRepository, @@ -38,6 +38,8 @@ class RedisTokensRepository(AbstractTokensRepository): for key in token_keys: token = self._token_from_hash(key) if token is not None: + # token creation dates are temporarily not tz-aware + token.created_at = token.created_at.replace(tzinfo=None) tokens.append(token) return tokens @@ -150,5 +152,7 @@ class RedisTokensRepository(AbstractTokensRepository): redis = self.connection for key, value in model.dict().items(): if isinstance(value, datetime): + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) value = value.isoformat() redis.hset(redis_key, key, str(value)) diff --git a/tests/common.py b/tests/common.py index a49885a..08ddc66 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,16 +1,21 @@ import json -import datetime +from datetime import datetime, timezone, timedelta from mnemonic import Mnemonic # for expiration tests. If headache, consider freezegun -RECOVERY_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.recovery_key.datetime" -DEVICE_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.new_device_key.datetime" +RECOVERY_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.time.datetime" +DEVICE_KEY_VALIDATION_DATETIME = RECOVERY_KEY_VALIDATION_DATETIME + +FIVE_MINUTES_INTO_FUTURE_NAIVE = datetime.now() + timedelta(minutes=5) +FIVE_MINUTES_INTO_FUTURE = datetime.now(timezone.utc) + timedelta(minutes=5) +FIVE_MINUTES_INTO_PAST_NAIVE = datetime.now() - timedelta(minutes=5) +FIVE_MINUTES_INTO_PAST = datetime.now(timezone.utc) - timedelta(minutes=5) -class NearFuture(datetime.datetime): +class NearFuture(datetime): @classmethod - def now(cls): - return datetime.datetime.now() + datetime.timedelta(minutes=13) + def now(cls, tz=None): + return datetime.now(tz) + timedelta(minutes=13) def read_json(file_path): @@ -41,7 +46,6 @@ def mnemonic_to_hex(mnemonic): def assert_recovery_recent(time_generated): assert ( - datetime.datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - - datetime.timedelta(seconds=5) - < datetime.datetime.now() + datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - timedelta(seconds=5) + < datetime.now() ) diff --git a/tests/conftest.py b/tests/conftest.py index 212b6da..52ded90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,9 @@ from selfprivacy_api.models.tokens.token import Token from selfprivacy_api.repositories.tokens.json_tokens_repository import ( JsonTokensRepository, ) +from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( + RedisTokensRepository, +) from tests.common import read_json @@ -63,21 +66,26 @@ def empty_json_repo(empty_tokens): @pytest.fixture -def tokens_file(empty_json_repo, tmpdir): +def empty_redis_repo(): + repo = RedisTokensRepository() + repo.reset() + assert repo.get_tokens() == [] + return repo + + +@pytest.fixture +def tokens_file(empty_redis_repo, tmpdir): """A state with tokens""" + repo = empty_redis_repo for token in TOKENS_FILE_CONTENTS["tokens"]: - empty_json_repo._store_token( + repo._store_token( Token( token=token["token"], device_name=token["name"], created_at=token["date"], ) ) - # temporary return for compatibility with older tests - - tokenfile = tmpdir / "empty_tokens.json" - assert path.exists(tokenfile) - return tokenfile + return repo @pytest.fixture diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 9d6e671..a19eae2 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -1,7 +1,6 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument # pylint: disable=missing-function-docstring -import datetime from tests.common import ( generate_api_query, @@ -9,6 +8,11 @@ from tests.common import ( NearFuture, RECOVERY_KEY_VALIDATION_DATETIME, ) + +# Graphql API's output should be timezone-naive +from tests.common import FIVE_MINUTES_INTO_FUTURE_NAIVE as FIVE_MINUTES_INTO_FUTURE +from tests.common import FIVE_MINUTES_INTO_PAST_NAIVE as FIVE_MINUTES_INTO_PAST + from tests.test_graphql.common import ( assert_empty, assert_data, @@ -153,7 +157,7 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): def test_graphql_generate_recovery_key_with_expiration_date( client, authorized_client, tokens_file ): - expiration_date = datetime.datetime.now() + datetime.timedelta(minutes=5) + expiration_date = FIVE_MINUTES_INTO_FUTURE key = graphql_make_new_recovery_key(authorized_client, expires_at=expiration_date) status = graphql_recovery_status(authorized_client) @@ -171,7 +175,7 @@ def test_graphql_generate_recovery_key_with_expiration_date( def test_graphql_use_recovery_key_after_expiration( client, authorized_client, tokens_file, mocker ): - expiration_date = datetime.datetime.now() + datetime.timedelta(minutes=5) + expiration_date = FIVE_MINUTES_INTO_FUTURE key = graphql_make_new_recovery_key(authorized_client, expires_at=expiration_date) # Timewarp to after it expires @@ -193,7 +197,7 @@ def test_graphql_use_recovery_key_after_expiration( def test_graphql_generate_recovery_key_with_expiration_in_the_past( authorized_client, tokens_file ): - expiration_date = datetime.datetime.now() - datetime.timedelta(minutes=5) + expiration_date = FIVE_MINUTES_INTO_PAST response = request_make_new_recovery_key( authorized_client, expires_at=expiration_date ) diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index a2dbb7a..7eede6a 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -2,7 +2,7 @@ # pylint: disable=unused-argument # pylint: disable=missing-function-docstring -from datetime import datetime, timedelta +from datetime import datetime, timezone from mnemonic import Mnemonic import pytest @@ -16,9 +16,8 @@ from selfprivacy_api.repositories.tokens.exceptions import ( TokenNotFound, NewDeviceKeyNotFound, ) -from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( - RedisTokensRepository, -) + +from tests.common import FIVE_MINUTES_INTO_PAST ORIGINAL_DEVICE_NAMES = [ @@ -28,6 +27,10 @@ ORIGINAL_DEVICE_NAMES = [ "forth_token", ] +TEST_DATE = datetime(2022, 7, 15, 17, 41, 31, 675698, timezone.utc) +# tokens are not tz-aware +TOKEN_TEST_DATE = datetime(2022, 7, 15, 17, 41, 31, 675698) + def mnemonic_from_hex(hexkey): return Mnemonic(language="english").to_mnemonic(bytes.fromhex(hexkey)) @@ -40,8 +43,8 @@ def mock_new_device_key_generate(mocker): autospec=True, return_value=NewDeviceKey( key="43478d05b35e4781598acd76e33832bb", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), - expires_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TEST_DATE, + expires_at=TEST_DATE, ), ) return mock @@ -55,8 +58,8 @@ def mock_new_device_key_generate_for_mnemonic(mocker): autospec=True, return_value=NewDeviceKey( key="2237238de23dc71ab558e317bdb8ff8e", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), - expires_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TEST_DATE, + expires_at=TEST_DATE, ), ) return mock @@ -83,7 +86,7 @@ def mock_recovery_key_generate_invalid(mocker): autospec=True, return_value=RecoveryKey( key="889bf49c1d3199d71a2e704718772bd53a422020334db051", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TEST_DATE, expires_at=None, uses_left=0, ), @@ -99,7 +102,7 @@ def mock_token_generate(mocker): return_value=Token( token="ZuLNKtnxDeq6w2dpOJhbB3iat_sJLPTPl_rN5uc5MvM", device_name="IamNewDevice", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TOKEN_TEST_DATE, ), ) return mock @@ -112,7 +115,7 @@ def mock_recovery_key_generate(mocker): autospec=True, return_value=RecoveryKey( key="889bf49c1d3199d71a2e704718772bd53a422020334db051", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TEST_DATE, expires_at=None, uses_left=1, ), @@ -120,14 +123,6 @@ def mock_recovery_key_generate(mocker): return mock -@pytest.fixture -def empty_redis_repo(): - repo = RedisTokensRepository() - repo.reset() - assert repo.get_tokens() == [] - return repo - - @pytest.fixture(params=["json", "redis"]) def empty_repo(request, empty_json_repo, empty_redis_repo): if request.param == "json": @@ -224,13 +219,13 @@ def test_create_token(empty_repo, mock_token_generate): assert repo.create_token(device_name="IamNewDevice") == Token( token="ZuLNKtnxDeq6w2dpOJhbB3iat_sJLPTPl_rN5uc5MvM", device_name="IamNewDevice", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TOKEN_TEST_DATE, ) assert repo.get_tokens() == [ Token( token="ZuLNKtnxDeq6w2dpOJhbB3iat_sJLPTPl_rN5uc5MvM", device_name="IamNewDevice", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TOKEN_TEST_DATE, ) ] @@ -266,7 +261,7 @@ def test_delete_not_found_token(some_tokens_repo): input_token = Token( token="imbadtoken", device_name="primary_token", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TEST_DATE, ) with pytest.raises(TokenNotFound): assert repo.delete_token(input_token) is None @@ -295,7 +290,7 @@ def test_refresh_not_found_token(some_tokens_repo, mock_token_generate): input_token = Token( token="idontknowwhoiam", device_name="tellmewhoiam?", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TEST_DATE, ) with pytest.raises(TokenNotFound): @@ -319,7 +314,7 @@ def test_create_get_recovery_key(some_tokens_repo, mock_recovery_key_generate): assert repo.create_recovery_key(uses_left=1, expiration=None) is not None assert repo.get_recovery_key() == RecoveryKey( key="889bf49c1d3199d71a2e704718772bd53a422020334db051", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TEST_DATE, expires_at=None, uses_left=1, ) @@ -358,10 +353,13 @@ def test_use_mnemonic_expired_recovery_key( some_tokens_repo, ): repo = some_tokens_repo - expiration = datetime.now() - timedelta(minutes=5) + expiration = FIVE_MINUTES_INTO_PAST assert repo.create_recovery_key(uses_left=2, expiration=expiration) is not None recovery_key = repo.get_recovery_key() - assert recovery_key.expires_at == expiration + # TODO: do not ignore timezone once json backend is deleted + assert recovery_key.expires_at.replace(tzinfo=None) == expiration.replace( + tzinfo=None + ) assert not repo.is_recovery_key_valid() with pytest.raises(RecoveryKeyNotFound): @@ -458,8 +456,8 @@ def test_get_new_device_key(some_tokens_repo, mock_new_device_key_generate): assert repo.get_new_device_key() == NewDeviceKey( key="43478d05b35e4781598acd76e33832bb", - created_at=datetime(2022, 7, 15, 17, 41, 31, 675698), - expires_at=datetime(2022, 7, 15, 17, 41, 31, 675698), + created_at=TEST_DATE, + expires_at=TEST_DATE, ) @@ -535,7 +533,7 @@ def test_use_mnemonic_expired_new_device_key( some_tokens_repo, ): repo = some_tokens_repo - expiration = datetime.now() - timedelta(minutes=5) + expiration = FIVE_MINUTES_INTO_PAST key = repo.get_new_device_key() assert key is not None diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index ff161fb..ba54745 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -11,6 +11,8 @@ from tests.common import ( NearFuture, assert_recovery_recent, ) +from tests.common import FIVE_MINUTES_INTO_FUTURE_NAIVE as FIVE_MINUTES_INTO_FUTURE +from tests.common import FIVE_MINUTES_INTO_PAST_NAIVE as FIVE_MINUTES_INTO_PAST DATE_FORMATS = [ "%Y-%m-%dT%H:%M:%S.%fZ", @@ -110,7 +112,7 @@ def rest_recover_with_mnemonic(client, mnemonic_token, device_name): def test_get_tokens_info(authorized_client, tokens_file): - assert rest_get_tokens_info(authorized_client) == [ + assert sorted(rest_get_tokens_info(authorized_client), key=lambda x: x["name"]) == [ {"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True}, { "name": "test_token2", @@ -321,7 +323,7 @@ def test_generate_recovery_token_with_expiration_date( ): # Generate token with expiration date # Generate expiration date in the future - expiration_date = datetime.datetime.now() + datetime.timedelta(minutes=5) + expiration_date = FIVE_MINUTES_INTO_FUTURE mnemonic_token = rest_make_recovery_token( authorized_client, expires_at=expiration_date, timeformat=timeformat ) @@ -333,7 +335,7 @@ def test_generate_recovery_token_with_expiration_date( "exists": True, "valid": True, "date": time_generated, - "expiration": expiration_date.strftime("%Y-%m-%dT%H:%M:%S.%f"), + "expiration": expiration_date.isoformat(), "uses_left": None, } @@ -360,7 +362,7 @@ def test_generate_recovery_token_with_expiration_in_the_past( authorized_client, tokens_file, timeformat ): # Server must return 400 if expiration date is in the past - expiration_date = datetime.datetime.utcnow() - datetime.timedelta(minutes=5) + expiration_date = FIVE_MINUTES_INTO_PAST expiration_date_str = expiration_date.strftime(timeformat) response = authorized_client.post( "/auth/recovery_token", From 51018dd6c2e8b71071d17cffb4b78dbb3966cb18 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 11 Jan 2023 17:18:23 +0000 Subject: [PATCH 59/81] refactor(tokens-repo): cleanup actions/api_tokens.py --- selfprivacy_api/actions/api_tokens.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 2337224..520c875 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -1,4 +1,7 @@ -"""App tokens actions""" +""" +App tokens actions. +The only actions on tokens that are accessible from APIs +""" from datetime import datetime, timezone from typing import Optional from pydantic import BaseModel @@ -24,6 +27,12 @@ class TokenInfoWithIsCaller(BaseModel): date: datetime is_caller: bool +def _naive(date_time: datetime) -> datetime: + if date_time is None: + return None + if date_time.tzinfo is not None: + date_time.astimezone(timezone.utc) + return date_time.replace(tzinfo=None) def get_api_tokens_with_caller_flag(caller_token: str) -> list[TokenInfoWithIsCaller]: """Get the tokens info""" @@ -82,12 +91,7 @@ class RecoveryTokenStatus(BaseModel): uses_left: Optional[int] = None -def naive(date_time: datetime) -> datetime: - if date_time is None: - return None - if date_time.tzinfo is not None: - date_time.astimezone(timezone.utc) - return date_time.replace(tzinfo=None) + def get_api_recovery_token_status() -> RecoveryTokenStatus: @@ -99,8 +103,8 @@ def get_api_recovery_token_status() -> RecoveryTokenStatus: return RecoveryTokenStatus( exists=True, valid=is_valid, - date=naive(token.created_at), - expiration=naive(token.expires_at), + date=_naive(token.created_at), + expiration=_naive(token.expires_at), uses_left=token.uses_left, ) From baf72b730b406483477202911e9f56c0cbfe0907 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 13 Jan 2023 09:59:27 +0000 Subject: [PATCH 60/81] refactor(tokens-repo): move reset to AbstractTokensRepo --- .../tokens/abstract_tokens_repository.py | 10 ++++++++++ .../repositories/tokens/json_tokens_repository.py | 7 +++++++ .../repositories/tokens/redis_tokens_repository.py | 12 +++++------- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index 3a20ede..d9a250e 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -123,6 +123,10 @@ class AbstractTokensRepository(ABC): return False return recovery_key.is_valid() + @abstractmethod + def _delete_recovery_key(self) -> None: + """Delete the recovery key""" + def get_new_device_key(self) -> NewDeviceKey: """Creates and returns the new device key""" new_device_key = NewDeviceKey.generate() @@ -156,6 +160,12 @@ class AbstractTokensRepository(ABC): return new_token + def reset(self): + for token in self.get_tokens(): + self.delete_token(token) + self.delete_new_device_key() + self._delete_recovery_key() + @abstractmethod def _store_token(self, new_token: Token): """Store a token directly""" diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index 09204a8..4cb7b3f 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -123,6 +123,13 @@ class JsonTokensRepository(AbstractTokensRepository): if tokens["recovery_token"]["uses_left"] is not None: tokens["recovery_token"]["uses_left"] -= 1 + def _delete_recovery_key(self) -> None: + """Delete the recovery key""" + with WriteUserData(UserDataFiles.TOKENS) as tokens_file: + if "recovery_token" in tokens_file: + del tokens_file["recovery_token"] + return + def _store_new_device_key(self, new_device_key: NewDeviceKey) -> None: with WriteUserData(UserDataFiles.TOKENS) as tokens_file: tokens_file["new_device"] = { diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index a16b79d..27271b7 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -51,13 +51,6 @@ class RedisTokensRepository(AbstractTokensRepository): raise TokenNotFound redis.delete(key) - def reset(self): - for token in self.get_tokens(): - self.delete_token(token) - self.delete_new_device_key() - redis = self.connection - redis.delete(RECOVERY_KEY_REDIS_KEY) - def get_recovery_key(self) -> Optional[RecoveryKey]: """Get the recovery key""" redis = self.connection @@ -75,6 +68,11 @@ class RedisTokensRepository(AbstractTokensRepository): self._store_model_as_hash(RECOVERY_KEY_REDIS_KEY, recovery_key) return recovery_key + def _delete_recovery_key(self) -> None: + """Delete the recovery key""" + redis = self.connection + redis.delete(RECOVERY_KEY_REDIS_KEY) + def _store_new_device_key(self, new_device_key: NewDeviceKey) -> None: """Store new device key directly""" self._store_model_as_hash(NEW_DEVICE_KEY_REDIS_KEY, new_device_key) From 817f414dd95995b339070850101d395d3a761819 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 13 Jan 2023 10:24:17 +0000 Subject: [PATCH 61/81] refactor(tokens-repo): break out storing recovery keys --- .../repositories/tokens/abstract_tokens_repository.py | 4 ++++ .../repositories/tokens/json_tokens_repository.py | 7 +++++-- .../repositories/tokens/redis_tokens_repository.py | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index d9a250e..764f3b6 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -123,6 +123,10 @@ class AbstractTokensRepository(ABC): return False return recovery_key.is_valid() + @abstractmethod + def _store_recovery_key(self, recovery_key: RecoveryKey) -> None: + """Store recovery key directly""" + @abstractmethod def _delete_recovery_key(self) -> None: """Delete the recovery key""" diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index 4cb7b3f..332bef8 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -103,6 +103,11 @@ class JsonTokensRepository(AbstractTokensRepository): recovery_key = RecoveryKey.generate(expiration, uses_left) + self._store_recovery_key(recovery_key) + + return recovery_key + + def _store_recovery_key(self, recovery_key: RecoveryKey) -> None: with WriteUserData(UserDataFiles.TOKENS) as tokens_file: key_expiration: Optional[str] = None if recovery_key.expires_at is not None: @@ -114,8 +119,6 @@ class JsonTokensRepository(AbstractTokensRepository): "uses_left": recovery_key.uses_left, } - return recovery_key - def _decrement_recovery_token(self): """Decrement recovery key use count by one""" if self.is_recovery_key_valid(): diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index 27271b7..0b3c19b 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -65,9 +65,12 @@ class RedisTokensRepository(AbstractTokensRepository): ) -> RecoveryKey: """Create the recovery key""" recovery_key = RecoveryKey.generate(expiration=expiration, uses_left=uses_left) - self._store_model_as_hash(RECOVERY_KEY_REDIS_KEY, recovery_key) + self._store_recovery_key(recovery_key) return recovery_key + def _store_recovery_key(self, recovery_key: RecoveryKey) -> None: + self._store_model_as_hash(RECOVERY_KEY_REDIS_KEY, recovery_key) + def _delete_recovery_key(self) -> None: """Delete the recovery key""" redis = self.connection From da19cc8c0ed881b83592ad588d908c70350e75d4 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 13 Jan 2023 10:30:04 +0000 Subject: [PATCH 62/81] refactor(tokens-repo): move create recovery key to abstract token repository --- .../tokens/abstract_tokens_repository.py | 4 +++- .../repositories/tokens/json_tokens_repository.py | 13 ------------- .../repositories/tokens/redis_tokens_repository.py | 10 ---------- 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index 764f3b6..e5daa4d 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -86,13 +86,15 @@ class AbstractTokensRepository(ABC): def get_recovery_key(self) -> Optional[RecoveryKey]: """Get the recovery key""" - @abstractmethod def create_recovery_key( self, expiration: Optional[datetime], uses_left: Optional[int], ) -> RecoveryKey: """Create the recovery key""" + recovery_key = RecoveryKey.generate(expiration, uses_left) + self._store_recovery_key(recovery_key) + return recovery_key def use_mnemonic_recovery_key( self, mnemonic_phrase: str, device_name: str diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index 332bef8..0f70a55 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -94,19 +94,6 @@ class JsonTokensRepository(AbstractTokensRepository): return recovery_key - def create_recovery_key( - self, - expiration: Optional[datetime], - uses_left: Optional[int], - ) -> RecoveryKey: - """Create the recovery key""" - - recovery_key = RecoveryKey.generate(expiration, uses_left) - - self._store_recovery_key(recovery_key) - - return recovery_key - def _store_recovery_key(self, recovery_key: RecoveryKey) -> None: with WriteUserData(UserDataFiles.TOKENS) as tokens_file: key_expiration: Optional[str] = None diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index 0b3c19b..8e8dfe5 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -58,16 +58,6 @@ class RedisTokensRepository(AbstractTokensRepository): return self._recovery_key_from_hash(RECOVERY_KEY_REDIS_KEY) return None - def create_recovery_key( - self, - expiration: Optional[datetime], - uses_left: Optional[int], - ) -> RecoveryKey: - """Create the recovery key""" - recovery_key = RecoveryKey.generate(expiration=expiration, uses_left=uses_left) - self._store_recovery_key(recovery_key) - return recovery_key - def _store_recovery_key(self, recovery_key: RecoveryKey) -> None: self._store_model_as_hash(RECOVERY_KEY_REDIS_KEY, recovery_key) From d0a17d7b7a70c74aee1f9e4764d2c701a3a987ca Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 13 Jan 2023 11:37:41 +0000 Subject: [PATCH 63/81] fix(tokens-repo): make json _get_stored_new_device_key return tz-aware keys --- .../repositories/tokens/json_tokens_repository.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/json_tokens_repository.py b/selfprivacy_api/repositories/tokens/json_tokens_repository.py index 0f70a55..be753ea 100644 --- a/selfprivacy_api/repositories/tokens/json_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/json_tokens_repository.py @@ -143,7 +143,11 @@ class JsonTokensRepository(AbstractTokensRepository): new_device_key = NewDeviceKey( key=tokens_file["new_device"]["token"], - created_at=tokens_file["new_device"]["date"], - expires_at=tokens_file["new_device"]["expiration"], + created_at=self.__date_from_tokens_file( + tokens_file, "new_device", "date" + ), + expires_at=self.__date_from_tokens_file( + tokens_file, "new_device", "expiration" + ), ) return new_device_key From 5fbfaa73ea91fb179c8ba87fee65635d90e7819a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 13 Jan 2023 11:41:17 +0000 Subject: [PATCH 64/81] feat(tokens-repo): add clone() method --- .../tokens/abstract_tokens_repository.py | 16 +++++++ .../test_repository/test_tokens_repository.py | 45 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py index e5daa4d..d81bd65 100644 --- a/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/abstract_tokens_repository.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from abc import ABC, abstractmethod from datetime import datetime from typing import Optional @@ -172,6 +174,20 @@ class AbstractTokensRepository(ABC): self.delete_new_device_key() self._delete_recovery_key() + def clone(self, source: AbstractTokensRepository) -> None: + """Clone the state of another repository to this one""" + self.reset() + for token in source.get_tokens(): + self._store_token(token) + + recovery_key = source.get_recovery_key() + if recovery_key is not None: + self._store_recovery_key(recovery_key) + + new_device_key = source._get_stored_new_device_key() + if new_device_key is not None: + self._store_new_device_key(new_device_key) + @abstractmethod def _store_token(self, new_token: Token): """Store a token directly""" diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 7eede6a..360bfa5 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -17,7 +17,17 @@ from selfprivacy_api.repositories.tokens.exceptions import ( NewDeviceKeyNotFound, ) -from tests.common import FIVE_MINUTES_INTO_PAST +from selfprivacy_api.repositories.tokens.json_tokens_repository import ( + JsonTokensRepository, +) +from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( + RedisTokensRepository, +) +from selfprivacy_api.repositories.tokens.abstract_tokens_repository import ( + AbstractTokensRepository, +) + +from tests.common import FIVE_MINUTES_INTO_PAST, FIVE_MINUTES_INTO_FUTURE ORIGINAL_DEVICE_NAMES = [ @@ -560,3 +570,36 @@ def test_use_mnemonic_new_device_key_when_empty(empty_repo): ) is None ) + + +def assert_identical( + repo_a: AbstractTokensRepository, repo_b: AbstractTokensRepository +): + tokens_a = repo_a.get_tokens() + tokens_b = repo_b.get_tokens() + assert len(tokens_a) == len(tokens_b) + for token in tokens_a: + assert token in tokens_b + assert repo_a.get_recovery_key() == repo_b.get_recovery_key() + assert repo_a._get_stored_new_device_key() == repo_b._get_stored_new_device_key() + + +def clone_to_redis(repo: JsonTokensRepository): + other_repo = RedisTokensRepository() + other_repo.clone(repo) + assert_identical(repo, other_repo) + + +# we cannot easily parametrize this unfortunately, since some_tokens and empty_repo cannot coexist +def test_clone_json_to_redis_empty(empty_repo): + repo = empty_repo + if isinstance(repo, JsonTokensRepository): + clone_to_redis(repo) + + +def test_clone_json_to_redis_full(some_tokens_repo): + repo = some_tokens_repo + if isinstance(repo, JsonTokensRepository): + repo.get_new_device_key() + repo.create_recovery_key(FIVE_MINUTES_INTO_FUTURE, 2) + clone_to_redis(repo) From 3344ab7c5dbacbc4647b5631fc9e3d208dc79d1a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 13 Jan 2023 12:13:20 +0000 Subject: [PATCH 65/81] feat(tokens-repo): add migration of tokens to redis --- selfprivacy_api/migrations/__init__.py | 2 + selfprivacy_api/migrations/redis_tokens.py | 48 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 selfprivacy_api/migrations/redis_tokens.py diff --git a/selfprivacy_api/migrations/__init__.py b/selfprivacy_api/migrations/__init__.py index adb7d24..222c95e 100644 --- a/selfprivacy_api/migrations/__init__.py +++ b/selfprivacy_api/migrations/__init__.py @@ -22,6 +22,7 @@ from selfprivacy_api.migrations.providers import CreateProviderFields from selfprivacy_api.migrations.prepare_for_nixos_2211 import ( MigrateToSelfprivacyChannelFrom2205, ) +from selfprivacy_api.migrations.redis_tokens import LoadTokensToRedis migrations = [ FixNixosConfigBranch(), @@ -31,6 +32,7 @@ migrations = [ CheckForFailedBindsMigration(), CreateProviderFields(), MigrateToSelfprivacyChannelFrom2205(), + LoadTokensToRedis(), ] diff --git a/selfprivacy_api/migrations/redis_tokens.py b/selfprivacy_api/migrations/redis_tokens.py new file mode 100644 index 0000000..c5eea2f --- /dev/null +++ b/selfprivacy_api/migrations/redis_tokens.py @@ -0,0 +1,48 @@ +from selfprivacy_api.migrations.migration import Migration + +from selfprivacy_api.repositories.tokens.json_tokens_repository import ( + JsonTokensRepository, +) +from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( + RedisTokensRepository, +) +from selfprivacy_api.repositories.tokens.abstract_tokens_repository import ( + AbstractTokensRepository, +) + + +class LoadTokensToRedis(Migration): + """Load Json tokens into Redis""" + + def get_migration_name(self): + return "load_tokens_to_redis" + + def get_migration_description(self): + return "Loads access tokens and recovery keys from legacy json file into redis token storage" + + def is_repo_empty(self, repo: AbstractTokensRepository) -> bool: + if repo.get_tokens() != []: + return False + if repo.get_recovery_key() is not None: + return False + return True + + def is_migration_needed(self): + try: + if not self.is_repo_empty(JsonTokensRepository()) and self.is_repo_empty( + RedisTokensRepository() + ): + return True + except Exception as e: + print(e) + return False + + def migrate(self): + # Write info about providers to userdata.json + try: + RedisTokensRepository().clone(JsonTokensRepository()) + + print("Done") + except Exception as e: + print(e) + print("Error migrating access tokens from json to redis") From c77191864e0f018320e09100244b96a742319c33 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 14 Jun 2023 14:01:15 +0300 Subject: [PATCH 66/81] style: reformat --- selfprivacy_api/actions/api_tokens.py | 5 ++--- selfprivacy_api/models/tokens/time.py | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 520c875..37b7631 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -27,6 +27,7 @@ class TokenInfoWithIsCaller(BaseModel): date: datetime is_caller: bool + def _naive(date_time: datetime) -> datetime: if date_time is None: return None @@ -34,6 +35,7 @@ def _naive(date_time: datetime) -> datetime: date_time.astimezone(timezone.utc) return date_time.replace(tzinfo=None) + def get_api_tokens_with_caller_flag(caller_token: str) -> list[TokenInfoWithIsCaller]: """Get the tokens info""" caller_name = TOKEN_REPO.get_token_by_token_string(caller_token).device_name @@ -91,9 +93,6 @@ class RecoveryTokenStatus(BaseModel): uses_left: Optional[int] = None - - - def get_api_recovery_token_status() -> RecoveryTokenStatus: """Get the recovery token status""" token = TOKEN_REPO.get_recovery_key() diff --git a/selfprivacy_api/models/tokens/time.py b/selfprivacy_api/models/tokens/time.py index 35fd992..967fcfb 100644 --- a/selfprivacy_api/models/tokens/time.py +++ b/selfprivacy_api/models/tokens/time.py @@ -1,13 +1,14 @@ from datetime import datetime, timezone + def is_past(dt: datetime) -> bool: # we cannot compare a naive now() # to dt which might be tz-aware or unaware dt = ensure_timezone(dt) return dt < datetime.now(timezone.utc) -def ensure_timezone(dt:datetime) -> datetime: + +def ensure_timezone(dt: datetime) -> datetime: if dt.tzinfo is None or dt.tzinfo.utcoffset(None) is None: - dt = dt.replace(tzinfo= timezone.utc) - return dt - + dt = dt.replace(tzinfo=timezone.utc) + return dt From 5be3c83952a748b87c4be6a3f33c47ed0fe82057 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 21 Jun 2023 12:15:33 +0000 Subject: [PATCH 67/81] fix(tokens-repo): persistent hashing --- .../tokens/redis_tokens_repository.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index 8e8dfe5..ccd63be 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -3,6 +3,7 @@ Token repository using Redis as backend. """ from typing import Optional from datetime import datetime, timezone +from hashlib import md5 from selfprivacy_api.repositories.tokens.abstract_tokens_repository import ( AbstractTokensRepository, @@ -28,7 +29,10 @@ class RedisTokensRepository(AbstractTokensRepository): @staticmethod def token_key_for_device(device_name: str): - return TOKENS_PREFIX + str(hash(device_name)) + hash = md5() + hash.update(bytes(device_name, "utf-8")) + digest = hash.hexdigest() + return TOKENS_PREFIX + digest def get_tokens(self) -> list[Token]: """Get the tokens""" @@ -38,16 +42,23 @@ class RedisTokensRepository(AbstractTokensRepository): for key in token_keys: token = self._token_from_hash(key) if token is not None: - # token creation dates are temporarily not tz-aware - token.created_at = token.created_at.replace(tzinfo=None) tokens.append(token) return tokens + def _discover_token_key(self, input_token: Token) -> str: + """brute-force searching for tokens, for robust deletion""" + redis = self.connection + token_keys = redis.keys(TOKENS_PREFIX + "*") + for key in token_keys: + token = self._token_from_hash(key) + if token == input_token: + return key + def delete_token(self, input_token: Token) -> None: """Delete the token""" redis = self.connection - key = RedisTokensRepository._token_redis_key(input_token) - if input_token not in self.get_tokens(): + key = self._discover_token_key(input_token) + if key is None: raise TokenNotFound redis.delete(key) @@ -131,7 +142,10 @@ class RedisTokensRepository(AbstractTokensRepository): return None def _token_from_hash(self, redis_key: str) -> Optional[Token]: - return self._hash_as_model(redis_key, Token) + token = self._hash_as_model(redis_key, Token) + if token is not None: + token.created_at = token.created_at.replace(tzinfo=None) + return token def _recovery_key_from_hash(self, redis_key: str) -> Optional[RecoveryKey]: return self._hash_as_model(redis_key, RecoveryKey) From b7cd703eaafa9e01a3a1716f2a4f3dd0c61da2ad Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 25 Oct 2023 14:53:11 +0000 Subject: [PATCH 68/81] fix(tokens): missing timezone import --- selfprivacy_api/repositories/tokens/redis_tokens_repository.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py index 944c9b9..834794c 100644 --- a/selfprivacy_api/repositories/tokens/redis_tokens_repository.py +++ b/selfprivacy_api/repositories/tokens/redis_tokens_repository.py @@ -4,6 +4,7 @@ Token repository using Redis as backend. from typing import Any, Optional from datetime import datetime from hashlib import md5 +from datetime import timezone from selfprivacy_api.repositories.tokens.abstract_tokens_repository import ( AbstractTokensRepository, @@ -53,6 +54,7 @@ class RedisTokensRepository(AbstractTokensRepository): token = self._token_from_hash(key) if token == input_token: return key + return None def delete_token(self, input_token: Token) -> None: """Delete the token""" @@ -148,6 +150,7 @@ class RedisTokensRepository(AbstractTokensRepository): if token is not None: token.created_at = token.created_at.replace(tzinfo=None) return token + return None def _recovery_key_from_hash(self, redis_key: str) -> Optional[RecoveryKey]: return self._hash_as_model(redis_key, RecoveryKey) From 3deaeb28c59a68aa186bfdece5f466068cf87be5 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 1 Nov 2023 15:29:21 +0000 Subject: [PATCH 69/81] test(auth): fix assert_ok's wrt nested structure --- tests/test_graphql/api_common.py | 89 +++++++++++++++++++++++++ tests/test_graphql/test_api_devices.py | 8 +-- tests/test_graphql/test_api_recovery.py | 16 ++--- 3 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 tests/test_graphql/api_common.py diff --git a/tests/test_graphql/api_common.py b/tests/test_graphql/api_common.py new file mode 100644 index 0000000..bfac767 --- /dev/null +++ b/tests/test_graphql/api_common.py @@ -0,0 +1,89 @@ +from tests.common import generate_api_query +from tests.conftest import TOKENS_FILE_CONTENTS, DEVICE_WE_AUTH_TESTS_WITH + +ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] + + +def assert_ok(response, request): + data = assert_data(response) + data[request]["success"] is True + data[request]["message"] is not None + data[request]["code"] == 200 + + +def assert_errorcode(response, request, code): + data = assert_data(response) + data[request]["success"] is False + data[request]["message"] is not None + data[request]["code"] == code + + +def assert_empty(response): + assert response.status_code == 200 + assert response.json().get("data") is None + + +def assert_data(response): + assert response.status_code == 200 + data = response.json().get("data") + assert data is not None + assert "api" in data.keys() + return data["api"] + + +API_DEVICES_QUERY = """ +devices { + creationDate + isCaller + name +} +""" + + +def request_devices(client): + return client.post( + "/graphql", + json={"query": generate_api_query([API_DEVICES_QUERY])}, + ) + + +def graphql_get_devices(client): + response = request_devices(client) + data = assert_data(response) + devices = data["devices"] + assert devices is not None + return devices + + +def set_client_token(client, token): + client.headers.update({"Authorization": "Bearer " + token}) + + +def assert_token_valid(client, token): + set_client_token(client, token) + assert graphql_get_devices(client) is not None + + +def assert_same(graphql_devices, abstract_devices): + """Orderless comparison""" + assert len(graphql_devices) == len(abstract_devices) + for original_device in abstract_devices: + assert original_device["name"] in [device["name"] for device in graphql_devices] + for device in graphql_devices: + if device["name"] == original_device["name"]: + assert device["creationDate"] == original_device["date"].isoformat() + + +def assert_original(client): + devices = graphql_get_devices(client) + assert_original_devices(devices) + + +def assert_original_devices(devices): + assert_same(devices, ORIGINAL_DEVICES) + + for device in devices: + if device["name"] == DEVICE_WE_AUTH_TESTS_WITH["name"]: + assert device["isCaller"] is True + else: + assert device["isCaller"] is False diff --git a/tests/test_graphql/test_api_devices.py b/tests/test_graphql/test_api_devices.py index 599fe24..b24bc7f 100644 --- a/tests/test_graphql/test_api_devices.py +++ b/tests/test_graphql/test_api_devices.py @@ -8,7 +8,7 @@ from tests.common import ( generate_api_query, ) from tests.conftest import DEVICE_WE_AUTH_TESTS_WITH, TOKENS_FILE_CONTENTS -from tests.test_graphql.common import ( +from tests.test_graphql.api_common import ( assert_data, assert_empty, assert_ok, @@ -38,7 +38,7 @@ def graphql_get_new_device_key(authorized_client) -> str: ) assert_ok(response, "getNewDeviceApiKey") - key = response.json()["data"]["getNewDeviceApiKey"]["key"] + key = response.json()["data"]["api"]["getNewDeviceApiKey"]["key"] assert key.split(" ").__len__() == 12 return key @@ -61,7 +61,7 @@ def graphql_try_auth_new_device(client, mnemonic_key, device_name): def graphql_authorize_new_device(client, mnemonic_key, device_name) -> str: response = graphql_try_auth_new_device(client, mnemonic_key, "new_device") assert_ok(response, "authorizeWithNewDeviceApiKey") - token = response.json()["data"]["authorizeWithNewDeviceApiKey"]["token"] + token = response.json()["data"]["api"]["authorizeWithNewDeviceApiKey"]["token"] assert_token_valid(client, token) @@ -182,7 +182,7 @@ def test_graphql_refresh_token(authorized_client, client, tokens_file): ) assert_ok(response, "refreshDeviceApiToken") - new_token = response.json()["data"]["refreshDeviceApiToken"]["token"] + new_token = response.json()["data"]["api"]["refreshDeviceApiToken"]["token"] assert_token_valid(client, new_token) set_client_token(client, new_token) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index ec5f094..e847b16 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -13,7 +13,7 @@ from tests.common import ( from tests.common import FIVE_MINUTES_INTO_FUTURE_NAIVE as FIVE_MINUTES_INTO_FUTURE from tests.common import FIVE_MINUTES_INTO_PAST_NAIVE as FIVE_MINUTES_INTO_PAST -from tests.test_graphql.common import ( +from tests.test_graphql.api_common import ( assert_empty, assert_data, assert_ok, @@ -46,7 +46,7 @@ def graphql_recovery_status(client): response = request_recovery_status(client) data = assert_data(response) - status = data["api"]["recoveryKey"] + status = data["recoveryKey"] assert status is not None return status @@ -70,7 +70,7 @@ def request_make_new_recovery_key(client, expires_at=None, uses=None): def graphql_make_new_recovery_key(client, expires_at=None, uses=None): response = request_make_new_recovery_key(client, expires_at, uses) assert_ok(response, "getNewRecoveryApiKey") - key = response.json()["data"]["getNewRecoveryApiKey"]["key"] + key = response.json()["data"]["api"]["getNewRecoveryApiKey"]["key"] assert key is not None assert key.split(" ").__len__() == 18 return key @@ -94,7 +94,7 @@ def request_recovery_auth(client, key, device_name): def graphql_use_recovery_key(client, key, device_name): response = request_recovery_auth(client, key, device_name) assert_ok(response, "useRecoveryApiKey") - token = response.json()["data"]["useRecoveryApiKey"]["token"] + token = response.json()["data"]["api"]["useRecoveryApiKey"]["token"] assert token is not None assert_token_valid(client, token) set_client_token(client, token) @@ -187,7 +187,7 @@ def test_graphql_use_recovery_key_after_expiration( response = request_recovery_auth(client, key, "new_test_token3") assert_errorcode(response, "useRecoveryApiKey", 404) - assert response.json()["data"]["useRecoveryApiKey"]["token"] is None + assert response.json()["data"]["api"]["useRecoveryApiKey"]["token"] is None assert_original(authorized_client) status = graphql_recovery_status(authorized_client) @@ -207,7 +207,7 @@ def test_graphql_generate_recovery_key_with_expiration_in_the_past( ) assert_errorcode(response, "getNewRecoveryApiKey", 400) - assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None + assert response.json()["data"]["api"]["getNewRecoveryApiKey"]["key"] is None assert graphql_recovery_status(authorized_client)["exists"] is False @@ -273,12 +273,12 @@ def test_graphql_generate_recovery_key_with_negative_uses( response = request_make_new_recovery_key(authorized_client, uses=-1) assert_errorcode(response, "getNewRecoveryApiKey", 400) - assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None + assert response.json()["data"]["api"]["getNewRecoveryApiKey"]["key"] is None def test_graphql_generate_recovery_key_with_zero_uses(authorized_client, tokens_file): response = request_make_new_recovery_key(authorized_client, uses=0) assert_errorcode(response, "getNewRecoveryApiKey", 400) - assert response.json()["data"]["getNewRecoveryApiKey"]["key"] is None + assert response.json()["data"]["api"]["getNewRecoveryApiKey"]["key"] is None assert graphql_recovery_status(authorized_client)["exists"] is False From a66ee2d3e565bbf815facda52a01d73d5504ae9b Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Wed, 1 Nov 2023 16:46:36 +0000 Subject: [PATCH 70/81] test(auth): fix future expiring too fast --- tests/common.py | 19 +++++++++++++++---- tests/test_graphql/test_api_recovery.py | 10 +++++----- .../test_repository/test_tokens_repository.py | 8 ++++---- tests/test_rest_endpoints/test_auth.py | 10 ++++++---- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/tests/common.py b/tests/common.py index df95474..97d0d7a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -6,10 +6,21 @@ from mnemonic import Mnemonic RECOVERY_KEY_VALIDATION_DATETIME = "selfprivacy_api.models.tokens.time.datetime" DEVICE_KEY_VALIDATION_DATETIME = RECOVERY_KEY_VALIDATION_DATETIME -FIVE_MINUTES_INTO_FUTURE_NAIVE = datetime.now() + timedelta(minutes=5) -FIVE_MINUTES_INTO_FUTURE = datetime.now(timezone.utc) + timedelta(minutes=5) -FIVE_MINUTES_INTO_PAST_NAIVE = datetime.now() - timedelta(minutes=5) -FIVE_MINUTES_INTO_PAST = datetime.now(timezone.utc) - timedelta(minutes=5) + +def five_minutes_into_future_naive(): + return datetime.now() + timedelta(minutes=5) + + +def five_minutes_into_future(): + return datetime.now(timezone.utc) + timedelta(minutes=5) + + +def five_minutes_into_past_naive(): + return datetime.now() - timedelta(minutes=5) + + +def five_minutes_into_past(): + return datetime.now(timezone.utc) - timedelta(minutes=5) class NearFuture(datetime): diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index e847b16..19f8a3d 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -10,8 +10,8 @@ from tests.common import ( ) # Graphql API's output should be timezone-naive -from tests.common import FIVE_MINUTES_INTO_FUTURE_NAIVE as FIVE_MINUTES_INTO_FUTURE -from tests.common import FIVE_MINUTES_INTO_PAST_NAIVE as FIVE_MINUTES_INTO_PAST +from tests.common import five_minutes_into_future_naive as five_minutes_into_future +from tests.common import five_minutes_into_past_naive as five_minutes_into_past from tests.test_graphql.api_common import ( assert_empty, @@ -161,7 +161,7 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): def test_graphql_generate_recovery_key_with_expiration_date( client, authorized_client, tokens_file ): - expiration_date = FIVE_MINUTES_INTO_FUTURE + expiration_date = five_minutes_into_future() key = graphql_make_new_recovery_key(authorized_client, expires_at=expiration_date) status = graphql_recovery_status(authorized_client) @@ -179,7 +179,7 @@ def test_graphql_generate_recovery_key_with_expiration_date( def test_graphql_use_recovery_key_after_expiration( client, authorized_client, tokens_file, mocker ): - expiration_date = FIVE_MINUTES_INTO_FUTURE + expiration_date = five_minutes_into_future() key = graphql_make_new_recovery_key(authorized_client, expires_at=expiration_date) # Timewarp to after it expires @@ -201,7 +201,7 @@ def test_graphql_use_recovery_key_after_expiration( def test_graphql_generate_recovery_key_with_expiration_in_the_past( authorized_client, tokens_file ): - expiration_date = FIVE_MINUTES_INTO_PAST + expiration_date = five_minutes_into_past() response = request_make_new_recovery_key( authorized_client, expires_at=expiration_date ) diff --git a/tests/test_graphql/test_repository/test_tokens_repository.py b/tests/test_graphql/test_repository/test_tokens_repository.py index 360bfa5..eb5e7cb 100644 --- a/tests/test_graphql/test_repository/test_tokens_repository.py +++ b/tests/test_graphql/test_repository/test_tokens_repository.py @@ -27,7 +27,7 @@ from selfprivacy_api.repositories.tokens.abstract_tokens_repository import ( AbstractTokensRepository, ) -from tests.common import FIVE_MINUTES_INTO_PAST, FIVE_MINUTES_INTO_FUTURE +from tests.common import five_minutes_into_past, five_minutes_into_future ORIGINAL_DEVICE_NAMES = [ @@ -363,7 +363,7 @@ def test_use_mnemonic_expired_recovery_key( some_tokens_repo, ): repo = some_tokens_repo - expiration = FIVE_MINUTES_INTO_PAST + expiration = five_minutes_into_past() assert repo.create_recovery_key(uses_left=2, expiration=expiration) is not None recovery_key = repo.get_recovery_key() # TODO: do not ignore timezone once json backend is deleted @@ -543,7 +543,7 @@ def test_use_mnemonic_expired_new_device_key( some_tokens_repo, ): repo = some_tokens_repo - expiration = FIVE_MINUTES_INTO_PAST + expiration = five_minutes_into_past() key = repo.get_new_device_key() assert key is not None @@ -601,5 +601,5 @@ def test_clone_json_to_redis_full(some_tokens_repo): repo = some_tokens_repo if isinstance(repo, JsonTokensRepository): repo.get_new_device_key() - repo.create_recovery_key(FIVE_MINUTES_INTO_FUTURE, 2) + repo.create_recovery_key(five_minutes_into_future(), 2) clone_to_redis(repo) diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index ba54745..d62fa18 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -11,8 +11,8 @@ from tests.common import ( NearFuture, assert_recovery_recent, ) -from tests.common import FIVE_MINUTES_INTO_FUTURE_NAIVE as FIVE_MINUTES_INTO_FUTURE -from tests.common import FIVE_MINUTES_INTO_PAST_NAIVE as FIVE_MINUTES_INTO_PAST +from tests.common import five_minutes_into_future_naive as five_minutes_into_future +from tests.common import five_minutes_into_past_naive as five_minutes_into_past DATE_FORMATS = [ "%Y-%m-%dT%H:%M:%S.%fZ", @@ -76,6 +76,8 @@ def rest_make_recovery_token(client, expires_at=None, timeformat=None, uses=None json=json, ) + if not response.status_code == 200: + raise ValueError(response.reason, response.text, response.json()["detail"]) assert response.status_code == 200 assert "token" in response.json() return response.json()["token"] @@ -323,7 +325,7 @@ def test_generate_recovery_token_with_expiration_date( ): # Generate token with expiration date # Generate expiration date in the future - expiration_date = FIVE_MINUTES_INTO_FUTURE + expiration_date = five_minutes_into_future() mnemonic_token = rest_make_recovery_token( authorized_client, expires_at=expiration_date, timeformat=timeformat ) @@ -362,7 +364,7 @@ def test_generate_recovery_token_with_expiration_in_the_past( authorized_client, tokens_file, timeformat ): # Server must return 400 if expiration date is in the past - expiration_date = FIVE_MINUTES_INTO_PAST + expiration_date = five_minutes_into_past() expiration_date_str = expiration_date.strftime(timeformat) response = authorized_client.post( "/auth/recovery_token", From 8caf7e1b24124cfcdac80d21b42b718ac9b5fe17 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Tue, 7 Nov 2023 01:00:38 +0000 Subject: [PATCH 71/81] fix(backups): do not infinitely retry automatic backup if it errors out --- selfprivacy_api/backup/__init__.py | 38 +++++++++++++++++++++++++----- selfprivacy_api/backup/jobs.py | 10 ++++++++ tests/test_graphql/test_backup.py | 32 +++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/selfprivacy_api/backup/__init__.py b/selfprivacy_api/backup/__init__.py index aa11f7f..a5fe066 100644 --- a/selfprivacy_api/backup/__init__.py +++ b/selfprivacy_api/backup/__init__.py @@ -1,7 +1,8 @@ """ This module contains the controller class for backups. """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone +import time import os from os import statvfs from typing import Callable, List, Optional @@ -37,6 +38,7 @@ from selfprivacy_api.backup.providers import get_provider from selfprivacy_api.backup.storage import Storage from selfprivacy_api.backup.jobs import ( get_backup_job, + get_backup_fail, add_backup_job, get_restore_job, add_restore_job, @@ -292,9 +294,9 @@ class Backups: def back_up( service: Service, reason: BackupReason = BackupReason.EXPLICIT ) -> Snapshot: - """The top-level function to back up a service""" - folders = service.get_folders() - service_name = service.get_id() + """The top-level function to back up a service + If it fails for any reason at all, it should both mark job as + errored and re-raise an error""" job = get_backup_job(service) if job is None: @@ -302,6 +304,10 @@ class Backups: Jobs.update(job, status=JobStatus.RUNNING) try: + if service.can_be_backed_up() is False: + raise ValueError("cannot backup a non-backuppable service") + folders = service.get_folders() + service_name = service.get_id() service.pre_backup() snapshot = Backups.provider().backupper.start_backup( folders, @@ -692,23 +698,43 @@ class Backups: """Get a timezone-aware time of the last backup of a service""" return Storage.get_last_backup_time(service.get_id()) + @staticmethod + def get_last_backup_error_time(service: Service) -> Optional[datetime]: + """Get a timezone-aware time of the last backup of a service""" + job = get_backup_fail(service) + if job is not None: + datetime_created = job.created_at + if datetime_created.tzinfo is None: + # assume it is in localtime + offset = timedelta(seconds=time.localtime().tm_gmtoff) + datetime_created = datetime_created - offset + return datetime.combine(datetime_created.date(), datetime_created.time(),timezone.utc) + return datetime_created + return None + @staticmethod def is_time_to_backup_service(service: Service, time: datetime): """Returns True if it is time to back up a service""" period = Backups.autobackup_period_minutes() - service_id = service.get_id() if not service.can_be_backed_up(): return False if period is None: return False - last_backup = Storage.get_last_backup_time(service_id) + last_error = Backups.get_last_backup_error_time(service) + + if last_error is not None: + if time < last_error + timedelta(seconds=AUTOBACKUP_JOB_EXPIRATION_SECONDS): + return False + + last_backup = Backups.get_last_backed_up(service) if last_backup is None: # queue a backup immediately if there are no previous backups return True if time > last_backup + timedelta(minutes=period): return True + return False # Helpers diff --git a/selfprivacy_api/backup/jobs.py b/selfprivacy_api/backup/jobs.py index ab4eaca..0aacd86 100644 --- a/selfprivacy_api/backup/jobs.py +++ b/selfprivacy_api/backup/jobs.py @@ -80,9 +80,19 @@ def get_job_by_type(type_id: str) -> Optional[Job]: return job +def get_failed_job_by_type(type_id: str) -> Optional[Job]: + for job in Jobs.get_jobs(): + if job.type_id == type_id and job.status == JobStatus.ERROR: + return job + + def get_backup_job(service: Service) -> Optional[Job]: return get_job_by_type(backup_job_type(service)) +def get_backup_fail(service: Service) -> Optional[Job]: + return get_failed_job_by_type(backup_job_type(service)) + + def get_restore_job(service: Service) -> Optional[Job]: return get_job_by_type(restore_job_type(service)) diff --git a/tests/test_graphql/test_backup.py b/tests/test_graphql/test_backup.py index 1903fba..27a2879 100644 --- a/tests/test_graphql/test_backup.py +++ b/tests/test_graphql/test_backup.py @@ -14,6 +14,8 @@ import secrets import tempfile +from selfprivacy_api.utils.huey import huey + import selfprivacy_api.services as services from selfprivacy_api.services import Service, get_all_services from selfprivacy_api.services.service import ServiceStatus @@ -119,6 +121,10 @@ def dummy_service(tmpdir, backups, raw_dummy_service) -> Service: # register our service services.services.append(service) + # make sure we are in immediate mode because this thing is non pickleable to store on queue. + huey.immediate = True + assert huey.immediate is True + assert get_service_by_id(service.get_id()) is not None yield service @@ -996,6 +1002,32 @@ def test_autobackup_timing(backups, dummy_service): assert Backups.is_time_to_backup_service(dummy_service, future) +def test_backup_unbackuppable(backups, dummy_service): + dummy_service.set_backuppable(False) + assert dummy_service.can_be_backed_up() is False + with pytest.raises(ValueError): + Backups.back_up(dummy_service) + + +def test_failed_autoback_prevents_more_autobackup(backups, dummy_service): + backup_period = 13 # minutes + now = datetime.now(timezone.utc) + + Backups.set_autobackup_period_minutes(backup_period) + assert Backups.is_time_to_backup_service(dummy_service, now) + + # artificially making an errored out backup job + dummy_service.set_backuppable(False) + with pytest.raises(ValueError): + Backups.back_up(dummy_service) + dummy_service.set_backuppable(True) + + assert Backups.get_last_backed_up(dummy_service) is None + assert Backups.get_last_backup_error_time(dummy_service) is not None + + assert Backups.is_time_to_backup_service(dummy_service, now) is False + + # Storage def test_snapshots_caching(backups, dummy_service): Backups.back_up(dummy_service) From b545a400c3657848bb570aed96d047514dc8c133 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 10 Nov 2023 11:47:48 +0000 Subject: [PATCH 72/81] doc(jobs): document that we are tz-naive when storing jobs --- selfprivacy_api/jobs/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfprivacy_api/jobs/__init__.py b/selfprivacy_api/jobs/__init__.py index 05b5ab8..7310016 100644 --- a/selfprivacy_api/jobs/__init__.py +++ b/selfprivacy_api/jobs/__init__.py @@ -8,8 +8,8 @@ A job is a dictionary with the following keys: - name: name of the job - description: description of the job - status: status of the job - - created_at: date of creation of the job - - updated_at: date of last update of the job + - created_at: date of creation of the job, naive localtime + - updated_at: date of last update of the job, naive localtime - finished_at: date of finish of the job - error: error message if the job failed - result: result of the job From 73a847f28849a181a52353a0eb7346787370af8b Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 10 Nov 2023 12:19:32 +0000 Subject: [PATCH 73/81] feature(time): timestamp parsers --- selfprivacy_api/utils/time.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 selfprivacy_api/utils/time.py diff --git a/selfprivacy_api/utils/time.py b/selfprivacy_api/utils/time.py new file mode 100644 index 0000000..5eb7e04 --- /dev/null +++ b/selfprivacy_api/utils/time.py @@ -0,0 +1,30 @@ +from datetime import datetime, timezone + + +def tzaware_parse_time(iso_timestamp: str) -> datetime: + """ + parse an iso8601 timestamp into timezone-aware datetime + assume utc if no timezone in stamp + example of timestamp: + 2023-11-10T12:07:47.868788+00:00 + + """ + dt = datetime.fromisoformat(iso_timestamp) + if dt.tzinfo is None: + dt = dt.astimezone(timezone.utc) + return dt + + +def tzaware_parse_time_strict(iso_timestamp: str) -> datetime: + """ + parse an iso8601 timestamp into timezone-aware datetime + raise an error if no timezone in stamp + example of timestamp: + 2023-11-10T12:07:47.868788+00:00 + + """ + dt = datetime.fromisoformat(iso_timestamp) + if dt.tzinfo is None: + raise ValueError("no timezone in timestamp", iso_timestamp) + dt = dt.astimezone(timezone.utc) + return dt From 4d893d56b24dfa9f0bb837d9faa846a9efa214a5 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 10 Nov 2023 12:29:25 +0000 Subject: [PATCH 74/81] test(common): add forced utc times for tests --- tests/common.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/common.py b/tests/common.py index 97d0d7a..55b95a6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -11,6 +11,10 @@ def five_minutes_into_future_naive(): return datetime.now() + timedelta(minutes=5) +def five_minutes_into_future_naive_utc(): + return datetime.utcnow() + timedelta(minutes=5) + + def five_minutes_into_future(): return datetime.now(timezone.utc) + timedelta(minutes=5) @@ -19,6 +23,10 @@ def five_minutes_into_past_naive(): return datetime.now() - timedelta(minutes=5) +def five_minutes_into_past_naive_utc(): + return datetime.utcnow() - timedelta(minutes=5) + + def five_minutes_into_past(): return datetime.now(timezone.utc) - timedelta(minutes=5) From e78bcca9f2f9978136f4e80e11cb854ac87bad4c Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 10 Nov 2023 12:49:30 +0000 Subject: [PATCH 75/81] test(auth): forced utc in recovery tests --- tests/test_graphql/test_api_recovery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 19f8a3d..593c50b 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -10,8 +10,8 @@ from tests.common import ( ) # Graphql API's output should be timezone-naive -from tests.common import five_minutes_into_future_naive as five_minutes_into_future -from tests.common import five_minutes_into_past_naive as five_minutes_into_past +from tests.common import five_minutes_into_future_naive_utc as five_minutes_into_future +from tests.common import five_minutes_into_past_naive_utc as five_minutes_into_past from tests.test_graphql.api_common import ( assert_empty, From 8453f62c746251c11a74aa4acd2e4517f35aa415 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 10 Nov 2023 13:05:38 +0000 Subject: [PATCH 76/81] refactor(time): more time functions --- selfprivacy_api/utils/time.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/selfprivacy_api/utils/time.py b/selfprivacy_api/utils/time.py index 5eb7e04..36871c3 100644 --- a/selfprivacy_api/utils/time.py +++ b/selfprivacy_api/utils/time.py @@ -1,6 +1,29 @@ from datetime import datetime, timezone +def ensure_tz_aware(dt: datetime) -> datetime: + """ + returns timezone-aware datetime + assumes utc on naive datetime input + """ + if dt.tzinfo is None: + dt = dt.astimezone(timezone.utc) + return dt + + +def ensure_tz_aware_strict(dt: datetime) -> datetime: + """ + returns timezone-aware datetime + raises error if input is a naive datetime + """ + if dt.tzinfo is None: + raise ValueError( + "no timezone in datetime (tz-aware datetime is required for this operation)", + dt, + ) + return dt + + def tzaware_parse_time(iso_timestamp: str) -> datetime: """ parse an iso8601 timestamp into timezone-aware datetime @@ -10,8 +33,7 @@ def tzaware_parse_time(iso_timestamp: str) -> datetime: """ dt = datetime.fromisoformat(iso_timestamp) - if dt.tzinfo is None: - dt = dt.astimezone(timezone.utc) + dt = ensure_tz_aware(dt) return dt @@ -24,7 +46,5 @@ def tzaware_parse_time_strict(iso_timestamp: str) -> datetime: """ dt = datetime.fromisoformat(iso_timestamp) - if dt.tzinfo is None: - raise ValueError("no timezone in timestamp", iso_timestamp) - dt = dt.astimezone(timezone.utc) + dt = ensure_tz_aware_strict(dt) return dt From 8badb9aaaf79fd20ff3311b936bfcf9a10d21766 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 10 Nov 2023 13:31:12 +0000 Subject: [PATCH 77/81] refactor(auth): tz_aware expiration comparison --- selfprivacy_api/actions/api_tokens.py | 6 ++++-- selfprivacy_api/utils/{time.py => timeutils.py} | 0 2 files changed, 4 insertions(+), 2 deletions(-) rename selfprivacy_api/utils/{time.py => timeutils.py} (100%) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 37b7631..3746c57 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -7,6 +7,7 @@ from typing import Optional from pydantic import BaseModel from mnemonic import Mnemonic +from selfprivacy_api.utils.timeutils import ensure_tz_aware from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( RedisTokensRepository, ) @@ -121,8 +122,9 @@ def get_new_api_recovery_key( ) -> str: """Get new recovery key""" if expiration_date is not None: - current_time = datetime.now().timestamp() - if expiration_date.timestamp() < current_time: + expiration_date = ensure_tz_aware(expiration_date) + current_time = datetime.now(timezone.utc) + if expiration_date < current_time: raise InvalidExpirationDate("Expiration date is in the past") if uses_left is not None: if uses_left <= 0: diff --git a/selfprivacy_api/utils/time.py b/selfprivacy_api/utils/timeutils.py similarity index 100% rename from selfprivacy_api/utils/time.py rename to selfprivacy_api/utils/timeutils.py From dd6f37a17d918e4ea92f3cc0959982c3d7e5c6ed Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 10 Nov 2023 17:10:01 +0000 Subject: [PATCH 78/81] feature(auth): tz_aware recovery --- selfprivacy_api/actions/api_tokens.py | 14 +++++++---- .../graphql/queries/api_queries.py | 2 +- tests/common.py | 7 +++--- tests/test_graphql/test_api_recovery.py | 24 +++++++++++++++---- tests/test_rest_endpoints/test_auth.py | 3 ++- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/selfprivacy_api/actions/api_tokens.py b/selfprivacy_api/actions/api_tokens.py index 3746c57..e93491f 100644 --- a/selfprivacy_api/actions/api_tokens.py +++ b/selfprivacy_api/actions/api_tokens.py @@ -7,7 +7,7 @@ from typing import Optional from pydantic import BaseModel from mnemonic import Mnemonic -from selfprivacy_api.utils.timeutils import ensure_tz_aware +from selfprivacy_api.utils.timeutils import ensure_tz_aware, ensure_tz_aware_strict from selfprivacy_api.repositories.tokens.redis_tokens_repository import ( RedisTokensRepository, ) @@ -95,16 +95,22 @@ class RecoveryTokenStatus(BaseModel): def get_api_recovery_token_status() -> RecoveryTokenStatus: - """Get the recovery token status""" + """Get the recovery token status, timezone-aware""" token = TOKEN_REPO.get_recovery_key() if token is None: return RecoveryTokenStatus(exists=False, valid=False) is_valid = TOKEN_REPO.is_recovery_key_valid() + + # New tokens are tz-aware, but older ones might not be + expiry_date = token.expires_at + if expiry_date is not None: + expiry_date = ensure_tz_aware_strict(expiry_date) + return RecoveryTokenStatus( exists=True, valid=is_valid, - date=_naive(token.created_at), - expiration=_naive(token.expires_at), + date=ensure_tz_aware_strict(token.created_at), + expiration=expiry_date, uses_left=token.uses_left, ) diff --git a/selfprivacy_api/graphql/queries/api_queries.py b/selfprivacy_api/graphql/queries/api_queries.py index cf56231..7052ded 100644 --- a/selfprivacy_api/graphql/queries/api_queries.py +++ b/selfprivacy_api/graphql/queries/api_queries.py @@ -38,7 +38,7 @@ class ApiRecoveryKeyStatus: def get_recovery_key_status() -> ApiRecoveryKeyStatus: - """Get recovery key status""" + """Get recovery key status, times are timezone-aware""" status = get_api_recovery_token_status() if status is None or not status.exists: return ApiRecoveryKeyStatus( diff --git a/tests/common.py b/tests/common.py index 55b95a6..c327ae9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -67,8 +67,7 @@ def mnemonic_to_hex(mnemonic): return Mnemonic(language="english").to_entropy(mnemonic).hex() -def assert_recovery_recent(time_generated): - assert ( - datetime.strptime(time_generated, "%Y-%m-%dT%H:%M:%S.%f") - timedelta(seconds=5) - < datetime.now() +def assert_recovery_recent(time_generated: str): + assert datetime.fromisoformat(time_generated) - timedelta(seconds=5) < datetime.now( + timezone.utc ) diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index 593c50b..b0155e7 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -2,6 +2,10 @@ # pylint: disable=unused-argument # pylint: disable=missing-function-docstring +import pytest + +from datetime import datetime, timezone + from tests.common import ( generate_api_query, assert_recovery_recent, @@ -11,6 +15,7 @@ from tests.common import ( # Graphql API's output should be timezone-naive from tests.common import five_minutes_into_future_naive_utc as five_minutes_into_future +from tests.common import five_minutes_into_future as five_minutes_into_future_tz from tests.common import five_minutes_into_past_naive_utc as five_minutes_into_past from tests.test_graphql.api_common import ( @@ -158,17 +163,24 @@ def test_graphql_generate_recovery_key(client, authorized_client, tokens_file): graphql_use_recovery_key(client, key, "new_test_token2") +@pytest.mark.parametrize( + "expiration_date", [five_minutes_into_future(), five_minutes_into_future_tz()] +) def test_graphql_generate_recovery_key_with_expiration_date( - client, authorized_client, tokens_file + client, authorized_client, tokens_file, expiration_date: datetime ): - expiration_date = five_minutes_into_future() key = graphql_make_new_recovery_key(authorized_client, expires_at=expiration_date) status = graphql_recovery_status(authorized_client) assert status["exists"] is True assert status["valid"] is True assert_recovery_recent(status["creationDate"]) - assert status["expirationDate"] == expiration_date.isoformat() + + # timezone-aware comparison. Should pass regardless of server's tz + assert datetime.fromisoformat( + status["expirationDate"] + ) == expiration_date.astimezone(timezone.utc) + assert status["usesLeft"] is None graphql_use_recovery_key(client, key, "new_test_token") @@ -194,7 +206,11 @@ def test_graphql_use_recovery_key_after_expiration( assert status["exists"] is True assert status["valid"] is False assert_recovery_recent(status["creationDate"]) - assert status["expirationDate"] == expiration_date.isoformat() + + # timezone-aware comparison. Should pass regardless of server's tz + assert datetime.fromisoformat( + status["expirationDate"] + ) == expiration_date.astimezone(timezone.utc) assert status["usesLeft"] is None diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index d62fa18..8565143 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -2,6 +2,7 @@ # pylint: disable=unused-argument # pylint: disable=missing-function-docstring import datetime +from datetime import timezone import pytest from tests.conftest import TOKENS_FILE_CONTENTS @@ -337,7 +338,7 @@ def test_generate_recovery_token_with_expiration_date( "exists": True, "valid": True, "date": time_generated, - "expiration": expiration_date.isoformat(), + "expiration": expiration_date.astimezone(timezone.utc).isoformat(), "uses_left": None, } From 1bbb804919a76d827f621a70f7069e52de12a474 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Fri, 10 Nov 2023 17:40:52 +0000 Subject: [PATCH 79/81] test(auth): token tests clearer about timezone assumptions --- selfprivacy_api/models/tokens/new_device_key.py | 4 ++-- selfprivacy_api/models/tokens/recovery_key.py | 1 + tests/test_models.py | 15 +++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/selfprivacy_api/models/tokens/new_device_key.py b/selfprivacy_api/models/tokens/new_device_key.py index 9fbd23b..241cbd3 100644 --- a/selfprivacy_api/models/tokens/new_device_key.py +++ b/selfprivacy_api/models/tokens/new_device_key.py @@ -22,7 +22,7 @@ class NewDeviceKey(BaseModel): def is_valid(self) -> bool: """ - Check if the recovery key is valid. + Check if key is valid. """ if is_past(self.expires_at): return False @@ -30,7 +30,7 @@ class NewDeviceKey(BaseModel): def as_mnemonic(self) -> str: """ - Get the recovery key as a mnemonic. + Get the key as a mnemonic. """ return Mnemonic(language="english").to_mnemonic(bytes.fromhex(self.key)) diff --git a/selfprivacy_api/models/tokens/recovery_key.py b/selfprivacy_api/models/tokens/recovery_key.py index 3b81398..3f52735 100644 --- a/selfprivacy_api/models/tokens/recovery_key.py +++ b/selfprivacy_api/models/tokens/recovery_key.py @@ -47,6 +47,7 @@ class RecoveryKey(BaseModel): ) -> "RecoveryKey": """ Factory to generate a random token. + If passed naive time as expiration, assumes utc """ creation_date = datetime.now(timezone.utc) if expiration is not None: diff --git a/tests/test_models.py b/tests/test_models.py index 2263e82..f01bb4f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,18 +1,25 @@ import pytest -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from selfprivacy_api.models.tokens.recovery_key import RecoveryKey from selfprivacy_api.models.tokens.new_device_key import NewDeviceKey -def test_recovery_key_expired(): - expiration = datetime.now() - timedelta(minutes=5) +def test_recovery_key_expired_utcnaive(): + expiration = datetime.utcnow() - timedelta(minutes=5) + key = RecoveryKey.generate(expiration=expiration, uses_left=2) + assert not key.is_valid() + + +def test_recovery_key_expired_tzaware(): + expiration = datetime.now(timezone.utc) - timedelta(minutes=5) key = RecoveryKey.generate(expiration=expiration, uses_left=2) assert not key.is_valid() def test_new_device_key_expired(): - expiration = datetime.now() - timedelta(minutes=5) + # key is supposed to be tzaware + expiration = datetime.now(timezone.utc) - timedelta(minutes=5) key = NewDeviceKey.generate() key.expires_at = expiration assert not key.is_valid() From e414f3b8fd46be99b2dfe4b0fb750086cb73271a Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 13 Nov 2023 09:15:12 -0700 Subject: [PATCH 80/81] fix(auth): fix timezone issues with recovery tokens --- selfprivacy_api/utils/timeutils.py | 4 +++- tests/common.py | 4 ++++ tests/test_graphql/api_common.py | 12 ++++++------ tests/test_graphql/test_api_recovery.py | 12 ++++++------ tests/test_rest_endpoints/test_auth.py | 6 +++--- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/selfprivacy_api/utils/timeutils.py b/selfprivacy_api/utils/timeutils.py index 36871c3..b6494c6 100644 --- a/selfprivacy_api/utils/timeutils.py +++ b/selfprivacy_api/utils/timeutils.py @@ -7,7 +7,9 @@ def ensure_tz_aware(dt: datetime) -> datetime: assumes utc on naive datetime input """ if dt.tzinfo is None: - dt = dt.astimezone(timezone.utc) + # astimezone() is dangerous, it makes an implicit assumption that + # the time is localtime + dt = dt.replace(tzinfo=timezone.utc) return dt diff --git a/tests/common.py b/tests/common.py index c327ae9..5199899 100644 --- a/tests/common.py +++ b/tests/common.py @@ -36,6 +36,10 @@ class NearFuture(datetime): def now(cls, tz=None): return datetime.now(tz) + timedelta(minutes=13) + @classmethod + def utcnow(cls): + return datetime.utcnow() + timedelta(minutes=13) + def read_json(file_path): with open(file_path, "r", encoding="utf-8") as file: diff --git a/tests/test_graphql/api_common.py b/tests/test_graphql/api_common.py index bfac767..4e4aec2 100644 --- a/tests/test_graphql/api_common.py +++ b/tests/test_graphql/api_common.py @@ -6,16 +6,16 @@ ORIGINAL_DEVICES = TOKENS_FILE_CONTENTS["tokens"] def assert_ok(response, request): data = assert_data(response) - data[request]["success"] is True - data[request]["message"] is not None - data[request]["code"] == 200 + assert data[request]["success"] is True + assert data[request]["message"] is not None + assert data[request]["code"] == 200 def assert_errorcode(response, request, code): data = assert_data(response) - data[request]["success"] is False - data[request]["message"] is not None - data[request]["code"] == code + assert data[request]["success"] is False + assert data[request]["message"] is not None + assert data[request]["code"] == code def assert_empty(response): diff --git a/tests/test_graphql/test_api_recovery.py b/tests/test_graphql/test_api_recovery.py index b0155e7..629bac0 100644 --- a/tests/test_graphql/test_api_recovery.py +++ b/tests/test_graphql/test_api_recovery.py @@ -177,9 +177,9 @@ def test_graphql_generate_recovery_key_with_expiration_date( assert_recovery_recent(status["creationDate"]) # timezone-aware comparison. Should pass regardless of server's tz - assert datetime.fromisoformat( - status["expirationDate"] - ) == expiration_date.astimezone(timezone.utc) + assert datetime.fromisoformat(status["expirationDate"]) == expiration_date.replace( + tzinfo=timezone.utc + ) assert status["usesLeft"] is None @@ -208,9 +208,9 @@ def test_graphql_use_recovery_key_after_expiration( assert_recovery_recent(status["creationDate"]) # timezone-aware comparison. Should pass regardless of server's tz - assert datetime.fromisoformat( - status["expirationDate"] - ) == expiration_date.astimezone(timezone.utc) + assert datetime.fromisoformat(status["expirationDate"]) == expiration_date.replace( + tzinfo=timezone.utc + ) assert status["usesLeft"] is None diff --git a/tests/test_rest_endpoints/test_auth.py b/tests/test_rest_endpoints/test_auth.py index 8565143..4d0d2ed 100644 --- a/tests/test_rest_endpoints/test_auth.py +++ b/tests/test_rest_endpoints/test_auth.py @@ -12,8 +12,8 @@ from tests.common import ( NearFuture, assert_recovery_recent, ) -from tests.common import five_minutes_into_future_naive as five_minutes_into_future -from tests.common import five_minutes_into_past_naive as five_minutes_into_past +from tests.common import five_minutes_into_future_naive_utc as five_minutes_into_future +from tests.common import five_minutes_into_past_naive_utc as five_minutes_into_past DATE_FORMATS = [ "%Y-%m-%dT%H:%M:%S.%fZ", @@ -338,7 +338,7 @@ def test_generate_recovery_token_with_expiration_date( "exists": True, "valid": True, "date": time_generated, - "expiration": expiration_date.astimezone(timezone.utc).isoformat(), + "expiration": expiration_date.replace(tzinfo=timezone.utc).isoformat(), "uses_left": None, } From c3cec36ad4f331a5397681f414e655f775ed7a34 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 13 Nov 2023 19:36:12 +0300 Subject: [PATCH 81/81] style: formatting --- selfprivacy_api/backup/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/selfprivacy_api/backup/__init__.py b/selfprivacy_api/backup/__init__.py index a5fe066..f575ac0 100644 --- a/selfprivacy_api/backup/__init__.py +++ b/selfprivacy_api/backup/__init__.py @@ -708,7 +708,9 @@ class Backups: # assume it is in localtime offset = timedelta(seconds=time.localtime().tm_gmtoff) datetime_created = datetime_created - offset - return datetime.combine(datetime_created.date(), datetime_created.time(),timezone.utc) + return datetime.combine( + datetime_created.date(), datetime_created.time(), timezone.utc + ) return datetime_created return None