test_wp10_tenant_api.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import yaml
  4. USER_UID = "01900000-0000-7000-8000-000000068901"
  5. class _ControlService:
  6. calls: list[dict] = []
  7. def issue_quota_claim(self, *, principal_id, host, request):
  8. type(self).calls.append({"principal_id": principal_id, "host": host, "request": request})
  9. return {"claim_uid": "claim-1", "tenant_id": "tenant-alpha", "status": "issued"}
  10. def provision_private(self, *, principal_id, host, request):
  11. type(self).calls.append({"principal_id": principal_id, "host": host, "request": request})
  12. return {"tenant_id": "tenant-alpha", "state": "active"}
  13. def issue_lifecycle_approval(self, *, reviewer_id, request):
  14. type(self).calls.append({"reviewer_id": reviewer_id, "request": request})
  15. return {"approval_ref": "01900000-0000-7000-8000-000000068999"}
  16. def lifecycle_transition(self, *, principal_id, host, operation, request):
  17. type(self).calls.append({"principal_id": principal_id, "host": host, "operation": operation, "request": request})
  18. return {"tenant_id": "tenant-alpha", "state": "frozen", "lease_fence": 1}
  19. def read(self, *, principal_id, host, kind):
  20. type(self).calls.append({"principal_id": principal_id, "host": host, "kind": kind})
  21. return {"tenant_id": "tenant-alpha", kind: []}
  22. def _client(monkeypatch):
  23. from app import create_app
  24. from app.api.system import tenant_routes
  25. _ControlService.calls = []
  26. monkeypatch.setenv("TRUSTED_TENANT_ROUTE_HOST", "private.example")
  27. monkeypatch.setattr(tenant_routes, "_service", lambda: _ControlService())
  28. monkeypatch.setattr(
  29. "app.core.system.auth.load_identity_from_token",
  30. lambda token, secret: {"id": USER_UID, "roles": [token]}
  31. if token in {"viewer", "editor", "admin"}
  32. else None,
  33. )
  34. app = create_app()
  35. app.config.update(TESTING=True)
  36. return app.test_client()
  37. def test_wp10_quota_claim_route_is_authenticated_server_derived_closed_and_no_store(monkeypatch):
  38. client = _client(monkeypatch)
  39. editor = {"Authorization": "Bearer editor", "Host": "private.example"}
  40. assert client.post("/api/system/tenant/quota-claims", json={}, headers={"Authorization": "Bearer viewer"}).status_code == 403
  41. rejected = client.post("/api/system/tenant/quota-claims", json={"tenant_id": "tenant-beta", "quota_name": "records", "amount": "1", "idempotency_key": "reserve-1"}, headers=editor)
  42. assert rejected.status_code == 400
  43. assert rejected.headers["Cache-Control"] == "no-store"
  44. created = client.post("/api/system/tenant/quota-claims", json={"quota_name": "records", "amount": "1", "idempotency_key": "reserve-1"}, headers=editor)
  45. assert created.status_code == 201
  46. assert created.headers["Cache-Control"] == "no-store"
  47. assert _ControlService.calls == [{
  48. "principal_id": USER_UID,
  49. "host": "private.example",
  50. "request": {"quota_name": "records", "amount": "1", "idempotency_key": "reserve-1"},
  51. }]
  52. def test_wp10_private_provision_route_is_admin_only_closed_and_no_store(monkeypatch):
  53. client = _client(monkeypatch)
  54. headers = {"Authorization": "Bearer admin", "Host": "private.example"}
  55. assert client.post("/api/system/tenant/provisions", json={"idempotency_key": "provision-1"}, headers={"Authorization": "Bearer editor"}).status_code == 403
  56. rejected = client.post("/api/system/tenant/provisions", json={"tenant_id": "tenant-beta", "idempotency_key": "provision-1"}, headers=headers)
  57. assert rejected.status_code == 400
  58. created = client.post("/api/system/tenant/provisions", json={"idempotency_key": "provision-1"}, headers=headers)
  59. assert created.status_code == 201
  60. assert created.headers["Cache-Control"] == "no-store"
  61. def test_wp10_approval_route_is_admin_only_closed_and_no_store(monkeypatch):
  62. client = _client(monkeypatch)
  63. admin = {"Authorization": "Bearer admin", "Host": "private.example"}
  64. assert client.post("/api/system/tenant/approvals", json={"operation": "begin_recovery", "expected_fence": "1", "idempotency_key": "recover-1"}, headers={"Authorization": "Bearer editor"}).status_code == 403
  65. rejected = client.post("/api/system/tenant/approvals", json={"tenant_id": "tenant-beta", "operation": "begin_recovery", "expected_fence": "1", "idempotency_key": "recover-1"}, headers=admin)
  66. assert rejected.status_code == 400
  67. created = client.post("/api/system/tenant/approvals", json={"operation": "begin_recovery", "expected_fence": "1", "idempotency_key": "recover-1"}, headers=admin)
  68. assert created.status_code == 201
  69. assert created.headers["Cache-Control"] == "no-store"
  70. def test_wp10_approval_openapi_contract_is_permissioned_required_and_closed():
  71. operation = yaml.safe_load(Path("docs/architecture/OPENAPI.yaml").read_text())["paths"]["/api/system/tenant/approvals"]["post"]
  72. assert operation["x-required-permission"] == "identity:manage"
  73. assert operation["requestBody"]["required"] is True
  74. schema = operation["requestBody"]["content"]["application/json"]["schema"]
  75. assert schema["additionalProperties"] is False
  76. assert set(schema["required"]) == {"operation", "expected_fence", "idempotency_key"}
  77. assert "201" in operation["responses"]
  78. assert operation["responses"]["201"]["headers"]["Cache-Control"]["schema"]["const"] == "no-store"
  79. assert operation["responses"]["default"]["headers"]["Cache-Control"]["schema"]["const"] == "no-store"
  80. def test_wp10_lifecycle_and_read_routes_are_rbac_closed_and_no_store(monkeypatch):
  81. client = _client(monkeypatch)
  82. admin = {"Authorization": "Bearer admin", "Host": "private.example"}
  83. editor = {"Authorization": "Bearer editor", "Host": "private.example"}
  84. assert client.post("/api/system/tenant/lifecycle/freeze", json={"expected_fence": "0", "idempotency_key": "freeze-1"}, headers=editor).status_code == 403
  85. rejected = client.post("/api/system/tenant/lifecycle/freeze", json={"tenant_id": "tenant-beta", "expected_fence": "0", "idempotency_key": "freeze-1"}, headers=admin)
  86. assert rejected.status_code == 400
  87. changed = client.post("/api/system/tenant/lifecycle/freeze", json={"expected_fence": "0", "idempotency_key": "freeze-1"}, headers=admin)
  88. assert changed.status_code == 200
  89. assert changed.headers["Cache-Control"] == "no-store"
  90. assert client.get("/api/system/tenant/status", headers=admin).status_code == 200
  91. assert client.get("/api/system/tenant/audit", headers=admin).headers["Cache-Control"] == "no-store"
  92. assert client.get("/api/system/tenant/manifests", headers=admin).status_code == 200