diff options
Diffstat (limited to 'tests/test_gn_auth_smoke.py')
| -rw-r--r-- | tests/test_gn_auth_smoke.py | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/tests/test_gn_auth_smoke.py b/tests/test_gn_auth_smoke.py new file mode 100644 index 0000000..7dd9645 --- /dev/null +++ b/tests/test_gn_auth_smoke.py @@ -0,0 +1,216 @@ +""" +Smoke tests for the gn-auth service. + +Run with no credentials against CD (or production): + + pytest -m "gn_auth and smoke" + +These tests verify the public-facing contract of the three authorization +endpoints called at request time by gn2 and gn3, plus the JWKS endpoint +used for JWT validation. +""" + +import pytest + + +pytestmark = [pytest.mark.gn_auth, pytest.mark.smoke] + +# --------------------------------------------------------------------------- +# Known public trait used in authorisation tests. +# HC_M2_0606_P (Hippocampus Consortium M430v2) is a public dataset with +# extensive test coverage across gn2/gn3. +# --------------------------------------------------------------------------- +_PUBLIC_TRAIT = "HC_M2_0606_P::1435395_s_at" +_PUBLIC_PHENOTYPE_TRAIT = "BXDPublish::10710" +_NONEXISTENT_TRAIT = "NONEXISTENT_DATASET_XYZ::00000" + + +# --------------------------------------------------------------------------- +# GET /auth/public-jwks +# --------------------------------------------------------------------------- + +class TestPublicJwks: + def test_returns_200(self, gn_auth_url, http): + resp = http.get(f"{gn_auth_url}/auth/public-jwks", timeout=30) + assert resp.status_code == 200 + + def test_response_has_jwks_key(self, gn_auth_url, http): + resp = http.get(f"{gn_auth_url}/auth/public-jwks", timeout=30) + data = resp.json() + assert "jwks" in data, f"Missing 'jwks' key in response: {data}" + + def test_at_least_one_rsa_key(self, gn_auth_url, http): + resp = http.get(f"{gn_auth_url}/auth/public-jwks", timeout=30) + jwks = resp.json().get("jwks", []) + assert len(jwks) >= 1, "Expected at least one key in JWKS" + key = jwks[-1] # newest key + assert key.get("kty") == "RSA" + # "alg" is optional per RFC 7517 — gn-auth omits it + assert "n" in key and "e" in key, "RSA key missing modulus or exponent" + assert "kid" in key, "RSA key missing 'kid'" + + +# --------------------------------------------------------------------------- +# GET /auth/system/roles (no token → public-view only) +# --------------------------------------------------------------------------- + +class TestSystemRolesUnauthenticated: + def test_returns_200_without_token(self, gn_auth_url, http): + resp = http.get(f"{gn_auth_url}/auth/system/roles", timeout=30) + assert resp.status_code == 200 + + def test_response_is_list(self, gn_auth_url, http): + resp = http.get(f"{gn_auth_url}/auth/system/roles", timeout=30) + data = resp.json() + assert isinstance(data, list), f"Expected list, got: {type(data)}" + + def test_contains_public_view_role(self, gn_auth_url, http): + resp = http.get(f"{gn_auth_url}/auth/system/roles", timeout=30) + roles = resp.json() + names = [r.get("role_name") for r in roles] + assert "public-view" in names, ( + f"Expected 'public-view' role when called without auth token; got: {names}" + ) + + def test_public_view_role_has_privileges(self, gn_auth_url, http): + resp = http.get(f"{gn_auth_url}/auth/system/roles", timeout=30) + public_view = next( + (r for r in resp.json() if r.get("role_name") == "public-view"), None + ) + assert public_view is not None + assert isinstance(public_view.get("privileges"), list) + assert len(public_view["privileges"]) >= 1 + + def test_role_schema(self, gn_auth_url, http): + resp = http.get(f"{gn_auth_url}/auth/system/roles", timeout=30) + for role in resp.json(): + assert "role_id" in role, f"Role missing 'role_id': {role}" + assert "role_name" in role, f"Role missing 'role_name': {role}" + assert "privileges" in role, f"Role missing 'privileges': {role}" + + +# --------------------------------------------------------------------------- +# POST /auth/data/authorisation +# --------------------------------------------------------------------------- + +class TestDataAuthorisation: + def test_known_public_probeset_trait_returns_200(self, gn_auth_url, http): + resp = http.post( + f"{gn_auth_url}/auth/data/authorisation", + json={"traits": [_PUBLIC_TRAIT]}, + timeout=30, + ) + assert resp.status_code == 200, ( + f"Expected 200 for known public trait '{_PUBLIC_TRAIT}', " + f"got {resp.status_code}: {resp.text}" + ) + + def test_known_public_trait_response_structure(self, gn_auth_url, http): + resp = http.post( + f"{gn_auth_url}/auth/data/authorisation", + json={"traits": [_PUBLIC_TRAIT]}, + timeout=30, + ) + data = resp.json() + assert "authorisation" in data, f"Missing 'authorisation' key: {data}" + assert isinstance(data["authorisation"], list) + assert len(data["authorisation"]) >= 1 + + def test_known_public_trait_has_view_privilege(self, gn_auth_url, http): + resp = http.post( + f"{gn_auth_url}/auth/data/authorisation", + json={"traits": [_PUBLIC_TRAIT]}, + timeout=30, + ) + resources = resp.json()["authorisation"] + all_privileges = [p for r in resources for p in r.get("privileges", [])] + assert any("view" in p for p in all_privileges), ( + f"Expected a view privilege for public trait '{_PUBLIC_TRAIT}'; " + f"got privileges: {all_privileges}" + ) + + def test_resource_entry_schema(self, gn_auth_url, http): + # Actual response schema (verified against CD 2026-05-27): + # {"resource_id": str, "resource_data": [...], "privileges": [...]} + # Note: "public" and "resource_name" fields are NOT present in the + # deployed response despite appearing in source comments. + resp = http.post( + f"{gn_auth_url}/auth/data/authorisation", + json={"traits": [_PUBLIC_TRAIT]}, + timeout=30, + ) + for resource in resp.json()["authorisation"]: + assert "resource_id" in resource, f"Missing resource_id: {resource}" + assert "resource_data" in resource, f"Missing resource_data: {resource}" + assert "privileges" in resource, f"Missing privileges: {resource}" + assert isinstance(resource["resource_data"], list) + assert isinstance(resource["privileges"], list) + + def test_known_public_phenotype_trait(self, gn_auth_url, http): + resp = http.post( + f"{gn_auth_url}/auth/data/authorisation", + json={"traits": [_PUBLIC_PHENOTYPE_TRAIT]}, + timeout=30, + ) + assert resp.status_code == 200, ( + f"Expected 200 for '{_PUBLIC_PHENOTYPE_TRAIT}', " + f"got {resp.status_code}: {resp.text}" + ) + + def test_nonexistent_trait_returns_404(self, gn_auth_url, http): + resp = http.post( + f"{gn_auth_url}/auth/data/authorisation", + json={"traits": [_NONEXISTENT_TRAIT]}, + timeout=30, + ) + assert resp.status_code == 404, ( + f"Expected 404 for nonexistent trait '{_NONEXISTENT_TRAIT}', " + f"got {resp.status_code}: {resp.text}" + ) + + def test_empty_traits_list_is_handled(self, gn_auth_url, http): + resp = http.post( + f"{gn_auth_url}/auth/data/authorisation", + json={"traits": []}, + timeout=30, + ) + # Must not be a 500; 200 with empty result or 400/404 are all acceptable. + assert resp.status_code != 500, ( + f"Empty traits list caused 500: {resp.text}" + ) + + def test_multiple_traits_in_one_request(self, gn_auth_url, http): + resp = http.post( + f"{gn_auth_url}/auth/data/authorisation", + json={"traits": [_PUBLIC_TRAIT, _PUBLIC_PHENOTYPE_TRAIT]}, + timeout=30, + ) + assert resp.status_code == 200 + data = resp.json() + assert "authorisation" in data + + +# --------------------------------------------------------------------------- +# GET /auth/system/roles (with valid token — Phase 2) +# --------------------------------------------------------------------------- + +@pytest.mark.auth_flow +class TestSystemRolesAuthenticated: + def test_returns_200_with_token(self, gn_auth_url, http, access_token): + resp = http.get( + f"{gn_auth_url}/auth/system/roles", + headers={"Authorization": f"Bearer {access_token}"}, + timeout=30, + ) + assert resp.status_code == 200 + + def test_authenticated_user_has_more_than_public_view(self, gn_auth_url, http, access_token): + resp = http.get( + f"{gn_auth_url}/auth/system/roles", + headers={"Authorization": f"Bearer {access_token}"}, + timeout=30, + ) + roles = resp.json() + names = [r.get("role_name") for r in roles] + # A logged-in user should see public-view plus at least their own roles. + assert "public-view" in names |
