diff options
Diffstat (limited to 'tests/unit/auth/test_admin_user_roles.py')
| -rw-r--r-- | tests/unit/auth/test_admin_user_roles.py | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/tests/unit/auth/test_admin_user_roles.py b/tests/unit/auth/test_admin_user_roles.py new file mode 100644 index 0000000..7ce0b1f --- /dev/null +++ b/tests/unit/auth/test_admin_user_roles.py @@ -0,0 +1,262 @@ +"""Tests for admin role-assignment HTTP endpoints.""" +import pytest + +from gn_auth.auth.db import sqlite3 as db +from gn_auth.auth.authorisation.roles.models import assign_user_role_by_name + +from tests.unit.auth import conftest +from tests.unit.auth.fixtures.resource_fixtures import SYSTEM_RESOURCE + +# Body used in all role-assign tests — assigning system-administrator on the +# system resource is a real, migrations-seeded combination. +_ASSIGN_BODY = { + "role_name": "system-administrator", + "resource_id": str(SYSTEM_RESOURCE.resource_id) +} + +# Target user for assignment: unaff@iliated.user (no roles initially) +_TARGET_USER = conftest.TEST_USERS[3] + + +def _setup_admin_mock(conn, clients, mocker): + """Grant resource-owner role on SYSTEM_RESOURCE and mock the token. + + resource-owner carries resource:user:assign-role, which is what the + endpoint checks. In production the caller would masquerade as the + resource owner; here we grant the role directly for test setup. + """ + admin = conftest.TEST_USERS[4] + with db.cursor(conn) as cursor: + assign_user_role_by_name( + cursor, admin, SYSTEM_RESOURCE.resource_id, "resource-owner") + mocker.patch( + "gn_auth.auth.authorisation.users.views.require_oauth.acquire", + conftest.get_tokeniser( + admin, + tuple(c for c in clients if c.user == admin)[0])) + return admin + + +@pytest.mark.unit_test +def test_assign_role_no_token_returns_401(fxtr_app): + """ + GIVEN: no Authorization header + WHEN: POST /auth/user/<uid>/roles/assign + THEN: 401 is returned + """ + with fxtr_app.test_client() as http: + res = http.post( + f"/auth/user/{_TARGET_USER.user_id}/roles/assign", + json=_ASSIGN_BODY) + assert res.status_code == 401 + + +@pytest.mark.unit_test +def test_assign_role_non_admin_returns_403(fxtr_app, mocker, fxtr_oauth2_clients): + """ + GIVEN: a valid token belonging to a non-admin user + WHEN: POST /auth/user/<uid>/roles/assign + THEN: 403 is returned + """ + _conn, clients = fxtr_oauth2_clients + user = conftest.TEST_USERS[3] # unaff@iliated.user — no privileges + mocker.patch( + "gn_auth.auth.authorisation.users.views.require_oauth.acquire", + conftest.get_tokeniser( + user, + tuple(c for c in clients if c.user == user)[0])) + with fxtr_app.test_client() as http: + res = http.post( + f"/auth/user/{_TARGET_USER.user_id}/roles/assign", + json=_ASSIGN_BODY, + headers={"Authorization": "Bearer some-mocked-token"}) + assert res.status_code == 403 + + +def _revoke_assigned_role(conn): + """Remove the role row written by the success tests. + + Keeps the DB in the state the fixtures expect — no user_roles entry for + _TARGET_USER — so teardown and any subsequent queries are not surprised. + """ + with db.cursor(conn) as cursor: + cursor.execute( + "DELETE FROM user_roles " + "WHERE user_id=? " + "AND role_id=(SELECT role_id FROM roles WHERE role_name=?) " + "AND resource_id=?", + (str(_TARGET_USER.user_id), + _ASSIGN_BODY["role_name"], + _ASSIGN_BODY["resource_id"])) + + +@pytest.mark.unit_test +def test_assign_role_admin_returns_200(fxtr_app, mocker, fxtr_oauth2_clients): + """ + GIVEN: a valid system-admin token and a valid role/resource body + WHEN: POST /auth/user/<uid>/roles/assign + THEN: 200 is returned + """ + conn, clients = fxtr_oauth2_clients + _setup_admin_mock(conn, clients, mocker) + try: + with fxtr_app.test_client() as http: + res = http.post( + f"/auth/user/{_TARGET_USER.user_id}/roles/assign", + json=_ASSIGN_BODY, + headers={"Authorization": "Bearer some-mocked-token"}) + assert res.status_code == 200 + finally: + _revoke_assigned_role(conn) + + +def _assign_target_role(conn): + """Pre-assign system-administrator to _TARGET_USER on SYSTEM_RESOURCE. + + Required setup for revoke tests: the endpoint can only revoke what exists. + """ + with db.cursor(conn) as cursor: + assign_user_role_by_name( + cursor, _TARGET_USER, SYSTEM_RESOURCE.resource_id, + _ASSIGN_BODY["role_name"]) + + +def _cleanup_target_role(conn): + """Remove _TARGET_USER's system-administrator row if still present. + + No-op when the revoke endpoint already deleted it; guards against + test failures that leave the DB dirty. + """ + with db.cursor(conn) as cursor: + cursor.execute( + "DELETE FROM user_roles " + "WHERE user_id=? " + "AND role_id=(SELECT role_id FROM roles WHERE role_name=?) " + "AND resource_id=?", + (str(_TARGET_USER.user_id), + _ASSIGN_BODY["role_name"], + _ASSIGN_BODY["resource_id"])) + + +@pytest.mark.unit_test +def test_assign_role_persists_to_db(fxtr_app, mocker, fxtr_oauth2_clients): + """ + GIVEN: a valid system-admin token and a valid role/resource body + WHEN: POST /auth/user/<uid>/roles/assign + THEN: the user_roles row is present in the DB for that user/role/resource + """ + conn, clients = fxtr_oauth2_clients + _setup_admin_mock(conn, clients, mocker) + try: + with fxtr_app.test_client() as http: + http.post( + f"/auth/user/{_TARGET_USER.user_id}/roles/assign", + json=_ASSIGN_BODY, + headers={"Authorization": "Bearer some-mocked-token"}) + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT COUNT(*) AS cnt FROM user_roles " + "INNER JOIN roles ON user_roles.role_id=roles.role_id " + "WHERE user_roles.user_id=? " + "AND roles.role_name=? " + "AND user_roles.resource_id=?", + (str(_TARGET_USER.user_id), + _ASSIGN_BODY["role_name"], + _ASSIGN_BODY["resource_id"])) + assert cursor.fetchone()["cnt"] == 1 + finally: + _revoke_assigned_role(conn) + + +# --------------------------------------------------------------------------- +# HTTP endpoint tests: POST /auth/user/<uid>/roles/revoke +# --------------------------------------------------------------------------- + +@pytest.mark.unit_test +def test_revoke_role_no_token_returns_401(fxtr_app): + """ + GIVEN: no Authorization header + WHEN: POST /auth/user/<uid>/roles/revoke + THEN: 401 is returned + """ + with fxtr_app.test_client() as http: + res = http.post( + f"/auth/user/{_TARGET_USER.user_id}/roles/revoke", + json=_ASSIGN_BODY) + assert res.status_code == 401 + + +@pytest.mark.unit_test +def test_revoke_role_non_admin_returns_403(fxtr_app, mocker, fxtr_oauth2_clients): + """ + GIVEN: a valid token belonging to a non-admin user + WHEN: POST /auth/user/<uid>/roles/revoke + THEN: 403 is returned + """ + _conn, clients = fxtr_oauth2_clients + user = conftest.TEST_USERS[3] # unaff@iliated.user — no privileges + mocker.patch( + "gn_auth.auth.authorisation.users.views.require_oauth.acquire", + conftest.get_tokeniser( + user, + tuple(c for c in clients if c.user == user)[0])) + with fxtr_app.test_client() as http: + res = http.post( + f"/auth/user/{_TARGET_USER.user_id}/roles/revoke", + json=_ASSIGN_BODY, + headers={"Authorization": "Bearer some-mocked-token"}) + assert res.status_code == 403 + + +@pytest.mark.unit_test +def test_revoke_role_admin_returns_200(fxtr_app, mocker, fxtr_oauth2_clients): + """ + GIVEN: a valid token with resource:user:assign-role and the target user + holds the role on the resource + WHEN: POST /auth/user/<uid>/roles/revoke + THEN: 200 is returned + """ + conn, clients = fxtr_oauth2_clients + _setup_admin_mock(conn, clients, mocker) + _assign_target_role(conn) + try: + with fxtr_app.test_client() as http: + res = http.post( + f"/auth/user/{_TARGET_USER.user_id}/roles/revoke", + json=_ASSIGN_BODY, + headers={"Authorization": "Bearer some-mocked-token"}) + assert res.status_code == 200 + finally: + _cleanup_target_role(conn) + + +@pytest.mark.unit_test +def test_revoke_role_removes_from_db(fxtr_app, mocker, fxtr_oauth2_clients): + """ + GIVEN: a valid token with resource:user:assign-role and the target user + holds the role on the resource + WHEN: POST /auth/user/<uid>/roles/revoke + THEN: the user_roles row is absent from the DB + """ + conn, clients = fxtr_oauth2_clients + _setup_admin_mock(conn, clients, mocker) + _assign_target_role(conn) + try: + with fxtr_app.test_client() as http: + http.post( + f"/auth/user/{_TARGET_USER.user_id}/roles/revoke", + json=_ASSIGN_BODY, + headers={"Authorization": "Bearer some-mocked-token"}) + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT COUNT(*) AS cnt FROM user_roles " + "INNER JOIN roles ON user_roles.role_id=roles.role_id " + "WHERE user_roles.user_id=? " + "AND roles.role_name=? " + "AND user_roles.resource_id=?", + (str(_TARGET_USER.user_id), + _ASSIGN_BODY["role_name"], + _ASSIGN_BODY["resource_id"])) + assert cursor.fetchone()["cnt"] == 0 + finally: + _cleanup_target_role(conn) |
