feat: strengthen module contracts and shared WebUI runtime
This commit is contained in:
@@ -25,6 +25,7 @@ from govoplan_core.core.access import (
|
||||
CAPABILITY_AUDIT_RECORDER,
|
||||
CAPABILITY_AUDIT_RETENTION,
|
||||
CAPABILITY_AUDIT_SINK,
|
||||
CAPABILITY_AUTH_API_PRINCIPAL_PROVIDER,
|
||||
CAPABILITY_TENANCY_TENANT_RESOLVER,
|
||||
AccessDecision,
|
||||
AccessAdministration,
|
||||
@@ -1082,16 +1083,62 @@ class AccessContractTests(unittest.TestCase):
|
||||
def get_organization_function_assignment(self, requested_assignment_id: str):
|
||||
return assignment if requested_assignment_id == assignment_id else None
|
||||
|
||||
def organization_function_assignments_for_identity(self, requested_identity_id: str, *, tenant_id: str | None = None):
|
||||
def organization_function_assignments_for_identity(
|
||||
self,
|
||||
requested_identity_id: str,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at=None,
|
||||
):
|
||||
del effective_at
|
||||
if requested_identity_id == identity_id and tenant_id in (None, assignment.tenant_id):
|
||||
return (assignment,)
|
||||
return ()
|
||||
|
||||
def organization_function_assignments_for_account(self, requested_account_id: str, *, tenant_id: str | None = None):
|
||||
def organization_function_assignments_for_account(
|
||||
self,
|
||||
requested_account_id: str,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at=None,
|
||||
):
|
||||
del effective_at
|
||||
if requested_account_id == account_id and tenant_id in (None, assignment.tenant_id):
|
||||
return (assignment,)
|
||||
return ()
|
||||
|
||||
def organization_function_assignments_for_identities(
|
||||
self,
|
||||
identity_ids,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at=None,
|
||||
):
|
||||
return {
|
||||
item_id: self.organization_function_assignments_for_identity(
|
||||
item_id,
|
||||
tenant_id=tenant_id,
|
||||
effective_at=effective_at,
|
||||
)
|
||||
for item_id in identity_ids
|
||||
}
|
||||
|
||||
def organization_function_assignments_for_accounts(
|
||||
self,
|
||||
account_ids,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at=None,
|
||||
):
|
||||
return {
|
||||
item_id: self.organization_function_assignments_for_account(
|
||||
item_id,
|
||||
tenant_id=tenant_id,
|
||||
effective_at=effective_at,
|
||||
)
|
||||
for item_id in account_ids
|
||||
}
|
||||
|
||||
class FakeOrganizationDirectory(CoreOrganizationDirectory):
|
||||
def get_organization_unit(self, requested_organization_unit_id: str):
|
||||
if requested_organization_unit_id != organization_unit_id:
|
||||
@@ -1317,7 +1364,7 @@ class AccessContractTests(unittest.TestCase):
|
||||
clear_runtime()
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
def test_api_principal_dependency_uses_access_resolver_capability(self) -> None:
|
||||
def test_api_principal_dependency_uses_auth_provider_capability(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-access-contract-"))
|
||||
try:
|
||||
account_id = "account-capability"
|
||||
@@ -1346,6 +1393,31 @@ class AccessContractTests(unittest.TestCase):
|
||||
def explain_scope(self, principal: PrincipalRef, required_scope: str) -> AccessDecision:
|
||||
return AccessDecision(allowed=self.has_scope(principal, required_scope), requirements=(required_scope,))
|
||||
|
||||
class CapabilityApiPrincipalProvider:
|
||||
def resolve_api_principal(
|
||||
self,
|
||||
request,
|
||||
session,
|
||||
*,
|
||||
authorization=None,
|
||||
x_api_key=None,
|
||||
):
|
||||
del authorization, x_api_key
|
||||
principal_ref = CapabilityPrincipalResolver().resolve_request(
|
||||
request,
|
||||
session=session,
|
||||
)
|
||||
account = session.get(Account, principal_ref.account_id)
|
||||
user = session.get(User, principal_ref.membership_id)
|
||||
assert account is not None
|
||||
assert user is not None
|
||||
return ApiPrincipal(
|
||||
principal=principal_ref,
|
||||
account=account,
|
||||
user=user,
|
||||
permission_evaluator=CapabilityPermissionEvaluator(),
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/capability-principal")
|
||||
@@ -1365,6 +1437,7 @@ class AccessContractTests(unittest.TestCase):
|
||||
capability_factories={
|
||||
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER: lambda context: CapabilityPrincipalResolver(),
|
||||
CAPABILITY_ACCESS_PERMISSION_EVALUATOR: lambda context: CapabilityPermissionEvaluator(),
|
||||
CAPABILITY_AUTH_API_PRINCIPAL_PROVIDER: lambda context: CapabilityApiPrincipalProvider(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1372,7 +1445,7 @@ class AccessContractTests(unittest.TestCase):
|
||||
title="access contract test",
|
||||
version="test",
|
||||
settings=SimpleNamespace(database_url=f"sqlite:///{root / 'test.db'}"),
|
||||
enabled_modules=(),
|
||||
enabled_modules=(ACCESS_MODULE_ID,),
|
||||
manifest_factories=(access_manifest,),
|
||||
base_routers=(router,),
|
||||
post_module_routers=(),
|
||||
|
||||
@@ -52,6 +52,7 @@ engine = _database.engine
|
||||
SessionLocal = _database.SessionLocal
|
||||
|
||||
from govoplan_core.server.app import app
|
||||
from govoplan_campaign.backend.sending.execution import SNAPSHOT_VERSION
|
||||
|
||||
|
||||
class ApiSmokeTests(unittest.TestCase):
|
||||
@@ -764,7 +765,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
assert version is not None
|
||||
self.assertEqual(version.raw_json["server"], {"mail_profile_id": profile_id})
|
||||
snapshot = version.execution_snapshot or {}
|
||||
self.assertEqual(snapshot["snapshot_version"], "5")
|
||||
self.assertEqual(snapshot["snapshot_version"], SNAPSHOT_VERSION)
|
||||
self.assertEqual(snapshot["mail_profile_id"], profile_id)
|
||||
self.assertNotIn("smtp", snapshot)
|
||||
self.assertNotIn("imap", snapshot)
|
||||
@@ -774,8 +775,10 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
deleted = self.client.delete(f"/api/v1/mail/profiles/{profile_id}", headers=headers)
|
||||
self.assertEqual(deleted.status_code, 200, deleted.text)
|
||||
self.assertFalse(deleted.json()["is_active"])
|
||||
self.assertFalse(deleted.json()["smtp_password_configured"])
|
||||
self.assertFalse(deleted.json()["imap_password_configured"])
|
||||
# Reusable hierarchy credentials outlive a deactivated profile. The
|
||||
# profile's retired legacy secret columns are still scrubbed below.
|
||||
self.assertTrue(deleted.json()["smtp_password_configured"])
|
||||
self.assertTrue(deleted.json()["imap_password_configured"])
|
||||
|
||||
from govoplan_audit.backend.db.models import AuditLog
|
||||
|
||||
@@ -864,7 +867,10 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertIsNotNone(version)
|
||||
assert version is not None
|
||||
snapshot_payload = version.execution_snapshot or {}
|
||||
self.assertEqual(snapshot_payload["snapshot_version"], "5")
|
||||
self.assertEqual(
|
||||
snapshot_payload["snapshot_version"],
|
||||
SNAPSHOT_VERSION,
|
||||
)
|
||||
self.assertEqual(snapshot_payload["mail_profile_id"], profile_id)
|
||||
self.assertNotIn("smtp", snapshot_payload)
|
||||
self.assertNotIn("imap", snapshot_payload)
|
||||
@@ -2945,7 +2951,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(delta_payload["jobs"][0]["send_status"], "outcome_unknown")
|
||||
self.assertEqual(
|
||||
delta_payload["jobs"][0]["last_error"],
|
||||
"SMTP delivery outcome requires operator reconciliation.",
|
||||
"Delivery outcome requires operator reconciliation.",
|
||||
)
|
||||
self.assertEqual(delta_payload["counts"]["send"]["outcome_unknown"], 1)
|
||||
self.assertTrue(str(delta_payload["watermark"]).startswith("seq:"))
|
||||
@@ -4172,7 +4178,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
from govoplan_campaign.backend.db.models import CampaignJob, CampaignVersion, SendAttempt
|
||||
from govoplan_mail.backend.db.models import MailServerProfile
|
||||
from govoplan_mail.backend.db.models import MailServerEndpoint, MailServerProfile
|
||||
|
||||
with SessionLocal() as session:
|
||||
version = session.get(CampaignVersion, version_id)
|
||||
@@ -4180,7 +4186,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
assert version is not None
|
||||
snapshot = version.execution_snapshot
|
||||
self.assertIsInstance(snapshot, dict)
|
||||
self.assertEqual(snapshot["snapshot_version"], "5")
|
||||
self.assertEqual(snapshot["snapshot_version"], SNAPSHOT_VERSION)
|
||||
self.assertEqual(snapshot["job_count"], 1)
|
||||
self.assertEqual(snapshot["queueable_job_count"], 1)
|
||||
self.assertTrue(snapshot["job_manifest_sha256"])
|
||||
@@ -4195,9 +4201,17 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
profile = session.get(MailServerProfile, snapshot["mail_profile_id"])
|
||||
self.assertIsNotNone(profile)
|
||||
assert profile is not None
|
||||
profile.smtp_config = {**profile.smtp_config, "host": "smtp.example.invalid"}
|
||||
profile.smtp_transport_revision = "manually-rotated-transport-revision"
|
||||
session.add(profile)
|
||||
smtp_server_id = snapshot.get("smtp_server_id")
|
||||
self.assertTrue(smtp_server_id)
|
||||
smtp_server = session.get(MailServerEndpoint, smtp_server_id)
|
||||
self.assertIsNotNone(smtp_server)
|
||||
assert smtp_server is not None
|
||||
smtp_server.config = {
|
||||
**smtp_server.config,
|
||||
"host": "smtp.example.invalid",
|
||||
}
|
||||
smtp_server.transport_revision = "manually-rotated-transport-revision"
|
||||
session.add(smtp_server)
|
||||
session.commit()
|
||||
|
||||
sent = self.client.post(
|
||||
|
||||
@@ -19,10 +19,25 @@ from govoplan_core.core.change_sequence import (
|
||||
sequence_watermark_is_expired,
|
||||
)
|
||||
from govoplan_core.core.events import EventActorRef, EventBus, EventObjectRef, EventTenantRef, PlatformEvent, event_bus_context
|
||||
from govoplan_core.core.runtime import (
|
||||
clear_runtime,
|
||||
configure_runtime,
|
||||
get_runtime_context,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
|
||||
|
||||
class ChangeSequenceTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._runtime_context = get_runtime_context()
|
||||
clear_runtime()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self._runtime_context is not None:
|
||||
configure_runtime(self._runtime_context)
|
||||
else:
|
||||
clear_runtime()
|
||||
|
||||
def test_records_changes_and_returns_entries_after_watermark(self) -> None:
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
@@ -93,6 +108,8 @@ class ChangeSequenceTests(unittest.TestCase):
|
||||
actor_id="user-1",
|
||||
payload={"scope_type": "tenant"},
|
||||
)
|
||||
self.assertEqual([], seen)
|
||||
session.commit()
|
||||
|
||||
self.assertEqual([case[-1] for case in cases], [event.type for event in seen])
|
||||
event = seen[1]
|
||||
@@ -105,6 +122,36 @@ class ChangeSequenceTests(unittest.TestCase):
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_record_change_discards_event_when_transaction_rolls_back(self) -> None:
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
try:
|
||||
Base.metadata.create_all(
|
||||
bind=engine,
|
||||
tables=[
|
||||
ChangeSequenceEntry.__table__,
|
||||
ChangeSequenceRetentionFloor.__table__,
|
||||
],
|
||||
)
|
||||
seen: list[PlatformEvent] = []
|
||||
bus = EventBus()
|
||||
bus.subscribe("*", seen.append)
|
||||
with SessionLocal() as session, event_bus_context(bus):
|
||||
record_change(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
module_id="files",
|
||||
collection="files.assets",
|
||||
resource_type="file",
|
||||
resource_id="file-1",
|
||||
operation="created",
|
||||
)
|
||||
session.rollback()
|
||||
|
||||
self.assertEqual([], seen)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_pruning_records_retention_floor_for_stale_watermarks(self) -> None:
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Response
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from govoplan_core.auth import get_api_principal
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.server.fastapi import create_govoplan_app
|
||||
from govoplan_core.server.platform import create_platform_router
|
||||
@@ -85,6 +86,7 @@ class ConditionalRequestTests(unittest.TestCase):
|
||||
registry=PlatformRegistry(),
|
||||
api_router=api_router,
|
||||
)
|
||||
app.dependency_overrides[get_api_principal] = lambda: object()
|
||||
|
||||
with TestClient(app) as client:
|
||||
for path in (
|
||||
|
||||
@@ -8,6 +8,8 @@ from unittest.mock import patch
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_event, audit_operation_context
|
||||
from govoplan_core.core.events import (
|
||||
@@ -17,6 +19,7 @@ from govoplan_core.core.events import (
|
||||
EventTenantRef,
|
||||
PlatformEvent,
|
||||
current_event_trace,
|
||||
emit_platform_event,
|
||||
event_bus_context,
|
||||
event_context,
|
||||
normalize_trace_id,
|
||||
@@ -31,13 +34,60 @@ from tests.db_isolation import temporary_database
|
||||
|
||||
|
||||
def _configure_audit_runtime() -> None:
|
||||
registry = build_platform_registry(("audit",))
|
||||
registry = build_platform_registry(("access", "audit"))
|
||||
context = ModuleContext(registry=registry, settings=object())
|
||||
registry.configure_capability_context(context)
|
||||
configure_runtime(context)
|
||||
|
||||
|
||||
class CoreEventTests(unittest.TestCase):
|
||||
def test_fallback_event_waits_for_outer_commit_after_savepoint(self) -> None:
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
seen: list[PlatformEvent] = []
|
||||
bus = EventBus()
|
||||
bus.subscribe("*", seen.append)
|
||||
try:
|
||||
with Session(engine) as session, event_bus_context(bus):
|
||||
with session.begin():
|
||||
with session.begin_nested():
|
||||
emit_platform_event(
|
||||
session,
|
||||
PlatformEvent(type="nested.created", module_id="core"),
|
||||
)
|
||||
self.assertEqual([], seen)
|
||||
self.assertEqual(
|
||||
["nested.created"],
|
||||
[event.type for event in seen],
|
||||
)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_fallback_event_discards_only_rolled_back_savepoint(self) -> None:
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
seen: list[PlatformEvent] = []
|
||||
bus = EventBus()
|
||||
bus.subscribe("*", seen.append)
|
||||
try:
|
||||
with Session(engine) as session, event_bus_context(bus):
|
||||
with session.begin():
|
||||
emit_platform_event(
|
||||
session,
|
||||
PlatformEvent(type="outer.created", module_id="core"),
|
||||
)
|
||||
savepoint = session.begin_nested()
|
||||
emit_platform_event(
|
||||
session,
|
||||
PlatformEvent(type="nested.created", module_id="core"),
|
||||
)
|
||||
savepoint.rollback()
|
||||
self.assertEqual([], seen)
|
||||
self.assertEqual(
|
||||
["outer.created"],
|
||||
[event.type for event in seen],
|
||||
)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_event_bus_adds_trace_ids_and_propagates_causation_to_nested_events(self) -> None:
|
||||
bus = EventBus()
|
||||
seen: list[PlatformEvent] = []
|
||||
@@ -267,6 +317,11 @@ class CoreEventTests(unittest.TestCase):
|
||||
object_id="user-2",
|
||||
details={"password": "secret", "field": "display_name"},
|
||||
)
|
||||
session.commit()
|
||||
from govoplan_audit.backend.outbox import SqlAuditOutbox
|
||||
|
||||
SqlAuditOutbox().dispatch_pending(session)
|
||||
session.commit()
|
||||
|
||||
action_events = [event for event in seen if event.type == "user.updated"]
|
||||
self.assertEqual(1, len(action_events))
|
||||
@@ -315,6 +370,11 @@ class CoreEventTests(unittest.TestCase):
|
||||
object_type="demo",
|
||||
object_id=action,
|
||||
)
|
||||
session.commit()
|
||||
from govoplan_audit.backend.outbox import SqlAuditOutbox
|
||||
|
||||
SqlAuditOutbox().dispatch_pending(session)
|
||||
session.commit()
|
||||
|
||||
by_type = {event.type: event.module_id for event in seen if event.type in cases}
|
||||
self.assertEqual(cases, by_type)
|
||||
|
||||
@@ -131,8 +131,15 @@ class DatabaseMigrationTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-core-baseline-test-") as directory:
|
||||
database = Path(directory) / "core.db"
|
||||
url = f"sqlite:///{database}"
|
||||
enabled_modules = ("access",)
|
||||
|
||||
command.upgrade(alembic_config(database_url=url, enabled_modules=()), "heads")
|
||||
command.upgrade(
|
||||
alembic_config(
|
||||
database_url=url,
|
||||
enabled_modules=enabled_modules,
|
||||
),
|
||||
"heads",
|
||||
)
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
@@ -157,7 +164,13 @@ class DatabaseMigrationTests(unittest.TestCase):
|
||||
).scalar_one()
|
||||
current = database_migration_heads(connection)
|
||||
|
||||
self.assertEqual(current, configured_migration_heads(url, enabled_modules=()))
|
||||
self.assertEqual(
|
||||
current,
|
||||
configured_migration_heads(
|
||||
url,
|
||||
enabled_modules=enabled_modules,
|
||||
),
|
||||
)
|
||||
self.assertIn("core_scopes", tables)
|
||||
self.assertNotIn("tenancy_tenants", tables)
|
||||
self.assertIn("access_accounts", tables)
|
||||
|
||||
@@ -119,20 +119,66 @@ class _FakeIdmDirectory:
|
||||
def get_organization_function_assignment(self, assignment_id: str) -> OrganizationFunctionAssignmentRef | None:
|
||||
return self.assignment if assignment_id == self.assignment.id else None
|
||||
|
||||
def organization_function_assignments_for_account(self, account_id: str, *, tenant_id: str | None = None):
|
||||
def organization_function_assignments_for_account(
|
||||
self,
|
||||
account_id: str,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at=None,
|
||||
):
|
||||
del effective_at
|
||||
if account_id != self.assignment.account_id:
|
||||
return ()
|
||||
if tenant_id is not None and tenant_id != self.assignment.tenant_id:
|
||||
return ()
|
||||
return (self.assignment,)
|
||||
|
||||
def organization_function_assignments_for_identity(self, identity_id: str, *, tenant_id: str | None = None):
|
||||
def organization_function_assignments_for_identity(
|
||||
self,
|
||||
identity_id: str,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at=None,
|
||||
):
|
||||
del effective_at
|
||||
if identity_id != self.assignment.identity_id:
|
||||
return ()
|
||||
if tenant_id is not None and tenant_id != self.assignment.tenant_id:
|
||||
return ()
|
||||
return (self.assignment,)
|
||||
|
||||
def organization_function_assignments_for_identities(
|
||||
self,
|
||||
identity_ids,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at=None,
|
||||
):
|
||||
return {
|
||||
identity_id: self.organization_function_assignments_for_identity(
|
||||
identity_id,
|
||||
tenant_id=tenant_id,
|
||||
effective_at=effective_at,
|
||||
)
|
||||
for identity_id in identity_ids
|
||||
}
|
||||
|
||||
def organization_function_assignments_for_accounts(
|
||||
self,
|
||||
account_ids,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at=None,
|
||||
):
|
||||
return {
|
||||
account_id: self.organization_function_assignments_for_account(
|
||||
account_id,
|
||||
tenant_id=tenant_id,
|
||||
effective_at=effective_at,
|
||||
)
|
||||
for account_id in account_ids
|
||||
}
|
||||
|
||||
|
||||
class IdentityOrganizationContractTests(unittest.TestCase):
|
||||
def test_protocol_shapes_match_reference_implementations(self) -> None:
|
||||
|
||||
@@ -230,7 +230,10 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
self.assertTrue({"access", "admin", "tenancy", "policy", "audit", "dashboard", "files", "mail", "campaigns", "docs", "ops"}.issubset(manifests))
|
||||
self.assertEqual(manifests["campaigns"].dependencies, ())
|
||||
self.assertTrue(manifests["campaigns"].required_capabilities)
|
||||
self.assertEqual(manifests["campaigns"].optional_dependencies, ("files", "mail", "notifications", "addresses"))
|
||||
self.assertEqual(
|
||||
manifests["campaigns"].optional_dependencies,
|
||||
("files", "mail", "notifications", "addresses", "postbox"),
|
||||
)
|
||||
self.assertEqual(manifests["dashboard"].dependencies, ())
|
||||
self.assertTrue(manifests["dashboard"].required_capabilities)
|
||||
self.assertEqual(manifests["docs"].dependencies, ())
|
||||
@@ -587,7 +590,7 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
|
||||
def test_enabled_module_permutations_register_expected_routes(self) -> None:
|
||||
cases = (
|
||||
("core_only", (), {"access"}, set()),
|
||||
("core_only", (), set(), set()),
|
||||
("files_only", ("files",), {"access", "files"}, {"/api/v1/files"}),
|
||||
("mail_only", ("mail",), {"access", "mail"}, {"/api/v1/mail"}),
|
||||
("campaign_without_files_or_mail", ("campaigns",), {"access", "campaigns"}, {"/api/v1/campaigns"}),
|
||||
@@ -602,8 +605,14 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
app, _settings_obj = self._app_for_modules(enabled_modules)
|
||||
route_paths = _route_paths(app)
|
||||
self.assertIn("/api/v1/platform/modules", route_paths)
|
||||
self.assertIn("/api/v1/auth/login", route_paths)
|
||||
self.assertIn("/api/v1/admin/users", route_paths)
|
||||
self.assertEqual(
|
||||
"access" in expected_modules,
|
||||
"/api/v1/auth/login" in route_paths,
|
||||
)
|
||||
self.assertEqual(
|
||||
"access" in expected_modules,
|
||||
"/api/v1/admin/users" in route_paths,
|
||||
)
|
||||
for prefix in ("/api/v1/campaigns", "/api/v1/files", "/api/v1/mail", "/api/v1/docs", "/api/v1/ops"):
|
||||
has_prefix = any(path == prefix or path.startswith(f"{prefix}/") for path in route_paths)
|
||||
self.assertEqual(prefix in expected_prefixes, has_prefix, f"{name}: {prefix}")
|
||||
@@ -613,12 +622,19 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
self.assertEqual(health.status_code, 200, health.text)
|
||||
self.assertEqual({"status": "ok"}, health.json())
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload_modules = {item["id"] for item in response.json()["modules"]}
|
||||
self.assertEqual(
|
||||
401 if "access" in expected_modules else 503,
|
||||
response.status_code,
|
||||
response.text,
|
||||
)
|
||||
payload_modules = {
|
||||
item.id
|
||||
for item in app.state.govoplan_registry.manifests()
|
||||
}
|
||||
self.assertEqual(expected_modules, payload_modules)
|
||||
|
||||
def test_governance_template_routes_are_contributed_by_admin_module(self) -> None:
|
||||
access_only_app, _settings_obj = self._app_for_modules(())
|
||||
access_only_app, _settings_obj = self._app_for_modules(("access",))
|
||||
access_only_paths = _route_paths(access_only_app)
|
||||
self.assertIn("/api/v1/admin/users", access_only_paths)
|
||||
self.assertNotIn("/api/v1/admin/system/governance-templates", access_only_paths)
|
||||
@@ -827,6 +843,37 @@ finally:
|
||||
with self.assertRaisesRegex(RegistryError, "requires unavailable capability"):
|
||||
registry.validate()
|
||||
|
||||
def test_registry_keeps_optional_auth_modules_access_free(self) -> None:
|
||||
for modules in ((), ("poll",), ("evaluation",), ("scheduling",)):
|
||||
with self.subTest(modules=modules):
|
||||
registry = build_platform_registry(modules)
|
||||
self.assertFalse(registry.has_module("access"))
|
||||
|
||||
def test_registry_prefers_selected_auth_provider_over_default(self) -> None:
|
||||
provider = ModuleManifest(
|
||||
id="alternative_auth",
|
||||
name="Alternative auth",
|
||||
version="test",
|
||||
capability_factories={
|
||||
"auth.principalResolver": lambda _context: object(),
|
||||
},
|
||||
)
|
||||
consumer = ModuleManifest(
|
||||
id="consumer",
|
||||
name="Consumer",
|
||||
version="test",
|
||||
required_capabilities=("auth.principalResolver",),
|
||||
)
|
||||
|
||||
registry = build_platform_registry(
|
||||
("alternative_auth", "consumer"),
|
||||
manifest_factories=(lambda: provider, lambda: consumer),
|
||||
)
|
||||
|
||||
self.assertFalse(registry.has_module("access"))
|
||||
self.assertTrue(registry.has_module("alternative_auth"))
|
||||
self.assertTrue(registry.has_module("consumer"))
|
||||
|
||||
def test_registry_resolves_required_interface_ranges(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
registry.register(ModuleManifest(
|
||||
@@ -903,7 +950,7 @@ finally:
|
||||
plan = plan_desired_enabled_modules(("campaigns",), manifests)
|
||||
|
||||
self.assertEqual(("access", "admin", "campaigns"), plan.enabled_modules)
|
||||
self.assertEqual((), plan.added_dependencies)
|
||||
self.assertEqual(("access",), plan.added_dependencies)
|
||||
|
||||
def test_module_install_plan_is_saved_alongside_desired_state(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-module-install-plan-", dir=_TEST_ROOT))
|
||||
@@ -3300,7 +3347,10 @@ finally:
|
||||
"version_min": "0.2.0",
|
||||
"version_max_exclusive": "0.3.0",
|
||||
}, modules["campaigns"]["requires_interfaces"])
|
||||
self.assertEqual(["files", "mail", "notifications", "addresses"], modules["campaigns"]["optional_dependencies"])
|
||||
self.assertEqual(
|
||||
["files", "mail", "notifications", "addresses", "postbox"],
|
||||
modules["campaigns"]["optional_dependencies"],
|
||||
)
|
||||
self.assertEqual("requires_review", modules["files"]["migration_safety"])
|
||||
self.assertIn("migration", modules["files"]["migration_notes"].lower())
|
||||
self.assertEqual("0.1.9", modules["files"]["version"])
|
||||
@@ -3946,8 +3996,10 @@ finally:
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload_modules = {item["id"] for item in response.json()["modules"]}
|
||||
self.assertEqual(response.status_code, 401, response.text)
|
||||
payload_modules = {
|
||||
item.id for item in app.state.govoplan_registry.manifests()
|
||||
}
|
||||
self.assertEqual({"access", "files"}, payload_modules)
|
||||
|
||||
def test_create_app_ignores_saved_desired_modules_that_are_not_installed(self) -> None:
|
||||
@@ -3979,8 +4031,10 @@ finally:
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload_modules = {item["id"] for item in response.json()["modules"]}
|
||||
self.assertEqual(response.status_code, 401, response.text)
|
||||
payload_modules = {
|
||||
item.id for item in app.state.govoplan_registry.manifests()
|
||||
}
|
||||
self.assertEqual({"access", "files"}, payload_modules)
|
||||
|
||||
def test_create_app_preserves_admin_when_configured_and_saved_state_is_older(self) -> None:
|
||||
@@ -4012,8 +4066,10 @@ finally:
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload_modules = {item["id"] for item in response.json()["modules"]}
|
||||
self.assertEqual(response.status_code, 401, response.text)
|
||||
payload_modules = {
|
||||
item.id for item in app.state.govoplan_registry.manifests()
|
||||
}
|
||||
self.assertEqual({"access", "admin", "files"}, payload_modules)
|
||||
|
||||
def test_module_lifecycle_enables_and_disables_routes_without_restart(self) -> None:
|
||||
@@ -4023,27 +4079,37 @@ finally:
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual({"access"}, {item["id"] for item in response.json()["modules"]})
|
||||
self.assertEqual(response.status_code, 503, response.text)
|
||||
self.assertEqual((), lifecycle.active_module_ids())
|
||||
self.assertFalse("/api/v1/files" in _route_paths(app))
|
||||
|
||||
result = lifecycle.apply_enabled_modules(("files",), migrate=False)
|
||||
self.assertEqual(("files",), result.activated_modules)
|
||||
self.assertEqual(("access", "files"), result.activated_modules)
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual({"access", "files"}, {item["id"] for item in response.json()["modules"]})
|
||||
self.assertEqual(response.status_code, 401, response.text)
|
||||
self.assertEqual(("access", "files"), lifecycle.active_module_ids())
|
||||
self.assertTrue("/api/v1/files" in _route_paths(app))
|
||||
self.assertEqual(401, client.get("/api/v1/files").status_code)
|
||||
|
||||
result = lifecycle.apply_enabled_modules((), migrate=False)
|
||||
self.assertEqual(("files",), result.deactivated_modules)
|
||||
self.assertEqual(("access", "files"), result.deactivated_modules)
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual({"access"}, {item["id"] for item in response.json()["modules"]})
|
||||
self.assertEqual(response.status_code, 503, response.text)
|
||||
self.assertEqual((), lifecycle.active_module_ids())
|
||||
disabled_response = client.get("/api/v1/files")
|
||||
self.assertEqual(404, disabled_response.status_code)
|
||||
self.assertEqual("Module is disabled: files", disabled_response.json()["detail"])
|
||||
|
||||
def test_module_lifecycle_live_apply_defaults_by_environment(self) -> None:
|
||||
app, settings = self._app_for_modules(())
|
||||
lifecycle = app.state.govoplan_lifecycle
|
||||
|
||||
self.assertTrue(lifecycle.live_apply_enabled())
|
||||
settings.app_env = "production"
|
||||
self.assertFalse(lifecycle.live_apply_enabled())
|
||||
settings.module_live_apply_enabled = True
|
||||
self.assertTrue(lifecycle.live_apply_enabled())
|
||||
|
||||
|
||||
def test_module_permutations_start_when_absent_modules_are_physically_unavailable(self) -> None:
|
||||
cases = (
|
||||
@@ -4073,10 +4139,9 @@ finally:
|
||||
|
||||
def test_module_migrations_are_registered_only_for_enabled_modules(self) -> None:
|
||||
core_plan = migration_metadata_plan(build_platform_registry(()))
|
||||
self.assertEqual(1, len(core_plan.script_locations))
|
||||
self.assertTrue(core_plan.script_locations[0].endswith("govoplan_access/backend/migrations/versions"))
|
||||
self.assertEqual(1, len(core_plan.metadata))
|
||||
self.assertEqual(("access",), tuple(item.module_id for item in core_plan.modules))
|
||||
self.assertEqual((), core_plan.script_locations)
|
||||
self.assertEqual((), core_plan.metadata)
|
||||
self.assertEqual((), core_plan.modules)
|
||||
|
||||
files_plan = migration_metadata_plan(build_platform_registry(("files",)))
|
||||
self.assertEqual(2, len(files_plan.script_locations))
|
||||
|
||||
119
tests/test_postbox_contract.py
Normal file
119
tests/test_postbox_contract.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.postbox import (
|
||||
CAPABILITY_POSTBOX_ACCESS,
|
||||
CAPABILITY_POSTBOX_DELIVERY,
|
||||
CAPABILITY_POSTBOX_DIRECTORY,
|
||||
CAPABILITY_POSTBOX_EVIDENCE,
|
||||
CAPABILITY_POSTBOX_MESSAGES,
|
||||
PostboxAccessProvider,
|
||||
PostboxDeliveryProvider,
|
||||
PostboxDirectoryProvider,
|
||||
PostboxEvidenceProvider,
|
||||
PostboxMessagesProvider,
|
||||
postbox_access_provider,
|
||||
postbox_delivery_provider,
|
||||
postbox_directory_provider,
|
||||
postbox_evidence_provider,
|
||||
postbox_messages_provider,
|
||||
)
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
|
||||
|
||||
class _PostboxProvider:
|
||||
def list_visible_postboxes(self, session, *, tenant_id, actor):
|
||||
del session, tenant_id, actor
|
||||
return ()
|
||||
|
||||
def resolve_postbox(self, session, *, tenant_id, target, materialize=False):
|
||||
del session, tenant_id, target, materialize
|
||||
return None
|
||||
|
||||
def delivery_catalog(self, session, *, tenant_id):
|
||||
del session, tenant_id
|
||||
return None
|
||||
|
||||
def explain_access(self, session, *, tenant_id, postbox_id, actor, action):
|
||||
del session, tenant_id, postbox_id, actor, action
|
||||
return None
|
||||
|
||||
def list_messages(
|
||||
self,
|
||||
session,
|
||||
*,
|
||||
tenant_id,
|
||||
postbox_ids,
|
||||
actor,
|
||||
limit=100,
|
||||
offset=0,
|
||||
):
|
||||
del session, tenant_id, postbox_ids, actor, limit, offset
|
||||
return ()
|
||||
|
||||
def get_message(self, session, *, tenant_id, message_id, actor):
|
||||
del session, tenant_id, message_id, actor
|
||||
return None
|
||||
|
||||
def mark_message(self, session, *, tenant_id, message_id, actor, state):
|
||||
del session, tenant_id, message_id, actor, state
|
||||
return None
|
||||
|
||||
def deliver(self, session, request):
|
||||
del session, request
|
||||
return None
|
||||
|
||||
def link_evidence(self, session, *, tenant_id, message_id, attachment):
|
||||
del session, tenant_id, message_id, attachment
|
||||
return None
|
||||
|
||||
|
||||
class PostboxContractTests(unittest.TestCase):
|
||||
def test_protocols_and_registry_helpers_resolve_without_module_imports(self) -> None:
|
||||
provider = _PostboxProvider()
|
||||
self.assertIsInstance(provider, PostboxDirectoryProvider)
|
||||
self.assertIsInstance(provider, PostboxAccessProvider)
|
||||
self.assertIsInstance(provider, PostboxMessagesProvider)
|
||||
self.assertIsInstance(provider, PostboxDeliveryProvider)
|
||||
self.assertIsInstance(provider, PostboxEvidenceProvider)
|
||||
|
||||
registry = PlatformRegistry()
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id="postbox_contract_test",
|
||||
name="Postbox contract test",
|
||||
version="test",
|
||||
capability_factories={
|
||||
name: lambda context: provider
|
||||
for name in (
|
||||
CAPABILITY_POSTBOX_DIRECTORY,
|
||||
CAPABILITY_POSTBOX_ACCESS,
|
||||
CAPABILITY_POSTBOX_MESSAGES,
|
||||
CAPABILITY_POSTBOX_DELIVERY,
|
||||
CAPABILITY_POSTBOX_EVIDENCE,
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
registry.configure_capability_context(
|
||||
ModuleContext(registry=registry, settings=object())
|
||||
)
|
||||
|
||||
self.assertIs(provider, postbox_directory_provider(registry))
|
||||
self.assertIs(provider, postbox_access_provider(registry))
|
||||
self.assertIs(provider, postbox_messages_provider(registry))
|
||||
self.assertIs(provider, postbox_delivery_provider(registry))
|
||||
self.assertIs(provider, postbox_evidence_provider(registry))
|
||||
|
||||
empty = PlatformRegistry()
|
||||
self.assertIsNone(postbox_directory_provider(empty))
|
||||
self.assertIsNone(postbox_access_provider(empty))
|
||||
self.assertIsNone(postbox_messages_provider(empty))
|
||||
self.assertIsNone(postbox_delivery_provider(empty))
|
||||
self.assertIsNone(postbox_evidence_provider(empty))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -15,6 +15,55 @@ from govoplan_core.server.fastapi import create_govoplan_app
|
||||
|
||||
|
||||
class QueryMetricsTests(TestCase):
|
||||
def test_non_sqlite_engine_uses_bounded_pool_configuration(self) -> None:
|
||||
configured = {
|
||||
"GOVOPLAN_DB_POOL_SIZE": "8",
|
||||
"GOVOPLAN_DB_MAX_OVERFLOW": "16",
|
||||
"GOVOPLAN_DB_POOL_TIMEOUT_SECONDS": "45",
|
||||
"GOVOPLAN_DB_POOL_RECYCLE_SECONDS": "900",
|
||||
}
|
||||
engine = object()
|
||||
with (
|
||||
patch.dict(os.environ, configured, clear=False),
|
||||
patch(
|
||||
"govoplan_core.db.session.create_engine",
|
||||
return_value=engine,
|
||||
) as create_engine,
|
||||
patch(
|
||||
"govoplan_core.db.session.instrument_engine",
|
||||
side_effect=lambda value: value,
|
||||
),
|
||||
):
|
||||
result = create_database_engine(
|
||||
"postgresql+psycopg://govoplan@example/govoplan",
|
||||
)
|
||||
|
||||
self.assertIs(engine, result)
|
||||
create_engine.assert_called_once_with(
|
||||
"postgresql+psycopg://govoplan@example/govoplan",
|
||||
pool_pre_ping=True,
|
||||
connect_args={},
|
||||
pool_size=8,
|
||||
max_overflow=16,
|
||||
pool_timeout=45,
|
||||
pool_recycle=900,
|
||||
)
|
||||
|
||||
def test_non_sqlite_engine_rejects_pool_values_outside_safe_bounds(self) -> None:
|
||||
invalid_values = (
|
||||
("GOVOPLAN_DB_POOL_SIZE", "0"),
|
||||
("GOVOPLAN_DB_MAX_OVERFLOW", "201"),
|
||||
("GOVOPLAN_DB_POOL_TIMEOUT_SECONDS", "not-a-number"),
|
||||
("GOVOPLAN_DB_POOL_RECYCLE_SECONDS", "86401"),
|
||||
)
|
||||
for name, value in invalid_values:
|
||||
with self.subTest(name=name, value=value):
|
||||
with patch.dict(os.environ, {name: value}, clear=False):
|
||||
with self.assertRaisesRegex(ValueError, name):
|
||||
create_database_engine(
|
||||
"postgresql+psycopg://govoplan@example/govoplan",
|
||||
)
|
||||
|
||||
def test_collect_query_metrics_counts_instrumented_engine_queries(self) -> None:
|
||||
engine = create_database_engine("sqlite:///:memory:")
|
||||
try:
|
||||
|
||||
173
tests/test_reference_options.py
Normal file
173
tests/test_reference_options.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from govoplan_core.core.access import (
|
||||
CAPABILITY_ACCESS_DIRECTORY,
|
||||
AccountRef,
|
||||
GroupRef,
|
||||
UserRef,
|
||||
)
|
||||
from govoplan_core.core.references import (
|
||||
access_scope_reference_options,
|
||||
validate_access_scope_reference,
|
||||
)
|
||||
|
||||
|
||||
class _Directory:
|
||||
users = (
|
||||
UserRef(
|
||||
id="membership-1",
|
||||
account_id="account-1",
|
||||
tenant_id="tenant-1",
|
||||
email="ada@example.test",
|
||||
display_name="Ada",
|
||||
),
|
||||
UserRef(
|
||||
id="membership-2",
|
||||
account_id="account-2",
|
||||
tenant_id="tenant-1",
|
||||
email="inactive@example.test",
|
||||
display_name="Inactive",
|
||||
status="inactive",
|
||||
),
|
||||
)
|
||||
groups = (
|
||||
GroupRef(id="group-1", tenant_id="tenant-1", name="Finance"),
|
||||
GroupRef(
|
||||
id="group-2",
|
||||
tenant_id="tenant-1",
|
||||
name="Former team",
|
||||
status="inactive",
|
||||
),
|
||||
)
|
||||
|
||||
def get_account(self, account_id):
|
||||
if account_id not in {"account-1", "account-2"}:
|
||||
return None
|
||||
return AccountRef(id=account_id, email=f"{account_id}@example.test")
|
||||
|
||||
def get_user(self, user_id):
|
||||
return next((item for item in self.users if item.id == user_id), None)
|
||||
|
||||
def get_users(self, user_ids):
|
||||
return {item.id: item for item in self.users if item.id in set(user_ids)}
|
||||
|
||||
def users_for_tenant(self, tenant_id):
|
||||
return tuple(item for item in self.users if item.tenant_id == tenant_id)
|
||||
|
||||
def get_group(self, group_id):
|
||||
return next((item for item in self.groups if item.id == group_id), None)
|
||||
|
||||
def get_groups(self, group_ids):
|
||||
return {item.id: item for item in self.groups if item.id in set(group_ids)}
|
||||
|
||||
def groups_for_tenant(self, tenant_id):
|
||||
return tuple(item for item in self.groups if item.tenant_id == tenant_id)
|
||||
|
||||
def groups_for_user(self, user_id, *, tenant_id):
|
||||
del user_id
|
||||
return self.groups_for_tenant(tenant_id)
|
||||
|
||||
def display_label(self, subject):
|
||||
return subject.label
|
||||
|
||||
|
||||
class _Registry:
|
||||
directory = _Directory()
|
||||
|
||||
def has_capability(self, name):
|
||||
return name == CAPABILITY_ACCESS_DIRECTORY
|
||||
|
||||
def capability(self, name):
|
||||
return self.directory if self.has_capability(name) else None
|
||||
|
||||
|
||||
class ReferenceOptionTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.principal = SimpleNamespace(
|
||||
tenant_id="tenant-1",
|
||||
account_id="account-1",
|
||||
display_name="Ada",
|
||||
email="ada@example.test",
|
||||
group_ids=frozenset({"group-1"}),
|
||||
)
|
||||
|
||||
def test_user_values_are_canonical_account_ids(self) -> None:
|
||||
options = access_scope_reference_options(
|
||||
_Registry(),
|
||||
self.principal,
|
||||
scope_type="user",
|
||||
administrative=True,
|
||||
)
|
||||
|
||||
self.assertEqual(["account-1", "account-2"], [item.value for item in options])
|
||||
self.assertEqual("membership-1", options[0].provenance["membership_id"])
|
||||
self.assertTrue(options[1].disabled)
|
||||
self.assertEqual("inactive", options[1].availability)
|
||||
|
||||
def test_unavailable_selected_values_remain_readable(self) -> None:
|
||||
options = access_scope_reference_options(
|
||||
_Registry(),
|
||||
self.principal,
|
||||
scope_type="group",
|
||||
query="missing",
|
||||
selected_values=("removed-group",),
|
||||
administrative=True,
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(options))
|
||||
self.assertEqual("removed-group", options[0].value)
|
||||
self.assertEqual("unavailable", options[0].availability)
|
||||
self.assertTrue(options[0].disabled)
|
||||
|
||||
def test_non_administrators_only_receive_permitted_scope_targets(self) -> None:
|
||||
users = access_scope_reference_options(
|
||||
_Registry(),
|
||||
self.principal,
|
||||
scope_type="user",
|
||||
administrative=False,
|
||||
)
|
||||
groups = access_scope_reference_options(
|
||||
_Registry(),
|
||||
self.principal,
|
||||
scope_type="group",
|
||||
administrative=False,
|
||||
)
|
||||
|
||||
self.assertEqual(["account-1"], [item.value for item in users])
|
||||
self.assertEqual(["group-1"], [item.value for item in groups])
|
||||
|
||||
def test_inactive_targets_are_rejected_but_existing_values_survive(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "Inactive groups"):
|
||||
validate_access_scope_reference(
|
||||
_Registry(),
|
||||
tenant_id="tenant-1",
|
||||
scope_type="group",
|
||||
scope_id="group-2",
|
||||
)
|
||||
self.assertEqual(
|
||||
"group-2",
|
||||
validate_access_scope_reference(
|
||||
_Registry(),
|
||||
tenant_id="tenant-1",
|
||||
scope_type="group",
|
||||
scope_id="group-2",
|
||||
preserve_existing="group-2",
|
||||
),
|
||||
)
|
||||
|
||||
def test_missing_optional_access_provider_keeps_reduced_mode(self) -> None:
|
||||
options = access_scope_reference_options(
|
||||
None,
|
||||
self.principal,
|
||||
scope_type="user",
|
||||
)
|
||||
|
||||
self.assertEqual("account-1", options[0].value)
|
||||
self.assertEqual("core", options[0].source_module)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5,6 +5,7 @@ import unittest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from govoplan_core.auth import get_api_principal
|
||||
from govoplan_core.core.modules import (
|
||||
FrontendModule,
|
||||
FrontendRoute,
|
||||
@@ -218,6 +219,7 @@ class ViewSurfaceContractTests(unittest.TestCase):
|
||||
app = FastAPI()
|
||||
app.state.govoplan_registry = registry
|
||||
app.include_router(create_platform_router(), prefix="/api/v1")
|
||||
app.dependency_overrides[get_api_principal] = lambda: object()
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
|
||||
@@ -13,8 +13,6 @@ import unittest
|
||||
class WheelRuntimeTests(unittest.TestCase):
|
||||
def test_built_wheel_contains_and_runs_core_migrations_without_source_checkout(self) -> None:
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
access_repository = repository_root.parent / "govoplan-access"
|
||||
self.assertTrue(access_repository.is_dir(), "wheel runtime check requires the mandatory Access repository")
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-core-wheel-") as directory:
|
||||
temporary_root = Path(directory)
|
||||
wheel_directory = temporary_root / "wheels"
|
||||
@@ -30,21 +28,8 @@ class WheelRuntimeTests(unittest.TestCase):
|
||||
str(repository_root),
|
||||
cwd=temporary_root,
|
||||
)
|
||||
self._run(
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"wheel",
|
||||
"--no-deps",
|
||||
"--wheel-dir",
|
||||
str(wheel_directory),
|
||||
str(access_repository),
|
||||
cwd=temporary_root,
|
||||
)
|
||||
core_wheels = tuple(wheel_directory.glob("govoplan_core-*.whl"))
|
||||
access_wheels = tuple(wheel_directory.glob("govoplan_access-*.whl"))
|
||||
self.assertEqual(1, len(core_wheels))
|
||||
self.assertEqual(1, len(access_wheels))
|
||||
|
||||
virtual_environment = temporary_root / "venv"
|
||||
self._run(sys.executable, "-m", "venv", str(virtual_environment), cwd=temporary_root)
|
||||
@@ -56,7 +41,6 @@ class WheelRuntimeTests(unittest.TestCase):
|
||||
"install",
|
||||
"--no-deps",
|
||||
str(core_wheels[0]),
|
||||
str(access_wheels[0]),
|
||||
cwd=temporary_root,
|
||||
)
|
||||
|
||||
@@ -85,7 +69,7 @@ class WheelRuntimeTests(unittest.TestCase):
|
||||
self.assertNotEqual(repository_root, runtime_root)
|
||||
self.assertTrue((runtime_root / "alembic.ini").is_file())
|
||||
self.assertTrue((runtime_root / "alembic" / "env.py").is_file())
|
||||
self.assertIn("4f2a9c8e7b6d", result["heads"])
|
||||
self.assertEqual(["c91f0a72be34"], result["heads"])
|
||||
self.assertIn("core_scopes", result["tables"])
|
||||
self.assertIn("core_system_settings", result["tables"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user