Implement PostgreSQL production store path
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 41s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 41s
Add the PostgreSQL backend, migration and stopped-write transfer tools, lease-aware deployment manifests, tenancy declarations, and shared conformance coverage. Persist grouping mutations in durable stores and separate process liveness from database readiness.
This commit is contained in:
parent
2063470ac8
commit
749461b97b
30 changed files with 2364 additions and 71 deletions
|
|
@ -3,10 +3,20 @@ from datetime import UTC, datetime
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from tenant_engine.app import create_app
|
||||
from tenant_engine.authz import WriteAuthorizer
|
||||
from tenant_engine.domain import CapabilityRole, Tenant, create_role_grant
|
||||
from tenant_engine.store import InMemoryTenantStore, TenantStore
|
||||
|
||||
|
||||
class _AllowAll(WriteAuthorizer):
|
||||
def authorize(self, *, action: str, tenant_id: str, actor: str) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def _client(store: TenantStore) -> TestClient:
|
||||
return TestClient(create_app(store=store, authorizer=_AllowAll()))
|
||||
|
||||
|
||||
class _BrokenStore:
|
||||
"""Test double: every active_roles() call raises, simulating an outage."""
|
||||
|
||||
|
|
@ -57,8 +67,8 @@ def _seeded_store() -> InMemoryTenantStore:
|
|||
|
||||
|
||||
def test_cache_read_roles_returns_active_roles() -> None:
|
||||
client = TestClient(create_app(store=_seeded_store()))
|
||||
response = client.get("/tenants/t-binky/roles")
|
||||
client = _client(_seeded_store())
|
||||
response = client.get("/tenants/t-binky/roles", params={"actor": "key-cape"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"tenant_id": "t-binky", "roles": ["CUS"]}
|
||||
|
|
@ -70,23 +80,25 @@ def test_cache_read_roles_resolves_by_identifier_not_only_internal_id() -> None:
|
|||
tenant's profile identifier (the IAM Profile `tenant` claim value), via
|
||||
a URL path segment containing colons -- never the internal tenant_id.
|
||||
"""
|
||||
client = TestClient(create_app(store=_seeded_store()))
|
||||
response = client.get("/tenants/tenant:friendly:binky/roles")
|
||||
client = _client(_seeded_store())
|
||||
response = client.get(
|
||||
"/tenants/tenant:friendly:binky/roles", params={"actor": "key-cape"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"tenant_id": "tenant:friendly:binky", "roles": ["CUS"]}
|
||||
|
||||
|
||||
def test_cache_read_roles_unknown_tenant_is_404() -> None:
|
||||
client = TestClient(create_app(store=_seeded_store()))
|
||||
response = client.get("/tenants/does-not-exist/roles")
|
||||
client = _client(_seeded_store())
|
||||
response = client.get("/tenants/does-not-exist/roles", params={"actor": "key-cape"})
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_live_lookup_roles_returns_active_roles() -> None:
|
||||
client = TestClient(create_app(store=_seeded_store()))
|
||||
response = client.get("/tenants/t-binky/roles/live")
|
||||
client = _client(_seeded_store())
|
||||
response = client.get("/tenants/t-binky/roles/live", params={"actor": "flex-auth"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["roles"] == ["CUS"]
|
||||
|
|
@ -94,9 +106,9 @@ def test_live_lookup_roles_returns_active_roles() -> None:
|
|||
|
||||
def test_live_lookup_fails_closed_on_store_outage() -> None:
|
||||
broken = _BrokenStore(_seeded_store())
|
||||
client = TestClient(create_app(store=broken))
|
||||
client = _client(broken)
|
||||
|
||||
response = client.get("/tenants/t-binky/roles/live")
|
||||
response = client.get("/tenants/t-binky/roles/live", params={"actor": "flex-auth"})
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.json() != {"tenant_id": "t-binky", "roles": []}, (
|
||||
|
|
@ -106,8 +118,35 @@ def test_live_lookup_fails_closed_on_store_outage() -> None:
|
|||
|
||||
def test_cache_read_also_fails_closed_on_store_outage() -> None:
|
||||
broken = _BrokenStore(_seeded_store())
|
||||
client = TestClient(create_app(store=broken))
|
||||
client = _client(broken)
|
||||
|
||||
response = client.get("/tenants/t-binky/roles")
|
||||
response = client.get("/tenants/t-binky/roles", params={"actor": "key-cape"})
|
||||
|
||||
assert response.status_code == 503
|
||||
|
||||
|
||||
def test_unauthorized_reads_are_denied_before_tenant_existence_is_observed() -> None:
|
||||
class _MustNotRead:
|
||||
def get_tenant(self, tenant_id):
|
||||
raise AssertionError(f"store read leaked for {tenant_id}")
|
||||
|
||||
def active_roles(self, tenant_id):
|
||||
raise AssertionError(f"role read leaked for {tenant_id}")
|
||||
|
||||
client = TestClient(create_app(store=_MustNotRead()))
|
||||
for path in (
|
||||
"/tenants/known?actor=unbound",
|
||||
"/tenants/unknown?actor=unbound",
|
||||
"/tenants/known/roles?actor=unbound",
|
||||
"/tenants/unknown/roles/live?actor=unbound",
|
||||
):
|
||||
response = client.get(path)
|
||||
assert response.status_code == 403
|
||||
assert response.json()["error_code"] == "write_denied"
|
||||
|
||||
|
||||
def test_read_requires_an_explicit_actor() -> None:
|
||||
client = _client(_seeded_store())
|
||||
assert client.get("/tenants/t-binky").status_code == 422
|
||||
assert client.get("/tenants/t-binky/roles").status_code == 422
|
||||
assert client.get("/tenants/t-binky/roles/live").status_code == 422
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue