""" 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