feat: add governed ownership workflows and admin tree navigation
This commit is contained in:
430
tests/test_ownership.py
Normal file
430
tests/test_ownership.py
Normal file
@@ -0,0 +1,430 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from govoplan_core.core.modules import ModuleManifest
|
||||
from govoplan_core.core.ownership import (
|
||||
OwnershipActionDecision,
|
||||
OwnershipIdempotencyConflict,
|
||||
OwnershipProviderRegistration,
|
||||
OwnershipResourceRef,
|
||||
OwnershipSubjectRef,
|
||||
OwnershipTransfer,
|
||||
OwnershipTransferError,
|
||||
OwnershipTransferExpired,
|
||||
OwnershipTransferPolicy,
|
||||
OwnershipTransferStatus,
|
||||
accept_ownership_transfer,
|
||||
approve_administrative_recovery,
|
||||
approve_ownership_request,
|
||||
execute_administrative_recovery,
|
||||
request_ownership,
|
||||
start_administrative_recovery,
|
||||
start_owner_initiated_transfer,
|
||||
)
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.runtime import (
|
||||
clear_runtime,
|
||||
configure_runtime,
|
||||
get_runtime_context,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
|
||||
|
||||
NOW = datetime(2026, 7, 30, 10, 0, tzinfo=timezone.utc)
|
||||
RESOURCE = OwnershipResourceRef(
|
||||
module_id="campaigns",
|
||||
resource_type="campaign",
|
||||
resource_id="campaign-1",
|
||||
)
|
||||
OWNER = OwnershipSubjectRef(type="account", id="owner-1")
|
||||
TARGET = OwnershipSubjectRef(type="account", id="target-1")
|
||||
|
||||
|
||||
class _OwnershipProvider:
|
||||
def __init__(self) -> None:
|
||||
self.owner = OWNER
|
||||
self.applied: list[tuple[OwnershipSubjectRef, OwnershipSubjectRef]] = []
|
||||
|
||||
def current_owner(self, session, *, tenant_id: str, resource_id: str):
|
||||
if tenant_id != "tenant-1" or resource_id != RESOURCE.resource_id:
|
||||
return None
|
||||
return self.owner
|
||||
|
||||
def authorize_ownership_action(
|
||||
self,
|
||||
session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
resource_id: str,
|
||||
action: str,
|
||||
actor: OwnershipSubjectRef,
|
||||
current_owner: OwnershipSubjectRef,
|
||||
target_owner: OwnershipSubjectRef,
|
||||
) -> OwnershipActionDecision:
|
||||
allowed = False
|
||||
if action in {"propose_transfer", "approve_requested_transfer"}:
|
||||
allowed = actor.id == current_owner.id
|
||||
elif action == "request_ownership":
|
||||
allowed = actor.id == target_owner.id
|
||||
elif action == "accept_transfer":
|
||||
allowed = actor.id == target_owner.id
|
||||
elif action == "accept_group_transfer":
|
||||
allowed = actor.id == "group-manager-1"
|
||||
elif action in {
|
||||
"request_recovery",
|
||||
"approve_recovery",
|
||||
"execute_recovery",
|
||||
}:
|
||||
allowed = actor.id.startswith("recovery-officer-")
|
||||
elif action in {"decline_transfer", "cancel_transfer"}:
|
||||
allowed = actor.id in {current_owner.id, target_owner.id}
|
||||
return OwnershipActionDecision(
|
||||
allowed=allowed,
|
||||
reason=None if allowed else f"Not authorized for {action}",
|
||||
)
|
||||
|
||||
def apply_owner(
|
||||
self,
|
||||
session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
resource_id: str,
|
||||
expected_owner: OwnershipSubjectRef,
|
||||
target_owner: OwnershipSubjectRef,
|
||||
actor: OwnershipSubjectRef,
|
||||
reason: str | None,
|
||||
) -> None:
|
||||
if self.owner != expected_owner:
|
||||
raise OwnershipTransferError("Owner changed while transfer was pending")
|
||||
self.applied.append((expected_owner, target_owner))
|
||||
self.owner = target_owner
|
||||
|
||||
|
||||
class OwnershipTransferTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.runtime_context = get_runtime_context()
|
||||
clear_runtime()
|
||||
self.engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(
|
||||
self.engine,
|
||||
tables=[OwnershipTransfer.__table__],
|
||||
)
|
||||
self.Session = sessionmaker(bind=self.engine, expire_on_commit=False)
|
||||
self.session: Session = self.Session()
|
||||
self.provider = _OwnershipProvider()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session.close()
|
||||
Base.metadata.drop_all(
|
||||
self.engine,
|
||||
tables=[OwnershipTransfer.__table__],
|
||||
)
|
||||
self.engine.dispose()
|
||||
if self.runtime_context is not None:
|
||||
configure_runtime(self.runtime_context)
|
||||
|
||||
def test_owner_initiated_transfer_requires_target_acceptance(self) -> None:
|
||||
transfer = start_owner_initiated_transfer(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=OWNER,
|
||||
target_owner=TARGET,
|
||||
idempotency_key="owner-transfer-1",
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
transfer.status,
|
||||
OwnershipTransferStatus.AWAITING_TARGET_ACCEPTANCE.value,
|
||||
)
|
||||
self.assertEqual(self.provider.owner, OWNER)
|
||||
|
||||
accept_ownership_transfer(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=TARGET,
|
||||
now=NOW + timedelta(hours=1),
|
||||
)
|
||||
|
||||
self.assertEqual(transfer.status, OwnershipTransferStatus.COMPLETED.value)
|
||||
self.assertEqual(self.provider.owner, TARGET)
|
||||
self.assertEqual(transfer.target_accepted_by_id, TARGET.id)
|
||||
self.assertEqual(
|
||||
["started", "accepted"],
|
||||
[decision["action"] for decision in transfer.decisions],
|
||||
)
|
||||
|
||||
def test_requested_transfer_requires_owner_then_requester(self) -> None:
|
||||
transfer = request_ownership(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=TARGET,
|
||||
idempotency_key="request-transfer-1",
|
||||
now=NOW,
|
||||
)
|
||||
self.assertEqual(
|
||||
transfer.status,
|
||||
OwnershipTransferStatus.AWAITING_OWNER_APPROVAL.value,
|
||||
)
|
||||
|
||||
approve_ownership_request(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=OWNER,
|
||||
now=NOW + timedelta(minutes=5),
|
||||
)
|
||||
self.assertEqual(
|
||||
transfer.status,
|
||||
OwnershipTransferStatus.AWAITING_TARGET_ACCEPTANCE.value,
|
||||
)
|
||||
self.assertEqual(self.provider.owner, OWNER)
|
||||
|
||||
accept_ownership_transfer(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=TARGET,
|
||||
now=NOW + timedelta(minutes=10),
|
||||
)
|
||||
self.assertEqual(self.provider.owner, TARGET)
|
||||
|
||||
def test_idempotency_key_replays_only_the_same_request(self) -> None:
|
||||
first = start_owner_initiated_transfer(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=OWNER,
|
||||
target_owner=TARGET,
|
||||
idempotency_key="same-key",
|
||||
now=NOW,
|
||||
)
|
||||
replay = start_owner_initiated_transfer(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=OWNER,
|
||||
target_owner=TARGET,
|
||||
idempotency_key="same-key",
|
||||
now=NOW,
|
||||
)
|
||||
self.assertEqual(replay.id, first.id)
|
||||
|
||||
with self.assertRaises(OwnershipIdempotencyConflict):
|
||||
start_owner_initiated_transfer(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=OWNER,
|
||||
target_owner=OwnershipSubjectRef(type="account", id="other"),
|
||||
idempotency_key="same-key",
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
def test_group_target_acceptance_uses_dedicated_authority(self) -> None:
|
||||
group = OwnershipSubjectRef(type="group", id="group-1")
|
||||
transfer = start_owner_initiated_transfer(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=OWNER,
|
||||
target_owner=group,
|
||||
idempotency_key="group-transfer",
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
accept_ownership_transfer(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=OwnershipSubjectRef(type="account", id="group-manager-1"),
|
||||
now=NOW + timedelta(minutes=10),
|
||||
)
|
||||
|
||||
self.assertEqual(self.provider.owner, group)
|
||||
self.assertEqual(transfer.target_accepted_by_id, "group-manager-1")
|
||||
|
||||
def test_standard_recovery_requires_quorum_and_delay(self) -> None:
|
||||
policy = OwnershipTransferPolicy()
|
||||
target = OwnershipSubjectRef(type="account", id="recovered-owner")
|
||||
transfer = start_administrative_recovery(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=OwnershipSubjectRef(
|
||||
type="account",
|
||||
id="recovery-officer-1",
|
||||
recently_authenticated=True,
|
||||
),
|
||||
target_owner=target,
|
||||
idempotency_key="recovery-1",
|
||||
reason="Owner is unavailable",
|
||||
policy=policy,
|
||||
now=NOW,
|
||||
)
|
||||
self.assertFalse(transfer.metadata_["encryption_keys_included"])
|
||||
|
||||
approve_administrative_recovery(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=OwnershipSubjectRef(
|
||||
type="account",
|
||||
id="recovery-officer-1",
|
||||
recently_authenticated=True,
|
||||
),
|
||||
now=NOW,
|
||||
)
|
||||
self.assertEqual(
|
||||
transfer.status,
|
||||
OwnershipTransferStatus.AWAITING_RECOVERY_APPROVALS.value,
|
||||
)
|
||||
approve_administrative_recovery(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=OwnershipSubjectRef(
|
||||
type="account",
|
||||
id="recovery-officer-2",
|
||||
recently_authenticated=True,
|
||||
),
|
||||
now=NOW + timedelta(minutes=1),
|
||||
)
|
||||
self.assertEqual(
|
||||
transfer.status,
|
||||
OwnershipTransferStatus.RECOVERY_SCHEDULED.value,
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
OwnershipTransferError,
|
||||
"delay has not elapsed",
|
||||
):
|
||||
execute_administrative_recovery(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=OwnershipSubjectRef(
|
||||
type="account",
|
||||
id="recovery-officer-3",
|
||||
recently_authenticated=True,
|
||||
),
|
||||
now=NOW + timedelta(hours=23),
|
||||
)
|
||||
|
||||
execute_administrative_recovery(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=OwnershipSubjectRef(
|
||||
type="account",
|
||||
id="recovery-officer-3",
|
||||
recently_authenticated=True,
|
||||
),
|
||||
now=NOW + timedelta(hours=24),
|
||||
)
|
||||
self.assertEqual(self.provider.owner, target)
|
||||
self.assertEqual(
|
||||
[
|
||||
"started",
|
||||
"recovery_approved",
|
||||
"recovery_approved",
|
||||
"recovery_executed",
|
||||
],
|
||||
[decision["action"] for decision in transfer.decisions],
|
||||
)
|
||||
self.assertEqual(
|
||||
2,
|
||||
transfer.decisions[0]["details"]["required_approvals"],
|
||||
)
|
||||
|
||||
def test_standard_recovery_rejects_stale_authentication_context(self) -> None:
|
||||
with self.assertRaisesRegex(
|
||||
PermissionError,
|
||||
"recent authentication",
|
||||
):
|
||||
start_administrative_recovery(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=OwnershipSubjectRef(
|
||||
type="account",
|
||||
id="recovery-officer-1",
|
||||
),
|
||||
target_owner=OwnershipSubjectRef(
|
||||
type="account",
|
||||
id="recovered-owner",
|
||||
),
|
||||
idempotency_key="stale-recovery",
|
||||
reason="Owner is unavailable",
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
def test_expiry_is_recorded_before_a_late_decision_is_rejected(self) -> None:
|
||||
transfer = start_owner_initiated_transfer(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
resource=RESOURCE,
|
||||
provider=self.provider,
|
||||
actor=OWNER,
|
||||
target_owner=TARGET,
|
||||
idempotency_key="expiring-transfer",
|
||||
expiry_days=1,
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
with self.assertRaises(OwnershipTransferExpired):
|
||||
accept_ownership_transfer(
|
||||
self.session,
|
||||
transfer=transfer,
|
||||
provider=self.provider,
|
||||
actor=TARGET,
|
||||
now=NOW + timedelta(days=2),
|
||||
)
|
||||
self.session.commit()
|
||||
|
||||
self.assertEqual(
|
||||
OwnershipTransferStatus.EXPIRED.value,
|
||||
transfer.status,
|
||||
)
|
||||
self.assertEqual(
|
||||
["started", "expired"],
|
||||
[decision["action"] for decision in transfer.decisions],
|
||||
)
|
||||
|
||||
def test_registry_resolves_one_provider_per_resource_type(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id="campaigns",
|
||||
name="Campaigns",
|
||||
version="1.0.0",
|
||||
ownership_providers=(
|
||||
OwnershipProviderRegistration(
|
||||
resource_type="campaign",
|
||||
provider=self.provider,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIs(registry.ownership_provider("CAMPAIGN"), self.provider)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
216
tests/test_ownership_api.py
Normal file
216
tests/test_ownership_api.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.modules import ModuleManifest
|
||||
from govoplan_core.core.ownership import (
|
||||
OwnershipActionDecision,
|
||||
OwnershipProviderRegistration,
|
||||
OwnershipSubjectRef,
|
||||
OwnershipTransfer,
|
||||
OwnershipTransferError,
|
||||
)
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.runtime import (
|
||||
clear_runtime,
|
||||
configure_runtime,
|
||||
get_runtime_context,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_core.server.ownership import router
|
||||
|
||||
|
||||
class _Provider:
|
||||
def __init__(self) -> None:
|
||||
self.owner = OwnershipSubjectRef(type="user", id="owner-1")
|
||||
|
||||
def current_owner(self, session, *, tenant_id: str, resource_id: str):
|
||||
if tenant_id == "tenant-1" and resource_id == "resource-1":
|
||||
return self.owner
|
||||
return None
|
||||
|
||||
def authorize_ownership_action(
|
||||
self,
|
||||
session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
resource_id: str,
|
||||
action: str,
|
||||
actor: OwnershipSubjectRef,
|
||||
current_owner: OwnershipSubjectRef,
|
||||
target_owner: OwnershipSubjectRef,
|
||||
) -> OwnershipActionDecision:
|
||||
del session, tenant_id, resource_id
|
||||
if action in {
|
||||
"propose_transfer",
|
||||
"approve_requested_transfer",
|
||||
"cancel_transfer",
|
||||
}:
|
||||
allowed = actor.id == current_owner.id
|
||||
elif action in {
|
||||
"request_ownership",
|
||||
"accept_transfer",
|
||||
"decline_transfer",
|
||||
}:
|
||||
allowed = actor.id == target_owner.id
|
||||
elif action == "view_transfer":
|
||||
allowed = actor.id in {current_owner.id, target_owner.id}
|
||||
else:
|
||||
allowed = False
|
||||
return OwnershipActionDecision(allowed=allowed)
|
||||
|
||||
def apply_owner(
|
||||
self,
|
||||
session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
resource_id: str,
|
||||
expected_owner: OwnershipSubjectRef,
|
||||
target_owner: OwnershipSubjectRef,
|
||||
actor: OwnershipSubjectRef,
|
||||
reason: str | None,
|
||||
) -> None:
|
||||
del session, tenant_id, resource_id, actor, reason
|
||||
if (
|
||||
self.owner.type != expected_owner.type
|
||||
or self.owner.id != expected_owner.id
|
||||
):
|
||||
raise OwnershipTransferError("Owner changed")
|
||||
self.owner = target_owner
|
||||
|
||||
|
||||
class OwnershipApiTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.runtime_context = get_runtime_context()
|
||||
clear_runtime()
|
||||
self.engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(
|
||||
self.engine,
|
||||
tables=[OwnershipTransfer.__table__],
|
||||
)
|
||||
self.session = sessionmaker(
|
||||
bind=self.engine,
|
||||
expire_on_commit=False,
|
||||
)()
|
||||
self.provider = _Provider()
|
||||
registry = PlatformRegistry()
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id="example",
|
||||
name="Example",
|
||||
version="1.0.0",
|
||||
ownership_providers=(
|
||||
OwnershipProviderRegistration(
|
||||
resource_type="example",
|
||||
provider=self.provider,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
self.principal = _principal("owner-1")
|
||||
app = FastAPI()
|
||||
app.state.govoplan_registry = registry
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
app.dependency_overrides[get_session] = lambda: self.session
|
||||
app.dependency_overrides[get_api_principal] = lambda: self.principal
|
||||
self.client = TestClient(app)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.client.close()
|
||||
self.session.close()
|
||||
Base.metadata.drop_all(
|
||||
self.engine,
|
||||
tables=[OwnershipTransfer.__table__],
|
||||
)
|
||||
self.engine.dispose()
|
||||
if self.runtime_context is not None:
|
||||
configure_runtime(self.runtime_context)
|
||||
|
||||
def test_owner_transfer_requires_target_acceptance_and_is_listable(self) -> None:
|
||||
created = self.client.post(
|
||||
"/api/v1/ownership/transfers",
|
||||
json={
|
||||
"resource": {
|
||||
"module_id": "example",
|
||||
"resource_type": "example",
|
||||
"resource_id": "resource-1",
|
||||
},
|
||||
"target_owner": {"type": "user", "id": "target-1"},
|
||||
"idempotency_key": "api-transfer-1",
|
||||
"reason": "Handover",
|
||||
},
|
||||
)
|
||||
self.assertEqual(201, created.status_code, created.text)
|
||||
transfer_id = created.json()["id"]
|
||||
self.assertEqual(
|
||||
"awaiting_target_acceptance",
|
||||
created.json()["status"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["started"],
|
||||
[item["action"] for item in created.json()["decisions"]],
|
||||
)
|
||||
|
||||
visible = self.client.get("/api/v1/ownership/transfers")
|
||||
self.assertEqual(200, visible.status_code, visible.text)
|
||||
self.assertEqual([transfer_id], [item["id"] for item in visible.json()["transfers"]])
|
||||
|
||||
self.principal = _principal("target-1")
|
||||
accepted = self.client.post(
|
||||
f"/api/v1/ownership/transfers/{transfer_id}/acceptance"
|
||||
)
|
||||
self.assertEqual(200, accepted.status_code, accepted.text)
|
||||
self.assertEqual("completed", accepted.json()["status"])
|
||||
self.assertEqual("target-1", self.provider.owner.id)
|
||||
self.assertEqual(
|
||||
["started", "accepted"],
|
||||
[item["action"] for item in accepted.json()["decisions"]],
|
||||
)
|
||||
|
||||
def test_resource_module_must_match_registered_provider(self) -> None:
|
||||
response = self.client.post(
|
||||
"/api/v1/ownership/transfers",
|
||||
json={
|
||||
"resource": {
|
||||
"module_id": "wrong-module",
|
||||
"resource_type": "example",
|
||||
"resource_id": "resource-1",
|
||||
},
|
||||
"target_owner": {"type": "user", "id": "target-1"},
|
||||
"idempotency_key": "wrong-provider-module",
|
||||
},
|
||||
)
|
||||
self.assertEqual(404, response.status_code, response.text)
|
||||
|
||||
|
||||
def _principal(user_id: str) -> ApiPrincipal:
|
||||
return ApiPrincipal(
|
||||
principal=PrincipalRef(
|
||||
account_id=f"account-{user_id}",
|
||||
membership_id=user_id,
|
||||
tenant_id="tenant-1",
|
||||
scopes=frozenset({"campaigns:campaign:share"}),
|
||||
),
|
||||
account=SimpleNamespace(id=f"account-{user_id}"),
|
||||
user=SimpleNamespace(id=user_id),
|
||||
auth_session=SimpleNamespace(created_at=datetime.now(timezone.utc)),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -69,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.assertEqual(["c91f0a72be34"], result["heads"])
|
||||
self.assertEqual(["d03a7b9c1e5f"], result["heads"])
|
||||
self.assertIn("core_scopes", result["tables"])
|
||||
self.assertIn("core_system_settings", result["tables"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user