From a89158377f829720a98342cf434a18b962006ee7 Mon Sep 17 00:00:00 2001 From: pennae Date: Mon, 18 Jul 2022 17:12:52 +0200 Subject: speed up test suite mostly by grouping tests that can reuse the same account (which is expensive to create) into classes and scoping accounts to classes. --- tests/api.py | 6 +- tests/conftest.py | 27 +- tests/test_auth_account.py | 99 +++---- tests/test_auth_device.py | 557 ++++++++++++++++++++------------------- tests/test_auth_email.py | 98 +++---- tests/test_auth_oauth.py | 619 ++++++++++++++++++++++---------------------- tests/test_auth_password.py | 189 +++++++------- tests/test_auth_session.py | 85 +++--- tests/test_oauth.py | 109 ++++---- tests/test_profile.py | 235 ++++++++--------- tests/test_push.py | 226 ++++++++-------- 11 files changed, 1140 insertions(+), 1110 deletions(-) (limited to 'tests') diff --git a/tests/api.py b/tests/api.py index 32e1159..da82d37 100644 --- a/tests/api.py +++ b/tests/api.py @@ -213,9 +213,9 @@ class PushServer: server = self.server = http.server.ThreadingHTTPServer(("localhost", PUSH_PORT), Handler) threading.Thread(target=server.serve_forever).start() - def wait(self, timeout=2): + def wait(self, timeout=0.5): return self.q.get(timeout=timeout) - def done(self, timeout=2): + def done(self, timeout=0.5): try: self.q.get(timeout=timeout) return False @@ -249,5 +249,5 @@ class MailServer: def stop(self): self.controller.stop() - def wait(self, timeout=2): + def wait(self, timeout=0.5): return self.q.get(timeout=timeout) diff --git a/tests/conftest.py b/tests/conftest.py index 43b32f2..bf877e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ def push_server(): s.server.shutdown() s.server.server_close() -@pytest.fixture +@pytest.fixture(scope="class") def mail_server(): s = MailServer() yield s @@ -55,17 +55,18 @@ def _account(client, primary, email, mail_server): email1 = f"test.account-{os.urandom(8).hex()}@test-auth" email2 = f"test.account2-{os.urandom(8).hex()}@test-auth" -@pytest.fixture(params=[True, False], ids=["primary", "secondary"]) -def account(client, request, mail_server): - for a in _account(client, request.param, email1, mail_server): +@pytest.fixture(params=[True, False], ids=["primary", "secondary"], scope="class") +def account(request, mail_server): + for a in _account(AuthClient(), request.param, email1, mail_server): yield a -@pytest.fixture(params=[True, False], ids=["primary", "secondary"]) -def account2(client, request, mail_server): - for a in _account(client, request.param, email2, mail_server): +@pytest.fixture(params=[True, False], ids=["primary", "secondary"], scope="class") +def account2(request, mail_server): + for a in _account(AuthClient(), request.param, email2, mail_server): yield a -@pytest.fixture -def unverified_account(client, mail_server): +@pytest.fixture(scope="class") +def unverified_account(mail_server): + client = AuthClient() s = client.create_account(email1, "") yield s s.destroy_account(s.email, "") @@ -109,10 +110,14 @@ def account_or_rt(account, request): yield r @pytest.fixture -def forgot_token(account): +def forgot_token(account, mail_server): resp = account.post_a("/password/forgot/send_code", { 'email': account.email }) assert 'passwordForgotToken' in resp assert resp['ttl'] == 300 assert resp['codeLength'] == 16 assert resp['tries'] == 1 - return PasswordChange(account.client, resp['passwordForgotToken'], 'passwordForgotToken') + (to, body) = mail_server.wait() + assert account.email in to + pc = PasswordChange(account.client, resp['passwordForgotToken'], 'passwordForgotToken') + pc.code = body.strip() + return pc diff --git a/tests/test_auth_account.py b/tests/test_auth_account.py index 0e7e22d..365a53c 100644 --- a/tests/test_auth_account.py +++ b/tests/test_auth_account.py @@ -41,15 +41,16 @@ def test_create_nomail(client): 'message': 'failed to send email' } -def test_create_exists(account): - with pytest.raises(ClientError) as e: - account.create_account(account.email, "") - assert e.value.details == { - 'code': 400, - 'errno': 101, - 'error': 'Bad Request', - 'message': 'account already exists' - } +class TestCreateExists: + def test_create_exists(self, account): + with pytest.raises(ClientError) as e: + account.create_account(account.email, "") + assert e.value.details == { + 'code': 400, + 'errno': 101, + 'error': 'Bad Request', + 'message': 'account already exists' + } @pytest.mark.parametrize("keys", [None, False, True]) @pytest.mark.parametrize("verify", [False, True]) @@ -120,25 +121,26 @@ def test_login_unverified(client, unverified_account): 'message': 'unverified account' } -def test_login_badcase(account): - with pytest.raises(ClientError) as e: - account.login(account.email.upper(), "") - assert e.value.details == { - 'code': 400, - 'errno': 120, - 'error': 'Bad Request', - 'message': 'incorrect email case' - } +class TestBadLogin: + def test_login_badcase(self, account): + with pytest.raises(ClientError) as e: + account.login(account.email.upper(), "") + assert e.value.details == { + 'code': 400, + 'errno': 120, + 'error': 'Bad Request', + 'message': 'incorrect email case' + } -def test_login_badpasswd(account): - with pytest.raises(ClientError) as e: - account.login(account.email, "ca0cb780-f114-405a-a5c2-1a4deb933a51") - assert e.value.details == { - 'code': 400, - 'errno': 103, - 'error': 'Bad Request', - 'message': 'incorrect password' - } + def test_login_badpasswd(self, account): + with pytest.raises(ClientError) as e: + account.login(account.email, "ca0cb780-f114-405a-a5c2-1a4deb933a51") + assert e.value.details == { + 'code': 400, + 'errno': 103, + 'error': 'Bad Request', + 'message': 'incorrect password' + } @pytest.mark.parametrize("keys", [None, False, True]) def test_login(client, account, keys): @@ -183,25 +185,26 @@ def test_destroy_noaccount(client): 'message': 'unknown account' } -def test_destroy_badcase(account): - with pytest.raises(ClientError) as e: - account.destroy_account(account.email.upper(), "") - assert e.value.details == { - 'code': 400, - 'errno': 120, - 'error': 'Bad Request', - 'message': 'incorrect email case' - } +class TestBadDestroy: + def test_destroy_badcase(self, account): + with pytest.raises(ClientError) as e: + account.destroy_account(account.email.upper(), "") + assert e.value.details == { + 'code': 400, + 'errno': 120, + 'error': 'Bad Request', + 'message': 'incorrect email case' + } -def test_destroy_badpasswd(account): - with pytest.raises(ClientError) as e: - account.destroy_account(account.email, "ca0cb780-f114-405a-a5c2-1a4deb933a51") - assert e.value.details == { - 'code': 400, - 'errno': 103, - 'error': 'Bad Request', - 'message': 'incorrect password' - } + def test_destroy_badpasswd(self, account): + with pytest.raises(ClientError) as e: + account.destroy_account(account.email, "ca0cb780-f114-405a-a5c2-1a4deb933a51") + assert e.value.details == { + 'code': 400, + 'errno': 103, + 'error': 'Bad Request', + 'message': 'incorrect password' + } @pytest.mark.parametrize("auth", [ {}, @@ -224,9 +227,7 @@ def test_keys_invalid(client, auth): @pytest.fixture def reset_token(account, forgot_token, mail_server): - (to, body) = mail_server.wait() - assert account.email in to - resp = forgot_token.post_a("/password/forgot/verify_code", { 'code': body.strip() }) + resp = forgot_token.post_a("/password/forgot/verify_code", { 'code': forgot_token.code }) return AccountReset(account.client, resp['accountResetToken']) @pytest.mark.parametrize("args", [ @@ -262,7 +263,7 @@ def test_reset(account, reset_token, mail_server, push_server): } assert push_server.done() -def test_reset(account, reset_token, mail_server, push_server): +def test_reset_twice(account, reset_token, mail_server, push_server): reset_token.post_a("/account/reset", { 'authPW': auth_pw(account.email, "") }) with pytest.raises(ClientError) as e: reset_token.post_a("/account/reset", { 'authPW': auth_pw(account.email, "") }) diff --git a/tests/test_auth_device.py b/tests/test_auth_device.py index 8504ba7..5ec42f3 100644 --- a/tests/test_auth_device.py +++ b/tests/test_auth_device.py @@ -29,58 +29,113 @@ def test_create_noauth(client): 'message': 'invalid request signature' } -@pytest.mark.parametrize("args,code,errno,error,message", [ - ({ 'name': 'dev', 'availableCommands': {'a':1} }, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({ 'name': 'dev', 'availableCommands': {'a':1}, 'extra': 0 }, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({}, - 400, 108, 'Bad Request', 'missing parameter in request body'), - ({ 'id': '00' * 16, 'name': 'dev' }, - 400, 108, 'Bad Request', 'missing parameter in request body'), -]) -def test_create_invalid(account_or_rt, args, code, errno, error, message): - with pytest.raises(ClientError) as e: - account_or_rt.post_a("/account/device", args) - assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} - -def test_destroy_noauth(client, populate_devices): - with pytest.raises(ClientError) as e: - client.post_a("/account/device/destroy") - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } - with pytest.raises(ClientError) as e: - client.post_a("/account/device/destroy", {'id': populate_devices[0]['id']}) - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } - -@pytest.mark.parametrize("args,code,errno,error,message", [ - ({ 'id': '00' }, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({ 'id': '00' * 16, 'extra': 0 }, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({ 'id': '00' * 16 }, - 400, 123, 'Bad Request', 'unknown device'), -]) -def test_destroy_invalid(account_or_rt, args, code, errno, error, message): - with pytest.raises(ClientError) as e: - account_or_rt.post_a("/account/device/destroy", args) - assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} +class TestBadCreate: + @pytest.mark.parametrize("args,code,errno,error,message", [ + ({ 'name': 'dev', 'availableCommands': {'a':1} }, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({ 'name': 'dev', 'availableCommands': {'a':1}, 'extra': 0 }, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({}, + 400, 108, 'Bad Request', 'missing parameter in request body'), + ({ 'id': '00' * 16, 'name': 'dev' }, + 400, 108, 'Bad Request', 'missing parameter in request body'), + ]) + def test_create_invalid(self, account_or_rt, args, code, errno, error, message): + with pytest.raises(ClientError) as e: + account_or_rt.post_a("/account/device", args) + assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} + +class TestDestroyNoauth: + def test_destroy_noauth(self, client, populate_devices): + with pytest.raises(ClientError) as e: + client.post_a("/account/device/destroy") + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } + with pytest.raises(ClientError) as e: + client.post_a("/account/device/destroy", {'id': populate_devices[0]['id']}) + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } + +class TestDestroyInvalid: + @pytest.mark.parametrize("args,code,errno,error,message", [ + ({ 'id': '00' }, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({ 'id': '00' * 16, 'extra': 0 }, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({ 'id': '00' * 16 }, + 400, 123, 'Bad Request', 'unknown device'), + ]) + def test_destroy_invalid(self, account_or_rt, args, code, errno, error, message): + with pytest.raises(ClientError) as e: + account_or_rt.post_a("/account/device/destroy", args) + assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} def test_create_destroy(account_or_rt): dev = account_or_rt.post_a("/account/device", device_data[0]) account_or_rt.post_a("/account/device/destroy", {'id': dev['id']}) -def test_create_unverified(unverified_account): - unverified_account.post_a("/account/device", device_data[0]) +class TestUnverified: + def test_create_unverified(self, unverified_account): + unverified_account.post_a("/account/device", device_data[0]) + + def test_list_unverified(self, unverified_account): + with pytest.raises(ClientError) as e: + unverified_account.get_a("/account/devices") + assert e.value.details == { + 'code': 400, + 'errno': 138, + 'error': 'Bad Request', + 'message': 'unverified session' + } + + def test_invoke_unverified(self, unverified_account): + body = {"target": "0" * 32, "command": "foo", "payload": {}, "ttl": 10} + with pytest.raises(ClientError) as e: + unverified_account.post_a("/account/devices/invoke_command", body) + assert e.value.details == { + 'code': 400, + 'errno': 138, + 'error': 'Bad Request', + 'message': 'unverified session' + } + + def test_commands_unverified(self, unverified_account): + with pytest.raises(ClientError) as e: + unverified_account.get_a("/account/device/commands?index=1") + assert e.value.details == { + 'code': 400, + 'errno': 138, + 'error': 'Bad Request', + 'message': 'unverified session' + } + + def test_attached_clients_unverified(self, unverified_account): + with pytest.raises(ClientError) as e: + unverified_account.get_a("/account/attached_clients") + assert e.value.details == { + 'code': 400, + 'errno': 138, + 'error': 'Bad Request', + 'message': 'unverified session' + } + + def test_attached_client_destroy_unverified(self, unverified_account): + with pytest.raises(ClientError) as e: + unverified_account.post_a("/account/attached_client/destroy") + assert e.value.details == { + 'code': 400, + 'errno': 138, + 'error': 'Bad Request', + 'message': 'unverified session' + } def test_list_noauth(client): with pytest.raises(ClientError) as e: @@ -92,19 +147,10 @@ def test_list_noauth(client): 'message': 'invalid request signature' } -def test_list_unverified(unverified_account): - with pytest.raises(ClientError) as e: - unverified_account.get_a("/account/devices") - assert e.value.details == { - 'code': 400, - 'errno': 138, - 'error': 'Bad Request', - 'message': 'unverified session' - } - -def test_list_none(account_or_rt): - devs = account_or_rt.get_a("/account/devices") - assert devs == [] +class TestListNone: + def test_list_none(self, account_or_rt): + devs = account_or_rt.get_a("/account/devices") + assert devs == [] @pytest.mark.usefixtures("populate_devices") def test_list(account_or_rt): @@ -157,76 +203,68 @@ def test_invoke_noauth(client): 'message': 'invalid request signature' } -def test_invoke_unverified(unverified_account): - body = {"target": "0" * 32, "command": "foo", "payload": {}, "ttl": 10} - with pytest.raises(ClientError) as e: - unverified_account.post_a("/account/devices/invoke_command", body) - assert e.value.details == { - 'code': 400, - 'errno': 138, - 'error': 'Bad Request', - 'message': 'unverified session' - } - -@pytest.mark.parametrize("args,code,errno,error,message", [ - ({"target": "00", "command": "foo", "payload": {}, "ttl": 10}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({"target": "00" * 16, "command": "foo", "payload": {}, "ttl": 10, 'extra': 0}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({"target": "0" * 32, "command": "foo", "payload": {}, "ttl": 10}, - 400, 123, 'Bad Request', 'unknown device'), -]) -def test_invoke_invalid(account_or_rt, args, code, errno, error, message): - with pytest.raises(ClientError) as e: - account_or_rt.post_a("/account/devices/invoke_command", args) - assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} - -def test_invoke_nocmd(account_or_rt, populate_devices): - body = {"target": populate_devices[0]['id'], "command": "foo", "payload": {}, "ttl": 10} - with pytest.raises(ClientError) as e: - account_or_rt.post_a("/account/devices/invoke_command", body) - assert e.value.details == { - 'code': 400, - 'errno': 157, - 'error': 'Bad Request', - 'message': 'unavailable device command' - } +class TestBadInvoke: + @pytest.mark.parametrize("args,code,errno,error,message", [ + ({"target": "00", "command": "foo", "payload": {}, "ttl": 10}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({"target": "00" * 16, "command": "foo", "payload": {}, "ttl": 10, 'extra': 0}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({"target": "0" * 32, "command": "foo", "payload": {}, "ttl": 10}, + 400, 123, 'Bad Request', 'unknown device'), + ]) + def test_invoke_invalid(self, account_or_rt, args, code, errno, error, message): + with pytest.raises(ClientError) as e: + account_or_rt.post_a("/account/devices/invoke_command", args) + assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} + + def test_invoke_nocmd(self, account_or_rt, populate_devices): + body = {"target": populate_devices[0]['id'], "command": "foo", "payload": {}, "ttl": 10} + with pytest.raises(ClientError) as e: + account_or_rt.post_a("/account/devices/invoke_command", body) + assert e.value.details == { + 'code': 400, + 'errno': 157, + 'error': 'Bad Request', + 'message': 'unavailable device command' + } + +class TestInvokeOther: + def test_invoke_other_account(self, account_or_rt, account2): + dev = account2.post_a("/account/device", device_data[0]) + body = {"target": dev['id'], "command": "foo", "payload": {}, "ttl": 10} + with pytest.raises(ClientError) as e: + account_or_rt.post_a("/account/devices/invoke_command", body) + assert e.value.details == { + 'code': 400, + 'errno': 123, + 'error': 'Bad Request', + 'message': 'unknown device' + } + +class TestInvoke: + @pytest.mark.parametrize("ttl", [None, 1, 60, 999999999]) + @pytest.mark.parametrize("has_sender", [False, True]) + def test_invoke(self, account_or_rt, login, ttl, has_sender): + sender = account_or_rt.post_a("/account/device", device_data[1])['id'] if has_sender else None + dev = login.post_a("/account/device", device_data[0]) + + body = { + "target": dev['id'], + "command": "a", + "payload": { "data": str(random.random()), }, + "ttl": ttl, + } + resp = account_or_rt.post_a("/account/devices/invoke_command", body) + assert resp['enqueued'] + assert not resp['notified'] + assert resp['notifyError'] == "no push callback" -def test_invoke_other_account(account_or_rt, account2): - dev = account2.post_a("/account/device", device_data[0]) - body = {"target": dev['id'], "command": "foo", "payload": {}, "ttl": 10} - with pytest.raises(ClientError) as e: - account_or_rt.post_a("/account/devices/invoke_command", body) - assert e.value.details == { - 'code': 400, - 'errno': 123, - 'error': 'Bad Request', - 'message': 'unknown device' - } - -@pytest.mark.parametrize("ttl", [None, 1, 60, 999999999]) -@pytest.mark.parametrize("has_sender", [False, True]) -def test_invoke(account_or_rt, login, ttl, has_sender): - sender = account_or_rt.post_a("/account/device", device_data[1])['id'] if has_sender else None - dev = login.post_a("/account/device", device_data[0]) - - body = { - "target": dev['id'], - "command": "a", - "payload": { "data": str(random.random()), }, - "ttl": ttl, - } - resp = account_or_rt.post_a("/account/devices/invoke_command", body) - assert resp['enqueued'] - assert not resp['notified'] - assert resp['notifyError'] == "no push callback" - - cmd = login.get_a("/account/device/commands?index=0&limit=11") - assert cmd['last'] - assert len(cmd['messages']) == 1 - assert cmd['messages'][0]['data']['command'] == 'a' - assert cmd['messages'][0]['data']['payload'] == body['payload'] - assert cmd['messages'][0]['data']['sender'] == sender + cmd = login.get_a("/account/device/commands?index=0&limit=11") + assert cmd['last'] + assert len(cmd['messages']) == 1 + assert cmd['messages'][0]['data']['command'] == 'a' + assert cmd['messages'][0]['data']['payload'] == body['payload'] + assert cmd['messages'][0]['data']['sender'] == sender def test_commands_noauth(client): with pytest.raises(ClientError) as e: @@ -238,65 +276,56 @@ def test_commands_noauth(client): 'message': 'invalid request signature' } -def test_commands_unverified(unverified_account): - with pytest.raises(ClientError) as e: - unverified_account.get_a("/account/device/commands?index=1") - assert e.value.details == { - 'code': 400, - 'errno': 138, - 'error': 'Bad Request', - 'message': 'unverified session' - } - -def test_commands_nodev(account_or_rt): - with pytest.raises(ClientError) as e: - account_or_rt.get_a("/account/device/commands?index=1") - assert e.value.details == { - 'code': 400, - 'errno': 123, - 'error': 'Bad Request', - 'message': 'unknown device' - } - -@pytest.mark.parametrize('n_cmds,offset,limit', [ - (1, 0, 0), - (1, 0, 1), - (1, 0, 100), - (1, 1, 0), - (1, 1, 1), - (1, 1, 100), - (2, 0, 0), - (2, 0, 1), - (2, 0, 100), - (2, 1, 0), - (2, 1, 1), - (2, 1, 100), - (101, 0, 100), - (101, 10, 100), -]) -def test_device_commands_list(account_or_rt, login, n_cmds, offset, limit): - account_or_rt.post_a("/account/device", device_data[1])['id'] - dev = login.post_a("/account/device", device_data[0]) - - bodies = [ { - "target": dev['id'], - "command": "a", - "payload": { "data": str(i), }, - } for i in range(0, n_cmds) ] - - for b in bodies: - resp = account_or_rt.post_a("/account/devices/invoke_command", b) - assert resp['enqueued'] - assert not resp['notified'] - assert resp['notifyError'] == "no push callback" - - cmd = login.get_a("/account/device/commands", params={ "index": 0, "limit": 1 }) - assert cmd == login.get_a("/account/device/commands", params={ "index": 0, "limit": 1 }) - - first_index = cmd['index'] - cmds = login.get_a("/account/device/commands", params={ "limit": limit, "index": first_index + offset }) - assert cmds['last'] == (offset + limit >= n_cmds) - assert len(cmds['messages']) == min(max(n_cmds - offset, 0), limit) +class TestDeviceCommands: + def test_commands_nodev(self, account_or_rt): + with pytest.raises(ClientError) as e: + account_or_rt.get_a("/account/device/commands?index=1") + assert e.value.details == { + 'code': 400, + 'errno': 123, + 'error': 'Bad Request', + 'message': 'unknown device' + } + + @pytest.mark.parametrize('n_cmds,offset,limit', [ + (1, 0, 0), + (1, 0, 1), + (1, 0, 100), + (1, 1, 0), + (1, 1, 1), + (1, 1, 100), + (2, 0, 0), + (2, 0, 1), + (2, 0, 100), + (2, 1, 0), + (2, 1, 1), + (2, 1, 100), + (101, 0, 100), + (101, 10, 100), + ]) + def test_device_commands_list(self, account_or_rt, login, n_cmds, offset, limit): + account_or_rt.post_a("/account/device", device_data[1])['id'] + dev = login.post_a("/account/device", device_data[0]) + + bodies = [ { + "target": dev['id'], + "command": "a", + "payload": { "data": str(i), }, + } for i in range(0, n_cmds) ] + + for b in bodies: + resp = account_or_rt.post_a("/account/devices/invoke_command", b) + assert resp['enqueued'] + assert not resp['notified'] + assert resp['notifyError'] == "no push callback" + + cmd = login.get_a("/account/device/commands", params={ "index": 0, "limit": 1 }) + assert cmd == login.get_a("/account/device/commands", params={ "index": 0, "limit": 1 }) + + first_index = cmd['index'] + cmds = login.get_a("/account/device/commands", params={ "limit": limit, "index": first_index + offset }) + assert cmds['last'] == (offset + limit >= n_cmds) + assert len(cmds['messages']) == min(max(n_cmds - offset, 0), limit) def test_attached_clients_unauth(client): with pytest.raises(ClientError) as e: @@ -308,25 +337,26 @@ def test_attached_clients_unauth(client): 'message': 'invalid request signature' } -def test_attached_clients_unverified(unverified_account): - with pytest.raises(ClientError) as e: - unverified_account.get_a("/account/attached_clients") - assert e.value.details == { - 'code': 400, - 'errno': 138, - 'error': 'Bad Request', - 'message': 'unverified session' - } - -def test_attached_clients_badauth(refresh_token): - with pytest.raises(ClientError) as e: - refresh_token.get_a("/account/attached_clients") - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } +class TestAttachedClientBadauth: + def test_attached_clients_badauth(self, refresh_token): + with pytest.raises(ClientError) as e: + refresh_token.get_a("/account/attached_clients") + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } + + def test_attached_client_destroy_badauth(self, refresh_token): + with pytest.raises(ClientError) as e: + refresh_token.post_a("/account/attached_client/destroy") + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } def test_attached_clients(account, refresh_token, push_server): dev1 = Device(account, "dev1") @@ -366,69 +396,50 @@ def test_attached_client_destroy_unauth(client): 'message': 'invalid request signature' } -def test_attached_client_destroy_unverified(unverified_account): - with pytest.raises(ClientError) as e: - unverified_account.post_a("/account/attached_client/destroy") - assert e.value.details == { - 'code': 400, - 'errno': 138, - 'error': 'Bad Request', - 'message': 'unverified session' - } - -def test_attached_client_destroy_badauth(refresh_token): - with pytest.raises(ClientError) as e: - refresh_token.post_a("/account/attached_client/destroy") - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } - -@pytest.mark.parametrize("args,code,errno,error,message", [ - ({"sessionTokenId": "0"}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({"refreshTokenId": "0"}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({"deviceId": "0"}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({"sessionTokenId": "00" * 16, 'extra': 0}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({"sessionTokenId": "00" * 16, 'refreshTokenId': "00" * 16}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({"sessionTokenId": "00" * 16, 'refreshTokenId': "00" * 16, 'deviceId': "00" * 16}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({'refreshTokenId': "00" * 16, 'deviceId': "00" * 16}, - 400, 107, 'Bad Request', 'invalid parameter in request body'), -]) -def test_attached_client_destroy_invalid(account, args, code, errno, error, message): - with pytest.raises(ClientError) as e: - account.post_a("/account/attached_client/destroy", args) - assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} - -@pytest.mark.parametrize("fn", [ - (lambda d: {'sessionTokenId': d['sessionTokenId']}), - (lambda d: {'refreshTokenId': d['refreshTokenId']}), - (lambda d: {'deviceId': d['deviceId']}), -], ids=["session", "refresh", "device"]) -def test_attached_client_destroy(account, refresh_token, fn): - Device(refresh_token, "dev2") - devs = account.get_a("/account/attached_clients") - account.post_a("/account/attached_client/destroy", fn(devs[0])) - devs2 = account.get_a("/account/attached_clients") - assert len(devs2) == len(devs) - 1 - -def test_attached_client_destroy_push(account, refresh_token, push_server): - dev = Device(account, "dev") - dev2 = Device(refresh_token, "dev2") - dev.update_pcb(push_server.good("4ed8d1d3-e756-4866-9169-aafe612eb1e9")) - account.post_a("/account/attached_client/destroy", { 'deviceId': dev2.id }) - p = push_server.wait() - assert p[0] == "/4ed8d1d3-e756-4866-9169-aafe612eb1e9" - assert dev.decrypt(p[2]) == { - 'command': 'fxaccounts:device_disconnected', - 'data': {'id': dev2.id}, - 'version': 1 - } - assert push_server.done() +class TestAttachedClients: + @pytest.mark.parametrize("args,code,errno,error,message", [ + ({"sessionTokenId": "0"}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({"refreshTokenId": "0"}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({"deviceId": "0"}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({"sessionTokenId": "00" * 16, 'extra': 0}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({"sessionTokenId": "00" * 16, 'refreshTokenId': "00" * 16}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({"sessionTokenId": "00" * 16, 'refreshTokenId': "00" * 16, 'deviceId': "00" * 16}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({'refreshTokenId': "00" * 16, 'deviceId': "00" * 16}, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ]) + def test_attached_client_destroy_invalid(self, account, args, code, errno, error, message): + with pytest.raises(ClientError) as e: + account.post_a("/account/attached_client/destroy", args) + assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} + + @pytest.mark.parametrize("fn", [ + (lambda d: {'sessionTokenId': d['sessionTokenId']}), + (lambda d: {'refreshTokenId': d['refreshTokenId']}), + (lambda d: {'deviceId': d['deviceId']}), + ], ids=["session", "refresh", "device"]) + def test_attached_client_destroy(self, account, refresh_token, fn): + Device(refresh_token, "dev2") + devs = account.get_a("/account/attached_clients") + account.post_a("/account/attached_client/destroy", fn(devs[0])) + devs2 = account.get_a("/account/attached_clients") + assert len(devs2) == len(devs) - 1 + + def test_attached_client_destroy_push(self, account, refresh_token, push_server): + dev = Device(account, "dev") + dev2 = Device(refresh_token, "dev2") + dev.update_pcb(push_server.good("4ed8d1d3-e756-4866-9169-aafe612eb1e9")) + account.post_a("/account/attached_client/destroy", { 'deviceId': dev2.id }) + p = push_server.wait() + assert p[0] == "/4ed8d1d3-e756-4866-9169-aafe612eb1e9" + assert dev.decrypt(p[2]) == { + 'command': 'fxaccounts:device_disconnected', + 'data': {'id': dev2.id}, + 'version': 1 + } + assert push_server.done() diff --git a/tests/test_auth_email.py b/tests/test_auth_email.py index 319f2d4..b7fa65a 100644 --- a/tests/test_auth_email.py +++ b/tests/test_auth_email.py @@ -3,58 +3,60 @@ from fxa.errors import ClientError from api import * -def test_status_noauth(client, refresh_token): - with pytest.raises(ClientError) as e: - client.post_a("/recovery_email/status") - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } - with pytest.raises(ClientError) as e: - refresh_token.post_a("/recovery_email/status") - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } +class TestUnverified: + def test_status_unverified(self, unverified_account): + resp = unverified_account.get_a("/recovery_email/status") + assert resp == { + 'email': unverified_account.email, + 'emailVerified': False, + 'sessionVerified': False, + 'verified': False + } -def test_status_unverified(unverified_account): - resp = unverified_account.get_a("/recovery_email/status") - assert resp == { - 'email': unverified_account.email, - 'emailVerified': False, - 'sessionVerified': False, - 'verified': False - } + @pytest.mark.parametrize("args,code,errno,error,message", [ + ({ 'uid': '00', 'code': "" }, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({ 'id': '00' * 16, 'code': 0 }, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ({ 'id': '00' * 16, 'code': "", 'extra': 0 }, + 400, 107, 'Bad Request', 'invalid parameter in request body'), + ]) + def test_verify_code_invalid(self, unverified_account, args, code, errno, error, message): + with pytest.raises(ClientError) as e: + unverified_account.post_a("/recovery_email/verify_code", args) + assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} -def test_status_verified(account): - resp = account.get_a("/recovery_email/status") - assert resp == { - 'email': account.email, - 'emailVerified': True, - 'sessionVerified': True, - 'verified': True - } +class TestVerified: + def test_status_noauth(self, client, refresh_token): + with pytest.raises(ClientError) as e: + client.post_a("/recovery_email/status") + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } + with pytest.raises(ClientError) as e: + refresh_token.post_a("/recovery_email/status") + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } -@pytest.mark.parametrize("args,code,errno,error,message", [ - ({ 'uid': '00', 'code': "" }, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({ 'id': '00' * 16, 'code': 0 }, - 400, 107, 'Bad Request', 'invalid parameter in request body'), - ({ 'id': '00' * 16, 'code': "", 'extra': 0 }, - 400, 107, 'Bad Request', 'invalid parameter in request body'), -]) -def test_verify_code_invalid(unverified_account, args, code, errno, error, message): - with pytest.raises(ClientError) as e: - unverified_account.post_a("/recovery_email/verify_code", args) - assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} + def test_status_verified(self, account): + resp = account.get_a("/recovery_email/status") + assert resp == { + 'email': account.email, + 'emailVerified': True, + 'sessionVerified': True, + 'verified': True + } -def test_verify_code(account): - # fixture does all the work - pass + def test_verify_code(self, account): + # fixture does all the work + pass def test_verify_code_reuse(client, mail_server): s = client.create_account("test@test", "") diff --git a/tests/test_auth_oauth.py b/tests/test_auth_oauth.py index 569d7ef..9c44f29 100644 --- a/tests/test_auth_oauth.py +++ b/tests/test_auth_oauth.py @@ -3,39 +3,317 @@ import pytest import time from fxa.errors import ClientError -@pytest.mark.parametrize("client_id,scopes", [ - # firefox - ("5882386c6d801776", "profile:write https://identity.mozilla.com/apps/oldsync https://identity.mozilla.com/tokens/session"), - # fenix - ("a2270f727f45f648", "profile https://identity.mozilla.com/apps/oldsync https://identity.mozilla.com/tokens/session"), -]) -def test_oauth_client_scopes(account, client_id, scopes): - body = { - "client_id": client_id, - "ttl": 60, - "grant_type": "fxa-credentials", - "access_type": "online", - "scope": scopes, - } - s = account.post_a("/oauth/token", body)['access_token'] - account.post_a("/oauth/destroy", { "client_id": client_id, "token": s }) +class TestOauth: + @pytest.mark.parametrize("client_id,scopes", [ + # firefox + ("5882386c6d801776", "profile:write https://identity.mozilla.com/apps/oldsync https://identity.mozilla.com/tokens/session"), + # fenix + ("a2270f727f45f648", "profile https://identity.mozilla.com/apps/oldsync https://identity.mozilla.com/tokens/session"), + ]) + def test_oauth_client_scopes(self, account, client_id, scopes): + body = { + "client_id": client_id, + "ttl": 60, + "grant_type": "fxa-credentials", + "access_type": "online", + "scope": scopes, + } + s = account.post_a("/oauth/token", body)['access_token'] + account.post_a("/oauth/destroy", { "client_id": client_id, "token": s }) -def test_oauth_authorization_noauth(account): - body = { - "client_id": "5882386c6d801776", - "ttl": 60, - "grant_type": "fxa-credentials", - "access_type": "online", - "scope": "profile", - } - with pytest.raises(ClientError) as e: - account.post("/oauth/authorization", body) - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } + def test_oauth_authorization_noauth(self, account): + body = { + "client_id": "5882386c6d801776", + "ttl": 60, + "grant_type": "fxa-credentials", + "access_type": "online", + "scope": "profile", + } + with pytest.raises(ClientError) as e: + account.post("/oauth/authorization", body) + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } + + @pytest.mark.parametrize("args", [ + { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "invalid", + "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" }, + { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "online", + "code_challenge": "", "code_challenge_method": "invalid", "response_type": "code" }, + { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "online", + "code_challenge": "", "code_challenge_method": "S256", "response_type": "invalid" }, + ]) + def test_oauth_authorization_invalid(self, account, args): + with pytest.raises(ClientError) as e: + account.post_a("/oauth/authorization", args) + assert e.value.details == { + 'code': 400, + 'errno': 107, + 'error': 'Bad Request', + 'message': 'invalid parameter in request body' + } + + def test_oauth_authorization_badclientid(self, account): + args = { "client_id": "invalid", "state": "", "scope": "profile", "access_type": "online", + "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" } + with pytest.raises(ClientError) as e: + account.post_a("/oauth/authorization", args) + assert e.value.details == { + 'code': 400, + 'errno': 162, + 'error': 'Bad Request', + 'message': 'unknown client_id' + } + + def test_oauth_authorization_badscope(self, account): + args = { "client_id": "5882386c6d801776", "state": "", "scope": "invalid", "access_type": "online", + "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" } + with pytest.raises(ClientError) as e: + account.post_a("/oauth/authorization", args) + assert e.value.details == { + 'code': 400, + 'errno': 169, + 'error': 'Bad Request', + 'message': 'requested scopes not allowed' + } + + # see below for combined /authorization + unauthed /token + + def test_oauth_destroy_notoken(self, account): + args = { "client_id": "5882386c6d801776", "token": "00" * 32 } + with pytest.raises(ClientError) as e: + account.post("/oauth/destroy", args) + assert e.value.details == { + 'code': 400, + 'errno': 107, + 'error': 'Bad Request', + 'message': 'invalid parameter in request body' + } + + @pytest.mark.xfail + def test_oauth_destroy_badclient(self, account, refresh_token): + args = { "client_id": "other", "token": refresh_token.bearer } + with pytest.raises(ClientError) as e: + account.post("/oauth/destroy", args) + assert e.value.details == { + 'code': 400, + 'errno': 107, + 'error': 'Bad Request', + 'message': 'invalid parameter in request body' + } + + def test_oauth_scoped_keys_badclient(self, account): + with pytest.raises(ClientError) as e: + account.post_a("/account/scoped-key-data", { + "client_id": "invalid", + "scope": "https://identity.mozilla.com/apps/oldsync" + }) + assert e.value.details == { + 'code': 400, + 'errno': 162, + 'error': 'Bad Request', + 'message': 'unknown client_id' + } + + def test_oauth_scoped_keys_badscope(self, account): + with pytest.raises(ClientError) as e: + account.post_a("/account/scoped-key-data", { + "client_id": "5882386c6d801776", + "scope": "scope" + }) + assert e.value.details == { + 'code': 400, + 'errno': 169, + 'error': 'Bad Request', + 'message': 'requested scopes not allowed' + } + + def test_oauth_scoped_keys(self, account): + resp = account.post_a("/account/scoped-key-data", { + "client_id": "5882386c6d801776", + "scope": "https://identity.mozilla.com/apps/oldsync" + }) + assert resp == { + "https://identity.mozilla.com/apps/oldsync": { + "identifier": "https://identity.mozilla.com/apps/oldsync", + "keyRotationSecret": "00" * 32, + "keyRotationTimestamp": 0, + }, + } + + @pytest.mark.parametrize("access_type", ["online", "offline"]) + def test_oauth_token_fxa_badclient(self, account, access_type): + body = { "client_id": "invalid", "ttl": 60, "grant_type": "fxa-credentials", + "access_type": access_type, "scope": "profile" } + with pytest.raises(ClientError) as e: + account.post_a("/oauth/token", body) + assert e.value.details == { + 'code': 400, + 'errno': 162, + 'error': 'Bad Request', + 'message': 'unknown client_id' + } + + @pytest.mark.parametrize("access_type", ["online", "offline"]) + def test_oauth_token_fxa_badscope(self, account, access_type): + body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", + "access_type": access_type, "scope": "scope" } + with pytest.raises(ClientError) as e: + account.post_a("/oauth/token", body) + assert e.value.details == { + 'code': 400, + 'errno': 169, + 'error': 'Bad Request', + 'message': 'requested scopes not allowed' + } + + @pytest.mark.parametrize("args,code,error,errno,message", [ + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", + "code": "invalid", "code_verifier": "test" }, + 400, 'Bad Request', 107, 'invalid parameter in request body'), + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", + "code_verifier": "test", "extra": 0 }, + 400, 'Bad Request', 107, 'invalid parameter in request body'), + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", + "code": "00" * 32, "code_verifier": "test" }, + 401, 'Unauthorized', 110, 'invalid authentication token'), + ({ "client_id": "invalid", "ttl": 60, "grant_type": "authorization_code", + "code_verifier": "test" }, + 400, 'Bad Request', 162, 'unknown client_id'), + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", + "code_verifier": "invalid" }, + 400, 'Bad Request', 107, 'invalid parameter in request body'), + ]) + def test_oauth_token_other_invalidcode(self, account, args, code, error, errno, message, auth_code): + args = copy.deepcopy(args) + if 'code' not in args: args['code'] = auth_code + with pytest.raises(ClientError) as e: + account.post("/oauth/token", args) + assert e.value.details == { 'code': code, 'errno': errno, 'error': error, 'message': message } + + @pytest.mark.parametrize("args,code,error,errno,message", [ + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", + "scope": "profile", "access_type": "online" }, + 400, 'Bad Request', 107, 'invalid parameter in request body'), + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", + "refresh_token": "invalid", "code_verifier": "test" }, + 400, 'Bad Request', 107, 'invalid parameter in request body'), + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", + "scope": "profile", "extra": 0 }, + 400, 'Bad Request', 107, 'invalid parameter in request body'), + ({ "client_id": "invalid", "ttl": 60, "grant_type": "refresh_token", + "scope": "profile" }, + 400, 'Bad Request', 162, 'unknown client_id'), + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", + "scope": "foo" }, + 400, 'Bad Request', 169, 'requested scopes not allowed'), + ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", + "scope": "profile:write" }, + 400, 'Bad Request', 169, 'requested scopes not allowed'), + ]) + def test_oauth_token_other_invalidrefresh(self, account, args, code, error, errno, message, refresh_token): + args = copy.deepcopy(args) + if 'refresh_token' not in args: args['refresh_token'] = refresh_token.bearer + with pytest.raises(ClientError) as e: + account.post("/oauth/token", args) + assert e.value.details == { 'code': code, 'errno': errno, 'error': error, 'message': message } + + @pytest.mark.parametrize("refresh", [False, True]) + def test_oauth_fxa(self, account, refresh): + body = { + "client_id": "5882386c6d801776", + "ttl": 60, + "grant_type": "fxa-credentials", + "access_type": "offline" if refresh else "online", + "scope": "profile", + } + resp = account.post_a("/oauth/token", body) + + assert 'access_token' in resp + assert ('refresh_token' in resp) == refresh + assert 'session_token' not in resp + assert resp['scope'] == 'profile' + assert resp['token_type'] == 'bearer' + assert resp['expires_in'] <= 60 + assert (resp['auth_at'] - time.time()) < 10 + assert 'keys_jwe' not in resp + + @pytest.mark.parametrize("keys_jwe", [None, "keyskeyskeys"]) + @pytest.mark.parametrize("refresh,session", [ + (False, False), + (True, False), + (True, True), + ]) + def test_oauth_auth_code(self, account, keys_jwe, refresh, session): + scope = "profile" + (" https://identity.mozilla.com/tokens/session" if session else "") + body = { + "client_id": "5882386c6d801776", + "state": "test", + "keys_jwe": keys_jwe, + "scope": scope, + "access_type": "offline" if refresh else "online", + "code_challenge": "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg", # "test" + "code_challenge_method": "S256", + "response_type": "code", + } + resp = account.post_a("/oauth/authorization", body) + assert resp['state'] == "test" + + body = { + "client_id": "5882386c6d801776", + "ttl": 60, + "grant_type": "authorization_code", + "code": resp['code'], + "code_verifier": "test", + } + resp = account.post("/oauth/token", body) + assert 'access_token' in resp + assert ('refresh_token' in resp) == refresh + assert ('session_token' in resp) == session + assert resp['scope'] == 'profile' + assert resp['token_type'] == 'bearer' + assert resp['expires_in'] <= 60 + assert (resp['auth_at'] - time.time()) < 10 + assert keys_jwe is None or (resp['keys_jwe'] == keys_jwe) + + def test_oauth_refresh(self, account, refresh_token): + body = { + "client_id": "5882386c6d801776", + "ttl": 60, + "grant_type": "refresh_token", + "refresh_token": refresh_token.bearer, + "scope": "profile", + } + resp = account.post("/oauth/token", body) + + assert 'access_token' in resp + assert 'refresh_token' not in resp + assert 'session_token' not in resp + assert resp['scope'] == 'profile' + assert resp['token_type'] == 'bearer' + assert resp['expires_in'] <= 60 + assert (resp['auth_at'] - time.time()) < 10 + assert 'keys_jwe' not in resp + + @pytest.mark.parametrize("grant_type,access_type", [ + ("authorization_code", "online"), + ("refresh_token", "online"), + ("fxa-credentials", "foo"), + ]) + def test_oauth_token_fxa_invalid(self, account, grant_type, access_type): + body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": grant_type, + "access_type": access_type, "scope": "scope" } + with pytest.raises(ClientError) as e: + account.post_a("/oauth/token", body) + assert e.value.details == { + 'code': 400, + 'errno': 107, + 'error': 'Bad Request', + 'message': 'invalid parameter in request body' + } def test_oauth_authorization_unverified(unverified_account): body = { @@ -54,99 +332,6 @@ def test_oauth_authorization_unverified(unverified_account): 'message': 'unverified session' } -@pytest.mark.parametrize("args", [ - { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "invalid", - "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" }, - { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "online", - "code_challenge": "", "code_challenge_method": "invalid", "response_type": "code" }, - { "client_id": "5882386c6d801776", "state": "", "scope": "profile", "access_type": "online", - "code_challenge": "", "code_challenge_method": "S256", "response_type": "invalid" }, -]) -def test_oauth_authorization_invalid(account, args): - with pytest.raises(ClientError) as e: - account.post_a("/oauth/authorization", args) - assert e.value.details == { - 'code': 400, - 'errno': 107, - 'error': 'Bad Request', - 'message': 'invalid parameter in request body' - } - -def test_oauth_authorization_badclientid(account): - args = { "client_id": "invalid", "state": "", "scope": "profile", "access_type": "online", - "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" } - with pytest.raises(ClientError) as e: - account.post_a("/oauth/authorization", args) - assert e.value.details == { - 'code': 400, - 'errno': 162, - 'error': 'Bad Request', - 'message': 'unknown client_id' - } - -def test_oauth_authorization_badscope(account): - args = { "client_id": "5882386c6d801776", "state": "", "scope": "invalid", "access_type": "online", - "code_challenge": "", "code_challenge_method": "S256", "response_type": "code" } - with pytest.raises(ClientError) as e: - account.post_a("/oauth/authorization", args) - assert e.value.details == { - 'code': 400, - 'errno': 169, - 'error': 'Bad Request', - 'message': 'requested scopes not allowed' - } - -# see below for combined /authorization + unauthed /token - -def test_oauth_destroy_notoken(account): - args = { "client_id": "5882386c6d801776", "token": "00" * 32 } - with pytest.raises(ClientError) as e: - account.post("/oauth/destroy", args) - assert e.value.details == { - 'code': 400, - 'errno': 107, - 'error': 'Bad Request', - 'message': 'invalid parameter in request body' - } - -@pytest.mark.xfail -def test_oauth_destroy_badclient(account, refresh_token): - args = { "client_id": "other", "token": refresh_token.bearer } - with pytest.raises(ClientError) as e: - account.post("/oauth/destroy", args) - assert e.value.details == { - 'code': 400, - 'errno': 107, - 'error': 'Bad Request', - 'message': 'invalid parameter in request body' - } - -def test_oauth_scoped_keys_badclient(account): - with pytest.raises(ClientError) as e: - account.post_a("/account/scoped-key-data", { - "client_id": "invalid", - "scope": "https://identity.mozilla.com/apps/oldsync" - }) - assert e.value.details == { - 'code': 400, - 'errno': 162, - 'error': 'Bad Request', - 'message': 'unknown client_id' - } - -def test_oauth_scoped_keys_badscope(account): - with pytest.raises(ClientError) as e: - account.post_a("/account/scoped-key-data", { - "client_id": "5882386c6d801776", - "scope": "scope" - }) - assert e.value.details == { - 'code': 400, - 'errno': 169, - 'error': 'Bad Request', - 'message': 'requested scopes not allowed' - } - def test_oauth_scoped_unverified(unverified_account): with pytest.raises(ClientError) as e: unverified_account.post_a("/account/scoped-key-data", { @@ -160,62 +345,6 @@ def test_oauth_scoped_unverified(unverified_account): 'message': 'unverified session' } -def test_oauth_scoped_keys(account): - resp = account.post_a("/account/scoped-key-data", { - "client_id": "5882386c6d801776", - "scope": "https://identity.mozilla.com/apps/oldsync" - }) - assert resp == { - "https://identity.mozilla.com/apps/oldsync": { - "identifier": "https://identity.mozilla.com/apps/oldsync", - "keyRotationSecret": "00" * 32, - "keyRotationTimestamp": 0, - }, - } - -@pytest.mark.parametrize("access_type", ["online", "offline"]) -def test_oauth_token_fxa_badclient(account, access_type): - body = { "client_id": "invalid", "ttl": 60, "grant_type": "fxa-credentials", - "access_type": access_type, "scope": "profile" } - with pytest.raises(ClientError) as e: - account.post_a("/oauth/token", body) - assert e.value.details == { - 'code': 400, - 'errno': 162, - 'error': 'Bad Request', - 'message': 'unknown client_id' - } - -@pytest.mark.parametrize("access_type", ["online", "offline"]) -def test_oauth_token_fxa_badscope(account, access_type): - body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", - "access_type": access_type, "scope": "scope" } - with pytest.raises(ClientError) as e: - account.post_a("/oauth/token", body) - assert e.value.details == { - 'code': 400, - 'errno': 169, - 'error': 'Bad Request', - 'message': 'requested scopes not allowed' - } - -@pytest.mark.parametrize("grant_type,access_type", [ - ("authorization_code", "online"), - ("refresh_token", "online"), - ("fxa-credentials", "foo"), -]) -def test_oauth_token_fxa_invalid(account, grant_type, access_type): - body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": grant_type, - "access_type": access_type, "scope": "scope" } - with pytest.raises(ClientError) as e: - account.post_a("/oauth/token", body) - assert e.value.details == { - 'code': 400, - 'errno': 107, - 'error': 'Bad Request', - 'message': 'invalid parameter in request body' - } - def test_oauth_token_unverified(unverified_account): body = { "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", "access_type": "online", "scope": "profile" } @@ -240,131 +369,3 @@ def auth_code(account): "response_type": "code", } return account.post_a("/oauth/authorization", body)['code'] - -@pytest.mark.parametrize("args,code,error,errno,message", [ - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", - "code": "invalid", "code_verifier": "test" }, - 400, 'Bad Request', 107, 'invalid parameter in request body'), - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", - "code_verifier": "test", "extra": 0 }, - 400, 'Bad Request', 107, 'invalid parameter in request body'), - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", - "code": "00" * 32, "code_verifier": "test" }, - 401, 'Unauthorized', 110, 'invalid authentication token'), - ({ "client_id": "invalid", "ttl": 60, "grant_type": "authorization_code", - "code_verifier": "test" }, - 400, 'Bad Request', 162, 'unknown client_id'), - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "authorization_code", - "code_verifier": "invalid" }, - 400, 'Bad Request', 107, 'invalid parameter in request body'), -]) -def test_oauth_token_other_invalidcode(account, args, code, error, errno, message, auth_code): - args = copy.deepcopy(args) - if 'code' not in args: args['code'] = auth_code - with pytest.raises(ClientError) as e: - account.post("/oauth/token", args) - assert e.value.details == { 'code': code, 'errno': errno, 'error': error, 'message': message } - -@pytest.mark.parametrize("args,code,error,errno,message", [ - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "fxa-credentials", - "scope": "profile", "access_type": "online" }, - 400, 'Bad Request', 107, 'invalid parameter in request body'), - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", - "refresh_token": "invalid", "code_verifier": "test" }, - 400, 'Bad Request', 107, 'invalid parameter in request body'), - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", - "scope": "profile", "extra": 0 }, - 400, 'Bad Request', 107, 'invalid parameter in request body'), - ({ "client_id": "invalid", "ttl": 60, "grant_type": "refresh_token", - "scope": "profile" }, - 400, 'Bad Request', 162, 'unknown client_id'), - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", - "scope": "foo" }, - 400, 'Bad Request', 169, 'requested scopes not allowed'), - ({ "client_id": "5882386c6d801776", "ttl": 60, "grant_type": "refresh_token", - "scope": "profile:write" }, - 400, 'Bad Request', 169, 'requested scopes not allowed'), -]) -def test_oauth_token_other_invalidrefresh(account, args, code, error, errno, message, refresh_token): - args = copy.deepcopy(args) - if 'refresh_token' not in args: args['refresh_token'] = refresh_token.bearer - with pytest.raises(ClientError) as e: - account.post("/oauth/token", args) - assert e.value.details == { 'code': code, 'errno': errno, 'error': error, 'message': message } - -@pytest.mark.parametrize("refresh", [False, True]) -def test_oauth_fxa(account, refresh): - body = { - "client_id": "5882386c6d801776", - "ttl": 60, - "grant_type": "fxa-credentials", - "access_type": "offline" if refresh else "online", - "scope": "profile", - } - resp = account.post_a("/oauth/token", body) - - assert 'access_token' in resp - assert ('refresh_token' in resp) == refresh - assert 'session_token' not in resp - assert resp['scope'] == 'profile' - assert resp['token_type'] == 'bearer' - assert resp['expires_in'] <= 60 - assert (resp['auth_at'] - time.time()) < 10 - assert 'keys_jwe' not in resp - -@pytest.mark.parametrize("keys_jwe", [None, "keyskeyskeys"]) -@pytest.mark.parametrize("refresh,session", [ - (False, False), - (True, False), - (True, True), -]) -def test_oauth_auth_code(account, keys_jwe, refresh, session): - scope = "profile" + (" https://identity.mozilla.com/tokens/session" if session else "") - body = { - "client_id": "5882386c6d801776", - "state": "test", - "keys_jwe": keys_jwe, - "scope": scope, - "access_type": "offline" if refresh else "online", - "code_challenge": "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg", # "test" - "code_challenge_method": "S256", - "response_type": "code", - } - resp = account.post_a("/oauth/authorization", body) - assert resp['state'] == "test" - - body = { - "client_id": "5882386c6d801776", - "ttl": 60, - "grant_type": "authorization_code", - "code": resp['code'], - "code_verifier": "test", - } - resp = account.post("/oauth/token", body) - assert 'access_token' in resp - assert ('refresh_token' in resp) == refresh - assert ('session_token' in resp) == session - assert resp['scope'] == 'profile' - assert resp['token_type'] == 'bearer' - assert resp['expires_in'] <= 60 - assert (resp['auth_at'] - time.time()) < 10 - assert keys_jwe is None or (resp['keys_jwe'] == keys_jwe) - -def test_oauth_refresh(account, refresh_token): - body = { - "client_id": "5882386c6d801776", - "ttl": 60, - "grant_type": "refresh_token", - "refresh_token": refresh_token.bearer, - "scope": "profile", - } - resp = account.post("/oauth/token", body) - - assert 'access_token' in resp - assert 'refresh_token' not in resp - assert 'session_token' not in resp - assert resp['scope'] == 'profile' - assert resp['token_type'] == 'bearer' - assert resp['expires_in'] <= 60 - assert (resp['auth_at'] - time.time()) < 10 - assert 'keys_jwe' not in resp diff --git a/tests/test_auth_password.py b/tests/test_auth_password.py index 7c2064a..fc7bb2b 100644 --- a/tests/test_auth_password.py +++ b/tests/test_auth_password.py @@ -4,38 +4,81 @@ from fxa.errors import ClientError from api import * -@pytest.mark.parametrize("args", [ - { 'email': "", 'oldAuthPW': '00' * 32 }, - { 'email': "test0@test", 'oldAuthPW': '00' }, - { 'email': "test0@test", 'oldAuthPW': '00' * 32, 'extra': 0 }, -]) -def test_change_start_invalid(account, args): - with pytest.raises(ClientError) as e: - account.post_a("/password/change/start", args) - assert e.value.details == { - 'code': 400, - 'errno': 107, - 'error': 'Bad Request', - 'message': 'invalid parameter in request body' - } - -def test_change_start_badaccount(account): - with pytest.raises(ClientError) as e: - account.post_a("/password/change/start", { 'email': "test0@test", 'oldAuthPW': '00' * 32 }) - assert e.value.details == { - 'code': 400, - 'errno': 102, - 'error': 'Bad Request', - 'message': 'unknown account' - } - with pytest.raises(ClientError) as e: - account.post_a("/password/change/start", { 'email': account.email.upper(), 'oldAuthPW': '00' * 32 }) - assert e.value.details == { - 'code': 400, - 'errno': 120, - 'error': 'Bad Request', - 'message': 'incorrect email case' - } +class TestPasswordInvalid: + @pytest.mark.parametrize("args", [ + { 'email': "", 'oldAuthPW': '00' * 32 }, + { 'email': "test0@test", 'oldAuthPW': '00' }, + { 'email': "test0@test", 'oldAuthPW': '00' * 32, 'extra': 0 }, + ]) + def test_change_start_invalid(self, account, args): + with pytest.raises(ClientError) as e: + account.post_a("/password/change/start", args) + assert e.value.details == { + 'code': 400, + 'errno': 107, + 'error': 'Bad Request', + 'message': 'invalid parameter in request body' + } + + def test_change_start_badaccount(self, account): + with pytest.raises(ClientError) as e: + account.post_a("/password/change/start", { 'email': "test0@test", 'oldAuthPW': '00' * 32 }) + assert e.value.details == { + 'code': 400, + 'errno': 102, + 'error': 'Bad Request', + 'message': 'unknown account' + } + with pytest.raises(ClientError) as e: + account.post_a("/password/change/start", { 'email': account.email.upper(), 'oldAuthPW': '00' * 32 }) + assert e.value.details == { + 'code': 400, + 'errno': 120, + 'error': 'Bad Request', + 'message': 'incorrect email case' + } + + def test_change_start_badpw(self, account): + with pytest.raises(ClientError) as e: + account.post_a("/password/change/start", { 'email': account.email, 'oldAuthPW': '00' * 32 }) + assert e.value.details == { + 'code': 400, + 'errno': 103, + 'error': 'Bad Request', + 'message': 'incorrect password' + } + + @pytest.mark.parametrize("args", [ + { 'email': "" }, + { 'email': "test0@test", 'extra': 0 }, + ]) + def test_forgot_start_invalid(self, account, args): + with pytest.raises(ClientError) as e: + account.post_a("/password/forgot/send_code", args) + assert e.value.details == { + 'code': 400, + 'errno': 107, + 'error': 'Bad Request', + 'message': 'invalid parameter in request body' + } + + def test_change_forgot_badaccount(self, account): + with pytest.raises(ClientError) as e: + account.post_a("/password/forgot/send_code", { 'email': "test0@test" }) + assert e.value.details == { + 'code': 400, + 'errno': 102, + 'error': 'Bad Request', + 'message': 'unknown account' + } + with pytest.raises(ClientError) as e: + account.post_a("/password/forgot/send_code", { 'email': account.email.upper() }) + assert e.value.details == { + 'code': 400, + 'errno': 120, + 'error': 'Bad Request', + 'message': 'incorrect email case' + } def test_change_start_unverified(unverified_account): with pytest.raises(ClientError) as e: @@ -50,16 +93,6 @@ def test_change_start_unverified(unverified_account): 'message': 'unverified account' } -def test_change_start_badpw(account): - with pytest.raises(ClientError) as e: - account.post_a("/password/change/start", { 'email': account.email, 'oldAuthPW': '00' * 32 }) - assert e.value.details == { - 'code': 400, - 'errno': 103, - 'error': 'Bad Request', - 'message': 'incorrect password' - } - @pytest.fixture def change_token(account): pw = auth_pw(account.email, "") @@ -67,20 +100,21 @@ def change_token(account): assert 'keyFetchToken' in resp return PasswordChange(account.client, resp['passwordChangeToken']) -@pytest.mark.parametrize("args", [ - { 'authPW': '00', 'wrapKb': '00' * 32, 'sessionToken': '00' * 32, }, - { 'authPW': '00' * 32, 'wrapKb': '00', 'sessionToken': '00' * 32, }, - { 'authPW': '00' * 32, 'wrapKb': '00' * 32, 'sessionToken': '00', }, -]) -def test_change_finish_invalid(change_token, args): - with pytest.raises(ClientError) as e: - change_token.post_a("/password/change/finish", args) - assert e.value.details == { - 'code': 400, - 'errno': 107, - 'error': 'Bad Request', - 'message': 'invalid parameter in request body' - } +class TestChangeInvalid: + @pytest.mark.parametrize("args", [ + { 'authPW': '00', 'wrapKb': '00' * 32, 'sessionToken': '00' * 32, }, + { 'authPW': '00' * 32, 'wrapKb': '00', 'sessionToken': '00' * 32, }, + { 'authPW': '00' * 32, 'wrapKb': '00' * 32, 'sessionToken': '00', }, + ]) + def test_change_finish_invalid(self, change_token, args): + with pytest.raises(ClientError) as e: + change_token.post_a("/password/change/finish", args) + assert e.value.details == { + 'code': 400, + 'errno': 107, + 'error': 'Bad Request', + 'message': 'invalid parameter in request body' + } def test_change_finish(account, change_token, mail_server): pw = auth_pw(account.email, "new") @@ -124,38 +158,6 @@ def test_change_finish_twice(account, change_token, mail_server): 'message': 'invalid request signature' } -@pytest.mark.parametrize("args", [ - { 'email': "" }, - { 'email': "test0@test", 'extra': 0 }, -]) -def test_forgot_start_invalid(account, args): - with pytest.raises(ClientError) as e: - account.post_a("/password/forgot/send_code", args) - assert e.value.details == { - 'code': 400, - 'errno': 107, - 'error': 'Bad Request', - 'message': 'invalid parameter in request body' - } - -def test_change_forgot_badaccount(account): - with pytest.raises(ClientError) as e: - account.post_a("/password/forgot/send_code", { 'email': "test0@test" }) - assert e.value.details == { - 'code': 400, - 'errno': 102, - 'error': 'Bad Request', - 'message': 'unknown account' - } - with pytest.raises(ClientError) as e: - account.post_a("/password/forgot/send_code", { 'email': account.email.upper() }) - assert e.value.details == { - 'code': 400, - 'errno': 120, - 'error': 'Bad Request', - 'message': 'incorrect email case' - } - def test_change_forgot_unverified(unverified_account): with pytest.raises(ClientError) as e: unverified_account.post_a("/password/forgot/send_code", { 'email': unverified_account.email }) @@ -180,8 +182,6 @@ def test_forgot_finish_invalid(change_token, args): } def test_forgot_finish_badcode(account, forgot_token, mail_server): - (to, body) = mail_server.wait() - assert account.email in to with pytest.raises(ClientError) as e: resp = forgot_token.post_a("/password/forgot/verify_code", { 'code': '' }) assert e.value.details == { @@ -192,17 +192,14 @@ def test_forgot_finish_badcode(account, forgot_token, mail_server): } def test_forgot_finish(account, forgot_token, mail_server): - (to, body) = mail_server.wait() - assert account.email in to - resp = forgot_token.post_a("/password/forgot/verify_code", { 'code': body.strip() }) + resp = forgot_token.post_a("/password/forgot/verify_code", { 'code': forgot_token.code }) assert 'accountResetToken' in resp def test_forgot_finish_twice(account, forgot_token, mail_server): - (to, body) = mail_server.wait() - forgot_token.post_a("/password/forgot/verify_code", { 'code': body.strip() }) + forgot_token.post_a("/password/forgot/verify_code", { 'code': forgot_token.code }) with pytest.raises(ClientError) as e: - forgot_token.post_a("/password/forgot/verify_code", { 'code': body.strip() }) + forgot_token.post_a("/password/forgot/verify_code", { 'code': forgot_token.code }) assert e.value.details == { 'code': 401, 'errno': 109, diff --git a/tests/test_auth_session.py b/tests/test_auth_session.py index 3a6e7c4..bcdd0bc 100644 --- a/tests/test_auth_session.py +++ b/tests/test_auth_session.py @@ -13,52 +13,53 @@ def test_session_loggedout(client): 'message': 'invalid request signature' } -def test_status(account): - resp = account.get_a("/session/status") - assert resp == { 'state': '', 'uid': account.props['uid'] } +class TestSession: + def test_status(self, account): + resp = account.get_a("/session/status") + assert resp == { 'state': '', 'uid': account.props['uid'] } -def test_resend(account, mail_server): - c = account.login(account.email, "") - (to, body) = mail_server.wait() - assert to == [account.email] - c.post_a("/session/resend_code", {}) - (to2, body2) = mail_server.wait() - assert to == to2 - assert body == body2 + def test_resend(self, account, mail_server): + c = account.login(account.email, "") + (to, body) = mail_server.wait() + assert to == [account.email] + c.post_a("/session/resend_code", {}) + (to2, body2) = mail_server.wait() + assert to == to2 + assert body == body2 -@pytest.mark.parametrize("args", [ - { 'custom_session_id': '00' }, - { 'extra': '00' }, -]) -def test_session_invalid(account, args): - with pytest.raises(ClientError) as e: - account.post_a("/session/destroy", args) - assert e.value.details == { - 'code': 400, - 'errno': 107, - 'error': 'Bad Request', - 'message': 'invalid parameter in request body' - } + @pytest.mark.parametrize("args", [ + { 'custom_session_id': '00' }, + { 'extra': '00' }, + ]) + def test_session_invalid(self, account, args): + with pytest.raises(ClientError) as e: + account.post_a("/session/destroy", args) + assert e.value.details == { + 'code': 400, + 'errno': 107, + 'error': 'Bad Request', + 'message': 'invalid parameter in request body' + } -def test_session_noid(account): - with pytest.raises(ClientError) as e: - account.post_a("/session/destroy", { 'custom_session_id': '0' * 64 }) - assert e.value.details == { - 'code': 400, - 'errno': 123, - 'error': 'Bad Request', - 'message': 'unknown device' - } + def test_session_noid(self, account): + with pytest.raises(ClientError) as e: + account.post_a("/session/destroy", { 'custom_session_id': '0' * 64 }) + assert e.value.details == { + 'code': 400, + 'errno': 123, + 'error': 'Bad Request', + 'message': 'unknown device' + } -def test_session_destroy_other(account, account2): - with pytest.raises(ClientError) as e: - account.post_a("/session/destroy", { 'custom_session_id': account2.auth.id }) - assert e.value.details == { - 'code': 400, - 'errno': 123, - 'error': 'Bad Request', - 'message': 'unknown device' - } + def test_session_destroy_other(self, account, account2): + with pytest.raises(ClientError) as e: + account.post_a("/session/destroy", { 'custom_session_id': account2.auth.id }) + assert e.value.details == { + 'code': 400, + 'errno': 123, + 'error': 'Bad Request', + 'message': 'unknown device' + } def test_session_destroy_unverified(unverified_account): unverified_account.destroy_session() diff --git a/tests/test_oauth.py b/tests/test_oauth.py index 3eb32ac..d809113 100644 --- a/tests/test_oauth.py +++ b/tests/test_oauth.py @@ -32,66 +32,67 @@ def test_destroy_invalid(oauth, args, code, errno, error, message): oauth.post("/destroy", args) assert e.value.details == {'code': code, 'errno': errno, 'error': error, 'message': message} -def test_destroy_access(oauth, access_token): - oauth.post("/verify", {'token': access_token}) - oauth.post("/destroy", {'access_token': access_token}) - with pytest.raises(ClientError) as e: +class TestOauth: + def test_destroy_access(self, oauth, access_token): oauth.post("/verify", {'token': access_token}) - assert e.value.details == { - 'code': 400, - 'errno': 109, - 'error': 'Bad Request', - 'message': 'invalid request parameter' - } + oauth.post("/destroy", {'access_token': access_token}) + with pytest.raises(ClientError) as e: + oauth.post("/verify", {'token': access_token}) + assert e.value.details == { + 'code': 400, + 'errno': 109, + 'error': 'Bad Request', + 'message': 'invalid request parameter' + } -def test_destroy_refresh(oauth, refresh_token): - refresh_token.get_a("/account/devices") - oauth.post("/destroy", {'refresh_token': refresh_token.bearer}) - with pytest.raises(ClientError) as e: + def test_destroy_refresh(self, oauth, refresh_token): refresh_token.get_a("/account/devices") - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } + oauth.post("/destroy", {'refresh_token': refresh_token.bearer}) + with pytest.raises(ClientError) as e: + refresh_token.get_a("/account/devices") + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } -def test_destroy_any(oauth, access_token, refresh_token): - oauth.post("/verify", {'token': access_token}) - oauth.post("/destroy", {'token': access_token}) - with pytest.raises(ClientError) as e: + def test_destroy_any(self, oauth, access_token, refresh_token): oauth.post("/verify", {'token': access_token}) - assert e.value.details == { - 'code': 400, - 'errno': 109, - 'error': 'Bad Request', - 'message': 'invalid request parameter' - } + oauth.post("/destroy", {'token': access_token}) + with pytest.raises(ClientError) as e: + oauth.post("/verify", {'token': access_token}) + assert e.value.details == { + 'code': 400, + 'errno': 109, + 'error': 'Bad Request', + 'message': 'invalid request parameter' + } - refresh_token.get_a("/account/devices") - oauth.post("/destroy", {'token': refresh_token.bearer}) - with pytest.raises(ClientError) as e: refresh_token.get_a("/account/devices") - assert e.value.details == { - 'code': 401, - 'errno': 109, - 'error': 'Unauthorized', - 'message': 'invalid request signature' - } + oauth.post("/destroy", {'token': refresh_token.bearer}) + with pytest.raises(ClientError) as e: + refresh_token.get_a("/account/devices") + assert e.value.details == { + 'code': 401, + 'errno': 109, + 'error': 'Unauthorized', + 'message': 'invalid request signature' + } -def test_oauth_verify(account, oauth, access_token): - assert oauth.post("/verify", {'token': access_token}) == { - 'user': account.props['uid'], - 'client_id': "5882386c6d801776", - 'scope': ['profile'], - } + def test_oauth_verify(self, account, oauth, access_token): + assert oauth.post("/verify", {'token': access_token}) == { + 'user': account.props['uid'], + 'client_id': "5882386c6d801776", + 'scope': ['profile'], + } -def test_oauth_verify_refresh(oauth, refresh_token): - with pytest.raises(ClientError) as e: - oauth.post("/verify", {'token': refresh_token.bearer}) - assert e.value.details == { - 'code': 400, - 'errno': 109, - 'error': 'Bad Request', - 'message': 'invalid request parameter' - } + def test_oauth_verify_refresh(self, oauth, refresh_token): + with pytest.raises(ClientError) as e: + oauth.post("/verify", {'token': refresh_token.bearer}) + assert e.value.details == { + 'code': 400, + 'errno': 109, + 'error': 'Bad Request', + 'message': 'invalid request parameter' + } diff --git a/tests/test_profile.py b/tests/test_profile.py index 1e21ecc..13dc8b4 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -7,128 +7,129 @@ from api import * def profile(account): return account.profile() -def test_profile_noauth(profile): - with pytest.raises(ClientError) as e: - profile.get("/profile") - assert e.value.details == { - 'code': 403, - 'errno': 100, - 'error': 'Forbidden', - 'message': 'unauthorized' - } +class TestProfile: + def test_profile_noauth(self, profile): + with pytest.raises(ClientError) as e: + profile.get("/profile") + assert e.value.details == { + 'code': 403, + 'errno': 100, + 'error': 'Forbidden', + 'message': 'unauthorized' + } -def test_display_name_noauth(profile): - with pytest.raises(ClientError) as e: - profile.post("/display_name", {'displayName': 'foo'}) - assert e.value.details == { - 'code': 403, - 'errno': 100, - 'error': 'Forbidden', - 'message': 'unauthorized' - } + def test_display_name_noauth(self, profile): + with pytest.raises(ClientError) as e: + profile.post("/display_name", {'displayName': 'foo'}) + assert e.value.details == { + 'code': 403, + 'errno': 100, + 'error': 'Forbidden', + 'message': 'unauthorized' + } -def test_avatar_upload_noauth(profile): - with pytest.raises(ClientError) as e: - profile.post("/avatar/upload", "foo", headers={'content-type': 'image/png'}) - assert e.value.details == { - 'code': 403, - 'errno': 100, - 'error': 'Forbidden', - 'message': 'unauthorized' - } + def test_avatar_upload_noauth(self, profile): + with pytest.raises(ClientError) as e: + profile.post("/avatar/upload", "foo", headers={'content-type': 'image/png'}) + assert e.value.details == { + 'code': 403, + 'errno': 100, + 'error': 'Forbidden', + 'message': 'unauthorized' + } -def test_avatar_delete_noauth(profile): - with pytest.raises(ClientError) as e: - profile.delete("/avatar/00000000000000000000000000000000") - assert e.value.details == { - 'code': 403, - 'errno': 100, - 'error': 'Forbidden', - 'message': 'unauthorized' - } + def test_avatar_delete_noauth(self, profile): + with pytest.raises(ClientError) as e: + profile.delete("/avatar/00000000000000000000000000000000") + assert e.value.details == { + 'code': 403, + 'errno': 100, + 'error': 'Forbidden', + 'message': 'unauthorized' + } -def test_profile(account, profile): - resp = profile.get_a("/profile") - assert resp == { - 'amrValues': None, - 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', - 'avatarDefault': True, - 'displayName': None, - 'email': account.email, - 'locale': None, - 'subscriptions': None, - 'twoFactorAuthentication': False, - 'uid': account.props['uid'] - } + def test_profile(self, account, profile): + resp = profile.get_a("/profile") + assert resp == { + 'amrValues': None, + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', + 'avatarDefault': True, + 'displayName': None, + 'email': account.email, + 'locale': None, + 'subscriptions': None, + 'twoFactorAuthentication': False, + 'uid': account.props['uid'] + } -def test_display_name(account, profile): - resp = profile.get_a("/profile") - assert resp == { - 'amrValues': None, - 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', - 'avatarDefault': True, - 'displayName': None, - 'email': account.email, - 'locale': None, - 'subscriptions': None, - 'twoFactorAuthentication': False, - 'uid': account.props['uid'] - } - profile.post_a("/display_name", {'displayName': 'foo'}) - resp = profile.get_a("/profile") - assert resp == { - 'amrValues': None, - 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', - 'avatarDefault': True, - 'displayName': 'foo', - 'email': account.email, - 'locale': None, - 'subscriptions': None, - 'twoFactorAuthentication': False, - 'uid': account.props['uid'] - } + def test_display_name(self, account, profile): + resp = profile.get_a("/profile") + assert resp == { + 'amrValues': None, + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', + 'avatarDefault': True, + 'displayName': None, + 'email': account.email, + 'locale': None, + 'subscriptions': None, + 'twoFactorAuthentication': False, + 'uid': account.props['uid'] + } + profile.post_a("/display_name", {'displayName': 'foo'}) + resp = profile.get_a("/profile") + assert resp == { + 'amrValues': None, + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', + 'avatarDefault': True, + 'displayName': 'foo', + 'email': account.email, + 'locale': None, + 'subscriptions': None, + 'twoFactorAuthentication': False, + 'uid': account.props['uid'] + } -def test_avatar(account, profile): - resp = profile.get_a("/avatar") - assert resp == { - 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', - 'avatarDefault': True, - 'id': '00000000000000000000000000000000' - } + def test_avatar(self, account, profile): + resp = profile.get_a("/avatar") + assert resp == { + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', + 'avatarDefault': True, + 'id': '00000000000000000000000000000000' + } -def test_avatar_upload(account, profile): - # server does not parse the bytes - profile.post_a("/avatar/upload", "foo", headers={'content-type': 'image/png'}) - resp = profile.get_a("/avatar") - new_id = resp['id'] - assert resp['avatar'] != f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000' - assert not resp['avatarDefault'] - assert resp['id'] != '00000000000000000000000000000000' - resp = profile.get_a("/profile") - assert resp['avatar'] != f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000' - assert not resp['avatarDefault'] + def test_avatar_upload(self, account, profile): + # server does not parse the bytes + profile.post_a("/avatar/upload", "foo", headers={'content-type': 'image/png'}) + resp = profile.get_a("/avatar") + new_id = resp['id'] + assert resp['avatar'] != f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000' + assert not resp['avatarDefault'] + assert resp['id'] != '00000000000000000000000000000000' + resp = profile.get_a("/profile") + assert resp['avatar'] != f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000' + assert not resp['avatarDefault'] -def test_avatar_delete(account, profile): - # server does not parse the bytes - profile.post_a("/avatar/upload", "foo", headers={'content-type': 'image/png'}) - resp = profile.get_a("/avatar") - new_id = resp['id'] - profile.delete_a(f"/avatar/{new_id}") - resp = profile.get_a("/avatar") - assert resp == { - 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', - 'avatarDefault': True, - 'id': '00000000000000000000000000000000' - } - resp = profile.get_a("/profile") - assert resp == { - 'amrValues': None, - 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', - 'avatarDefault': True, - 'displayName': None, - 'email': account.email, - 'locale': None, - 'subscriptions': None, - 'twoFactorAuthentication': False, - 'uid': account.props['uid'] - } + def test_avatar_delete(self, account, profile): + # server does not parse the bytes + profile.post_a("/avatar/upload", "foo", headers={'content-type': 'image/png'}) + resp = profile.get_a("/avatar") + new_id = resp['id'] + profile.delete_a(f"/avatar/{new_id}") + resp = profile.get_a("/avatar") + assert resp == { + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', + 'avatarDefault': True, + 'id': '00000000000000000000000000000000' + } + resp = profile.get_a("/profile") + assert resp == { + 'amrValues': None, + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', + 'avatarDefault': True, + 'displayName': resp['displayName'], # ignore this field + 'email': account.email, + 'locale': None, + 'subscriptions': None, + 'twoFactorAuthentication': False, + 'uid': account.props['uid'] + } diff --git a/tests/test_push.py b/tests/test_push.py index e6d732a..37688ad 100644 --- a/tests/test_push.py +++ b/tests/test_push.py @@ -2,32 +2,38 @@ import pytest from api import * -def test_account_destroy(account, push_server): - dev = Device(account, "dev", pcb=push_server.good("09d0114e-3b23-4ba3-8474-efac7433e3ba")) - account.destroy_account(account.email, "") - p = push_server.wait() - assert p[0] == "/09d0114e-3b23-4ba3-8474-efac7433e3ba" - assert dev.decrypt(p[2]) == { - 'command': 'fxaccounts:account_destroyed', - 'data': {'uid': account.props['uid']}, - 'version': 1, - } - assert push_server.done() +# scope each test to not leak devices into the others -def test_account_verify(client, push_server, mail_server): - account = client.create_account("test@test", "") - try: - (to, body) = mail_server.wait() - assert to == ["test@test"] - data = json.loads(base64.urlsafe_b64decode(body.split("#/verify/", maxsplit=1)[1]).decode('utf8')) - dev = Device(account, "dev", pcb=push_server.good("8accbe08-4040-44c2-8fd9-cf2669b56cb1")) - account.post_a('/recovery_email/verify_code', { 'uid': data['uid'], 'code': data['code'] }) +class TestAccountDestroy: + def test_account_destroy(self, account, push_server): + dev = Device(account, "dev", pcb=push_server.good("09d0114e-3b23-4ba3-8474-efac7433e3ba")) + account.destroy_account(account.email, "") p = push_server.wait() - assert p[0] == "/8accbe08-4040-44c2-8fd9-cf2669b56cb1" - assert p[2] == b'' + assert p[0] == "/09d0114e-3b23-4ba3-8474-efac7433e3ba" + assert dev.decrypt(p[2]) == { + 'command': 'fxaccounts:account_destroyed', + 'data': {'uid': account.props['uid']}, + 'version': 1, + } assert push_server.done() - finally: - account.destroy_account(account.email, "") + +class TestAccountVerify: + def test_account_verify(self, client, push_server, mail_server): + account = client.create_account("test@test", "") + try: + (to, body) = mail_server.wait() + assert to == ["test@test"] + data = json.loads(base64.urlsafe_b64decode(body.split("#/verify/", maxsplit=1)[1]).decode('utf8')) + dev = Device(account, "dev", pcb=push_server.good("8accbe08-4040-44c2-8fd9-cf2669b56cb1")) + account.post_a('/recovery_email/verify_code', { 'uid': data['uid'], 'code': data['code'] }) + p = push_server.wait() + assert p[0] == "/8accbe08-4040-44c2-8fd9-cf2669b56cb1" + assert p[2] == b'' + assert push_server.done() + finally: + account.destroy_account(account.email, "") + +# these two destroy their sessions def test_session_destroy(client, account, push_server): dev1 = Device(account, "dev1") @@ -57,91 +63,95 @@ def test_device_connected(client, account, push_server): } assert push_server.done() -def test_device_invoke(account, login, push_server): - dev = Device(account, "dev1", commands={'a':'a'}, pcb=push_server.good("3610b071-e2ef-4daa-a4e3-eaa74e50f2a0")) - account.post_a("/account/devices/invoke_command", { - "target": dev.id, - "command": "a", - "payload": {"data": "foo"}, - "ttl": 10, - }) - p = push_server.wait() - assert p[0] == "/3610b071-e2ef-4daa-a4e3-eaa74e50f2a0" - msg = dev.decrypt(p[2]) - # NOTE needed because index is unpredictable due to pg sequence use - del msg['data']['index'] - del msg['data']['url'] - assert msg == { - 'command': 'fxaccounts:command_received', - 'data': {'command': 'a', 'sender': dev.id}, - 'version': 1, - } - assert push_server.done() +class TestDeviceInvoke: + def test_device_invoke(self, account, login, push_server): + dev = Device(account, "dev1", commands={'a':'a'}, pcb=push_server.good("3610b071-e2ef-4daa-a4e3-eaa74e50f2a0")) + account.post_a("/account/devices/invoke_command", { + "target": dev.id, + "command": "a", + "payload": {"data": "foo"}, + "ttl": 10, + }) + p = push_server.wait() + assert p[0] == "/3610b071-e2ef-4daa-a4e3-eaa74e50f2a0" + msg = dev.decrypt(p[2]) + # NOTE needed because index is unpredictable due to pg sequence use + del msg['data']['index'] + del msg['data']['url'] + assert msg == { + 'command': 'fxaccounts:command_received', + 'data': {'command': 'a', 'sender': dev.id}, + 'version': 1, + } + assert push_server.done() -def test_expiry(account, login, push_server): - dev = Device(account, "dev1", commands={'a':'a'}, pcb=push_server.bad("59ba8fcc-f3b0-4b1f-ac27-36d66e022d1e")) - account.post_a("/account/devices/invoke_command", { - "target": dev.id, - "command": "a", - "payload": {"data": "foo"}, - "ttl": 10, - }) - p = push_server.wait() - assert p[0] == "/err/59ba8fcc-f3b0-4b1f-ac27-36d66e022d1e" - account.post_a("/account/devices/invoke_command", { - "target": dev.id, - "command": "a", - "payload": {"data": "foo"}, - "ttl": 10, - }) - with pytest.raises(queue.Empty): - push_server.wait() - dev.update_pcb(push_server.good("59ba8fcc-f3b0-4b1f-ac27-36d66e022d1e")) - account.post_a("/account/devices/invoke_command", { - "target": dev.id, - "command": "a", - "payload": {"data": "foo"}, - "ttl": 10, - }) - p = push_server.wait() - assert p[0] == "/59ba8fcc-f3b0-4b1f-ac27-36d66e022d1e" - assert push_server.done() +class TestExpiry: + def test_expiry(self, account, login, push_server): + dev = Device(account, "dev1", commands={'a':'a'}, pcb=push_server.bad("59ba8fcc-f3b0-4b1f-ac27-36d66e022d1e")) + account.post_a("/account/devices/invoke_command", { + "target": dev.id, + "command": "a", + "payload": {"data": "foo"}, + "ttl": 10, + }) + p = push_server.wait() + assert p[0] == "/err/59ba8fcc-f3b0-4b1f-ac27-36d66e022d1e" + account.post_a("/account/devices/invoke_command", { + "target": dev.id, + "command": "a", + "payload": {"data": "foo"}, + "ttl": 10, + }) + with pytest.raises(queue.Empty): + push_server.wait() + dev.update_pcb(push_server.good("59ba8fcc-f3b0-4b1f-ac27-36d66e022d1e")) + account.post_a("/account/devices/invoke_command", { + "target": dev.id, + "command": "a", + "payload": {"data": "foo"}, + "ttl": 10, + }) + p = push_server.wait() + assert p[0] == "/59ba8fcc-f3b0-4b1f-ac27-36d66e022d1e" + assert push_server.done() -def test_device_notify(account, login, push_server): - dev1 = Device(account, "dev1") - dev2 = Device(login, "dev2") - dev1.update_pcb(push_server.good("738ac7e3-96ef-461c-880c-0af20e311354")) - dev2.update_pcb(push_server.good("85a98191-9486-46e2-877a-1152e3d4af4e")) - account.post_a("/account/devices/notify", { - "to": "all", - "_endpointAction": "accountVerify", - "excluded": [dev2.id], - "payload": {'a':1}, - "TTL": 0, - }) - p = push_server.wait() - assert p[0] == "/738ac7e3-96ef-461c-880c-0af20e311354" - assert dev1.decrypt(p[2]) == {'a':1} - account.post_a("/account/devices/notify", { - "to": [dev2.id], - "_endpointAction": "accountVerify", - "payload": {'a':2}, - "TTL": 0, - }) - p = push_server.wait() - assert p[0] == "/85a98191-9486-46e2-877a-1152e3d4af4e" - assert dev2.decrypt(p[2]) == {'a':2} - assert push_server.done() +class TestDeviceNotify: + def test_device_notify(self, account, login, push_server): + dev1 = Device(account, "dev1") + dev2 = Device(login, "dev2") + dev1.update_pcb(push_server.good("738ac7e3-96ef-461c-880c-0af20e311354")) + dev2.update_pcb(push_server.good("85a98191-9486-46e2-877a-1152e3d4af4e")) + account.post_a("/account/devices/notify", { + "to": "all", + "_endpointAction": "accountVerify", + "excluded": [dev2.id], + "payload": {'a':1}, + "TTL": 0, + }) + p = push_server.wait() + assert p[0] == "/738ac7e3-96ef-461c-880c-0af20e311354" + assert dev1.decrypt(p[2]) == {'a':1} + account.post_a("/account/devices/notify", { + "to": [dev2.id], + "_endpointAction": "accountVerify", + "payload": {'a':2}, + "TTL": 0, + }) + p = push_server.wait() + assert p[0] == "/85a98191-9486-46e2-877a-1152e3d4af4e" + assert dev2.decrypt(p[2]) == {'a':2} + assert push_server.done() -def test_profile(account, push_server): - dev = Device(account, "dev", pcb=push_server.good("12608154-8942-4f1c-9de2-a56465d27d6e")) - profile = account.profile() - profile.post_a("/display_name", {"displayName": "foo"}) - p = push_server.wait() - assert p[0] == "/12608154-8942-4f1c-9de2-a56465d27d6e" - assert dev.decrypt(p[2]) == {'command': 'fxaccounts:profile_updated', 'version': 1} - profile.post_a("/avatar/upload", "doesn't parse the image", headers={'content-type': 'image/png'}) - p = push_server.wait() - assert p[0] == "/12608154-8942-4f1c-9de2-a56465d27d6e" - assert dev.decrypt(p[2]) == {'command': 'fxaccounts:profile_updated', 'version': 1} - assert push_server.done() +class TestProfile: + def test_profile(self, account, push_server): + dev = Device(account, "dev", pcb=push_server.good("12608154-8942-4f1c-9de2-a56465d27d6e")) + profile = account.profile() + profile.post_a("/display_name", {"displayName": "foo"}) + p = push_server.wait() + assert p[0] == "/12608154-8942-4f1c-9de2-a56465d27d6e" + assert dev.decrypt(p[2]) == {'command': 'fxaccounts:profile_updated', 'version': 1} + profile.post_a("/avatar/upload", "doesn't parse the image", headers={'content-type': 'image/png'}) + p = push_server.wait() + assert p[0] == "/12608154-8942-4f1c-9de2-a56465d27d6e" + assert dev.decrypt(p[2]) == {'command': 'fxaccounts:profile_updated', 'version': 1} + assert push_server.done() -- cgit v1.2.3