Release v0.1.6

This commit is contained in:
2026-07-07 16:00:38 +02:00
parent a2053518d1
commit 150b720f12
149 changed files with 14311 additions and 8005 deletions

View File

@@ -0,0 +1,577 @@
from __future__ import annotations
import shutil
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from fastapi import APIRouter, Depends
from fastapi.testclient import TestClient
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal
from govoplan_core.core.access import (
ACCESS_CAPABILITY_NAMES,
ACCESS_MODULE_ID,
CAPABILITY_ACCESS_ADMINISTRATION,
CAPABILITY_ACCESS_DIRECTORY,
CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER,
CAPABILITY_ACCESS_PERMISSION_EVALUATOR,
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER,
CAPABILITY_ACCESS_RESOURCE_ACCESS,
CAPABILITY_ACCESS_TENANT_PROVISIONER,
CAPABILITY_AUDIT_SINK,
CAPABILITY_SECURITY_SECRET_PROVIDER,
CAPABILITY_TENANCY_TENANT_RESOLVER,
AccessDecision,
AccessAdministration,
AccessDirectory,
AccessGovernanceMaterializer,
AccessSubjectRef,
AccountRef,
AuditEvent,
AuditSink,
GovernanceTemplateMaterialization,
GroupRef,
PermissionEvaluator,
PrincipalRef,
PrincipalResolver,
ResourceAccessService,
RoleRef,
SecretProvider,
TenantRef,
TenantAccessProvisioner,
TenantOwnerCandidateRef,
TenantResolver,
UserRef,
)
from govoplan_core.core.campaigns import (
CAPABILITY_CAMPAIGNS_ACCESS,
CAPABILITY_CAMPAIGNS_DELIVERY_TASKS,
CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT,
CAPABILITY_CAMPAIGNS_POLICY_CONTEXT,
CAPABILITY_CAMPAIGNS_RETENTION,
CampaignAccessProvider,
CampaignDeliveryTaskProvider,
CampaignMailPolicyContext,
CampaignMailPolicyContextProvider,
CampaignPolicyContext,
CampaignPolicyContextProvider,
CampaignRetentionProvider,
)
from govoplan_core.core.modules import ModuleContext, ModuleManifest
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.db.base import Base
from govoplan_core.db.session import configure_database, get_database
from govoplan_core.server.app import create_app
from govoplan_core.server.config import GovoplanServerConfig
from govoplan_access.backend.db.models import Account, Group, User, UserGroupMembership
from govoplan_tenancy.backend.db.models import Tenant
class _FakeAccessDirectory:
account = AccountRef(id="account-1", email="demo@example.test", display_name="Demo")
user = UserRef(id="user-1", account_id=account.id, tenant_id="tenant-1", email=account.email)
group = GroupRef(id="group-1", tenant_id="tenant-1", name="Team")
def get_account(self, account_id: str) -> AccountRef | None:
return self.account if account_id == self.account.id else None
def get_user(self, user_id: str) -> UserRef | None:
return self.user if user_id == self.user.id else None
def get_users(self, user_ids):
return {self.user.id: self.user} if self.user.id in set(user_ids) else {}
def users_for_tenant(self, tenant_id: str):
return (self.user,) if tenant_id == self.user.tenant_id else ()
def get_group(self, group_id: str) -> GroupRef | None:
return self.group if group_id == self.group.id else None
def get_groups(self, group_ids):
return {self.group.id: self.group} if self.group.id in set(group_ids) else {}
def groups_for_tenant(self, tenant_id: str):
return (self.group,) if tenant_id == self.group.tenant_id else ()
def groups_for_user(self, user_id: str, *, tenant_id: str):
if user_id == self.user.id and tenant_id == self.user.tenant_id:
return (self.group,)
return ()
def display_label(self, subject: AccessSubjectRef) -> str | None:
if subject.kind == "user" and subject.id == self.user.id:
return self.user.display_name or self.user.email
if subject.kind == "group" and subject.id == self.group.id:
return self.group.name
return subject.label
class _FakePrincipalResolver:
def resolve_request(self, request: object, *, session: object | None = None) -> PrincipalRef:
del request, session
return PrincipalRef(
account_id="account-1",
membership_id="user-1",
tenant_id="tenant-1",
scopes=frozenset({"files:file:read"}),
group_ids=frozenset({"group-1"}),
)
class _FakeTenantResolver:
tenant = TenantRef(id="tenant-1", name="Tenant")
def get_tenant(self, tenant_id: str) -> TenantRef | None:
return self.tenant if tenant_id == self.tenant.id else None
def current_tenant(self, principal: PrincipalRef) -> TenantRef | None:
return self.get_tenant(principal.tenant_id or "")
class _FakePermissionEvaluator:
def scopes_grant(self, scopes, required_scope: str) -> bool:
return required_scope in set(scopes)
def has_scope(self, principal: PrincipalRef, required_scope: str) -> bool:
return self.scopes_grant(principal.scopes, required_scope)
def explain_scope(self, principal: PrincipalRef, required_scope: str) -> AccessDecision:
allowed = self.has_scope(principal, required_scope)
return AccessDecision(allowed=allowed, reason=None if allowed else "missing scope", requirements=(required_scope,))
class _FakeResourceAccessService:
def explain(self, principal: PrincipalRef, *, resource_type: str, resource_id: str, action: str) -> AccessDecision:
del principal, resource_type, resource_id, action
return AccessDecision(allowed=True)
class _FakeTenantAccessProvisioner:
def ensure_default_roles(self, session: object, tenant: object | None = None):
del session, tenant
return {}
def tenant_owner_candidates(self, session: object):
del session
return (TenantOwnerCandidateRef(account_id="account-1", email="demo@example.test", display_name="Demo"),)
def ensure_tenant_owner_membership(self, session: object, *, tenant: object, owner_account_id: str) -> str:
del session, tenant, owner_account_id
return "user-1"
class _FakeAccessAdministration:
def system_account_count(self, session: object) -> int:
del session
return 1
def role_count_for_tenant(self, session: object, tenant_id: str) -> int:
del session, tenant_id
return 2
def active_api_key_count_for_tenant(self, session: object, tenant_id: str) -> int:
del session, tenant_id
return 3
def actor_email_by_user_id(self, session: object, user_ids):
del session
return {user_id: "demo@example.test" for user_id in user_ids}
def user_ids_for_actor_filter(self, session: object, *, operator: str, value: str):
del session, operator, value
return ("user-1",)
class _FakeAccessGovernanceMaterializer:
def __init__(self) -> None:
self.synced: list[GovernanceTemplateMaterialization] = []
self.removed: list[GovernanceTemplateMaterialization] = []
def sync_template(self, session: object, template: GovernanceTemplateMaterialization) -> None:
del session
self.synced.append(template)
def remove_template(self, session: object, template: GovernanceTemplateMaterialization) -> None:
del session
self.removed.append(template)
class _FakeCampaignMailPolicyContextProvider:
context = CampaignMailPolicyContext(
id="campaign-1",
tenant_id="tenant-1",
owner_user_id="user-1",
owner_group_id="group-1",
mail_profile_policy={"allow_campaign_profiles": False},
)
def get_campaign_mail_policy_context(self, session: object, *, tenant_id: str, campaign_id: str):
del session
if tenant_id == self.context.tenant_id and campaign_id == self.context.id:
return self.context
return None
def set_campaign_mail_profile_policy(self, session: object, *, tenant_id: str, campaign_id: str, policy):
del session
if tenant_id != self.context.tenant_id or campaign_id != self.context.id:
raise ValueError("Campaign policy not found")
return dict(policy)
class _FakeCampaignAccessProvider:
def campaign_exists(self, session: object, *, tenant_id: str, campaign_id: str) -> bool:
del session
return tenant_id == "tenant-1" and campaign_id == "campaign-1"
def can_read_campaign(
self,
session: object,
*,
tenant_id: str,
campaign_id: str,
user_id: str,
group_ids=(),
tenant_admin: bool = False,
) -> bool:
del session
return self.campaign_exists(object(), tenant_id=tenant_id, campaign_id=campaign_id) and (
tenant_admin or user_id == "user-1" or "group-1" in set(group_ids)
)
class _FakeCampaignPolicyContextProvider:
context = CampaignPolicyContext(
id="campaign-1",
tenant_id="tenant-1",
owner_user_id="user-1",
owner_group_id="group-1",
settings={"privacy_retention_policy": {"audit_detail_level": "redacted"}},
)
def get_campaign_policy_context(self, session: object, *, campaign_id: str, tenant_id: str | None = None):
del session
if campaign_id != self.context.id or (tenant_id is not None and tenant_id != self.context.tenant_id):
return None
return self.context
def set_campaign_settings(self, session: object, *, tenant_id: str, campaign_id: str, settings):
del session
if tenant_id != self.context.tenant_id or campaign_id != self.context.id:
raise ValueError("Campaign privacy policy not found")
return dict(settings)
class _FakeCampaignDeliveryTaskProvider:
def send_campaign_job(self, session: object, *, job_id: str, enqueue_imap_task: bool = True):
del session
return {"job_id": job_id, "enqueue_imap_task": enqueue_imap_task}
def append_sent_for_job(self, session: object, *, job_id: str):
del session
return {"job_id": job_id, "status": "appended"}
class _FakeCampaignRetentionProvider:
def apply_retention(self, session: object, *, dry_run: bool, now, policy_for_campaign_id):
del session, now
policy_for_campaign_id("campaign-1")
return {"raw_campaign_json": {"eligible": int(dry_run)}}
class _FakeSecretProvider:
def __init__(self) -> None:
self._values: dict[str, str] = {}
def store_secret(self, *, scope: str, name: str, value: str) -> str:
ref = f"{scope}/{name}"
self._values[ref] = value
return ref
def read_secret(self, secret_ref: str) -> str | None:
return self._values.get(secret_ref)
def delete_secret(self, secret_ref: str) -> None:
self._values.pop(secret_ref, None)
class _FakeAuditSink:
def __init__(self) -> None:
self.events: list[AuditEvent] = []
def record(self, event: AuditEvent) -> None:
self.events.append(event)
class AccessContractTests(unittest.TestCase):
def test_access_capability_names_are_stable(self) -> None:
self.assertEqual("access", ACCESS_MODULE_ID)
self.assertEqual("access.principalResolver", CAPABILITY_ACCESS_PRINCIPAL_RESOLVER)
self.assertEqual("access.directory", CAPABILITY_ACCESS_DIRECTORY)
self.assertEqual("access.permissionEvaluator", CAPABILITY_ACCESS_PERMISSION_EVALUATOR)
self.assertEqual("access.resourceAccess", CAPABILITY_ACCESS_RESOURCE_ACCESS)
self.assertEqual("access.tenantProvisioner", CAPABILITY_ACCESS_TENANT_PROVISIONER)
self.assertEqual("access.administration", CAPABILITY_ACCESS_ADMINISTRATION)
self.assertEqual("access.governanceMaterializer", CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER)
self.assertEqual("campaigns.access", CAPABILITY_CAMPAIGNS_ACCESS)
self.assertEqual("campaigns.deliveryTasks", CAPABILITY_CAMPAIGNS_DELIVERY_TASKS)
self.assertEqual("campaigns.mailPolicyContext", CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT)
self.assertEqual("campaigns.policyContext", CAPABILITY_CAMPAIGNS_POLICY_CONTEXT)
self.assertEqual("campaigns.retention", CAPABILITY_CAMPAIGNS_RETENTION)
self.assertEqual("tenancy.tenantResolver", CAPABILITY_TENANCY_TENANT_RESOLVER)
self.assertEqual("security.secretProvider", CAPABILITY_SECURITY_SECRET_PROVIDER)
self.assertEqual("audit.sink", CAPABILITY_AUDIT_SINK)
self.assertEqual(len(ACCESS_CAPABILITY_NAMES), len(set(ACCESS_CAPABILITY_NAMES)))
def test_access_refs_are_small_immutable_dtos(self) -> None:
principal = PrincipalRef(
account_id="account-1",
membership_id="user-1",
tenant_id="tenant-1",
scopes=frozenset({"campaigns:campaign:read"}),
group_ids=frozenset({"group-1"}),
email="demo@example.test",
)
role = RoleRef(id="role-1", name="Reviewer", level="tenant", tenant_id="tenant-1")
self.assertEqual("account-1", principal.account_id)
self.assertEqual(frozenset({"campaigns:campaign:read"}), principal.scopes)
self.assertEqual("Reviewer", role.name)
with self.assertRaises(AttributeError):
principal.account_id = "changed" # type: ignore[misc]
def test_protocol_shapes_match_reference_implementations(self) -> None:
self.assertIsInstance(_FakePrincipalResolver(), PrincipalResolver)
self.assertIsInstance(_FakeTenantResolver(), TenantResolver)
self.assertIsInstance(_FakeAccessDirectory(), AccessDirectory)
self.assertIsInstance(_FakePermissionEvaluator(), PermissionEvaluator)
self.assertIsInstance(_FakeResourceAccessService(), ResourceAccessService)
self.assertIsInstance(_FakeTenantAccessProvisioner(), TenantAccessProvisioner)
self.assertIsInstance(_FakeAccessAdministration(), AccessAdministration)
self.assertIsInstance(_FakeAccessGovernanceMaterializer(), AccessGovernanceMaterializer)
self.assertIsInstance(_FakeCampaignAccessProvider(), CampaignAccessProvider)
self.assertIsInstance(_FakeCampaignDeliveryTaskProvider(), CampaignDeliveryTaskProvider)
self.assertIsInstance(_FakeCampaignMailPolicyContextProvider(), CampaignMailPolicyContextProvider)
self.assertIsInstance(_FakeCampaignPolicyContextProvider(), CampaignPolicyContextProvider)
self.assertIsInstance(_FakeCampaignRetentionProvider(), CampaignRetentionProvider)
self.assertIsInstance(_FakeSecretProvider(), SecretProvider)
self.assertIsInstance(_FakeAuditSink(), AuditSink)
def test_campaign_mail_policy_context_capability_shape(self) -> None:
provider = _FakeCampaignMailPolicyContextProvider()
context = provider.get_campaign_mail_policy_context(object(), tenant_id="tenant-1", campaign_id="campaign-1")
self.assertEqual("campaign-1", context.id) # type: ignore[union-attr]
self.assertEqual("user-1", context.owner_user_id) # type: ignore[union-attr]
self.assertEqual({"allow_campaign_profiles": False}, context.mail_profile_policy) # type: ignore[union-attr]
self.assertEqual({"allow_campaign_profiles": True}, provider.set_campaign_mail_profile_policy(object(), tenant_id="tenant-1", campaign_id="campaign-1", policy={"allow_campaign_profiles": True}))
def test_campaign_access_capability_shape(self) -> None:
provider = _FakeCampaignAccessProvider()
self.assertTrue(provider.campaign_exists(object(), tenant_id="tenant-1", campaign_id="campaign-1"))
self.assertTrue(provider.can_read_campaign(object(), tenant_id="tenant-1", campaign_id="campaign-1", user_id="other", group_ids=("group-1",)))
self.assertFalse(provider.can_read_campaign(object(), tenant_id="tenant-1", campaign_id="missing", user_id="user-1"))
def test_campaign_policy_delivery_and_retention_capability_shapes(self) -> None:
policy_provider = _FakeCampaignPolicyContextProvider()
delivery_provider = _FakeCampaignDeliveryTaskProvider()
retention_provider = _FakeCampaignRetentionProvider()
context = policy_provider.get_campaign_policy_context(object(), tenant_id="tenant-1", campaign_id="campaign-1")
self.assertEqual("tenant-1", context.tenant_id) # type: ignore[union-attr]
self.assertEqual({"privacy_retention_policy": {"audit_detail_level": "redacted"}}, context.settings) # type: ignore[union-attr]
self.assertEqual({"demo": True}, policy_provider.set_campaign_settings(object(), tenant_id="tenant-1", campaign_id="campaign-1", settings={"demo": True}))
self.assertEqual({"job_id": "job-1", "enqueue_imap_task": False}, delivery_provider.send_campaign_job(object(), job_id="job-1", enqueue_imap_task=False))
self.assertEqual({"job_id": "job-1", "status": "appended"}, delivery_provider.append_sent_for_job(object(), job_id="job-1"))
self.assertEqual({"raw_campaign_json": {"eligible": 1}}, retention_provider.apply_retention(object(), dry_run=True, now=object(), policy_for_campaign_id=lambda campaign_id: object()))
def test_access_capabilities_register_and_resolve_through_platform_registry(self) -> None:
directory = _FakeAccessDirectory()
resolver = _FakePrincipalResolver()
evaluator = _FakePermissionEvaluator()
tenant_resolver = _FakeTenantResolver()
administration = _FakeAccessAdministration()
materializer = _FakeAccessGovernanceMaterializer()
manifest = ModuleManifest(
id=ACCESS_MODULE_ID,
name="Access",
version="test",
capability_factories={
CAPABILITY_ACCESS_DIRECTORY: lambda context: directory,
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER: lambda context: resolver,
CAPABILITY_ACCESS_PERMISSION_EVALUATOR: lambda context: evaluator,
CAPABILITY_ACCESS_ADMINISTRATION: lambda context: administration,
CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER: lambda context: materializer,
CAPABILITY_TENANCY_TENANT_RESOLVER: lambda context: tenant_resolver,
},
)
registry = PlatformRegistry()
registry.register(manifest)
registry.configure_capability_context(ModuleContext(registry=registry, settings=object()))
self.assertIs(directory, registry.require_capability(CAPABILITY_ACCESS_DIRECTORY))
self.assertIs(resolver, registry.require_capability(CAPABILITY_ACCESS_PRINCIPAL_RESOLVER))
self.assertIs(evaluator, registry.require_capability(CAPABILITY_ACCESS_PERMISSION_EVALUATOR))
self.assertIs(administration, registry.require_capability(CAPABILITY_ACCESS_ADMINISTRATION))
self.assertIs(materializer, registry.require_capability(CAPABILITY_ACCESS_GOVERNANCE_MATERIALIZER))
self.assertIs(tenant_resolver, registry.require_capability(CAPABILITY_TENANCY_TENANT_RESOLVER))
def test_sql_directory_and_tenant_resolver_return_access_dtos(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-access-directory-"))
try:
configure_database(f"sqlite:///{root / 'directory.db'}")
database = get_database()
Base.metadata.create_all(bind=database.engine)
with database.session() as session:
tenant = Tenant(id="tenant-directory", slug="directory", name="Directory Tenant", settings={})
account = Account(
id="account-directory",
email="directory@example.test",
normalized_email="directory@example.test",
display_name="Directory User",
)
user = User(
id="user-directory",
tenant_id=tenant.id,
account_id=account.id,
email=account.email,
display_name=None,
settings={},
mail_profile_policy={},
)
group = Group(id="group-directory", tenant_id=tenant.id, slug="review", name="Review", description=None)
membership = UserGroupMembership(tenant_id=tenant.id, user_id=user.id, group_id=group.id)
session.add_all([tenant, account, user, group, membership])
session.commit()
from govoplan_access.backend.directory import SqlAccessDirectory
from govoplan_tenancy.backend.capabilities import SqlTenantResolver
directory = SqlAccessDirectory()
tenant_resolver = SqlTenantResolver()
self.assertEqual("directory@example.test", directory.get_account("account-directory").email) # type: ignore[union-attr]
self.assertEqual("Directory User", directory.get_user("user-directory").display_name) # type: ignore[union-attr]
self.assertEqual(["user-directory"], [user.id for user in directory.users_for_tenant("tenant-directory")])
self.assertEqual(["group-directory"], [group.id for group in directory.groups_for_tenant("tenant-directory")])
self.assertEqual(["group-directory"], [group.id for group in directory.groups_for_user("user-directory", tenant_id="tenant-directory")])
self.assertEqual("Review", directory.display_label(AccessSubjectRef(kind="group", id="group-directory", tenant_id="tenant-directory")))
self.assertEqual("directory", tenant_resolver.get_tenant("tenant-directory").slug) # type: ignore[union-attr]
self.assertEqual(
"tenant-directory",
tenant_resolver.current_tenant(
PrincipalRef(account_id="account-directory", membership_id="user-directory", tenant_id="tenant-directory")
).id, # type: ignore[union-attr]
)
finally:
shutil.rmtree(root, ignore_errors=True)
def test_api_principal_dependency_uses_access_resolver_capability(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-access-contract-"))
try:
account_id = "account-capability"
tenant_id = "tenant-capability"
user_id = "user-capability"
class CapabilityPrincipalResolver:
def resolve_request(self, request: object, *, session: object | None = None) -> PrincipalRef:
del request, session
return PrincipalRef(
account_id=account_id,
membership_id=user_id,
tenant_id=tenant_id,
scopes=frozenset(),
)
class CapabilityPermissionEvaluator:
def scopes_grant(self, scopes, required_scope: str) -> bool:
del scopes
return required_scope == "demo:scope:read"
def has_scope(self, principal: PrincipalRef, required_scope: str) -> bool:
del principal
return required_scope == "demo:scope:read"
def explain_scope(self, principal: PrincipalRef, required_scope: str) -> AccessDecision:
return AccessDecision(allowed=self.has_scope(principal, required_scope), requirements=(required_scope,))
router = APIRouter()
@router.get("/capability-principal")
def capability_principal(principal: ApiPrincipal = Depends(get_api_principal)) -> dict[str, object]:
return {
"account_id": principal.account_id,
"tenant_id": principal.tenant_id,
"user_id": principal.user.id,
"has_demo_scope": principal.has("demo:scope:read"),
}
def access_manifest() -> ModuleManifest:
return ModuleManifest(
id=ACCESS_MODULE_ID,
name="Access",
version="test",
capability_factories={
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER: lambda context: CapabilityPrincipalResolver(),
CAPABILITY_ACCESS_PERMISSION_EVALUATOR: lambda context: CapabilityPermissionEvaluator(),
},
)
config = GovoplanServerConfig(
title="access contract test",
version="test",
settings=SimpleNamespace(database_url=f"sqlite:///{root / 'test.db'}"),
enabled_modules=(),
manifest_factories=(access_manifest,),
base_routers=(router,),
post_module_routers=(),
extra_routers=(),
lifespan=None,
app_configurators=(),
)
app = create_app(config)
database = get_database()
Base.metadata.create_all(bind=database.engine)
with database.session() as session:
tenant = Tenant(id=tenant_id, slug="capability", name="Capability Tenant", settings={})
account = Account(
id=account_id,
email="capability@example.test",
normalized_email="capability@example.test",
display_name="Capability User",
)
user = User(
id=user_id,
tenant_id=tenant_id,
account_id=account_id,
email=account.email,
display_name=account.display_name,
settings={},
mail_profile_policy={},
)
session.add_all([tenant, account, user])
session.commit()
with TestClient(app) as client:
response = client.get("/api/v1/capability-principal")
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(
{
"account_id": account_id,
"tenant_id": tenant_id,
"user_id": user_id,
"has_demo_scope": True,
},
response.json(),
)
finally:
shutil.rmtree(root, ignore_errors=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -6,6 +6,7 @@ from pathlib import Path
from alembic import command
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
from sqlalchemy import create_engine, inspect, text
from govoplan_core.db.base import Base
@@ -18,6 +19,14 @@ from govoplan_core.db.migrations import (
)
def configured_migration_heads(database_url: str) -> set[str]:
return set(ScriptDirectory.from_config(alembic_config(database_url=database_url)).get_heads())
def database_migration_heads(connection) -> set[str]:
return set(MigrationContext.configure(connection).get_current_heads())
class DatabaseMigrationTests(unittest.TestCase):
def test_repairs_create_all_schema_drift_and_upgrades_to_head(self) -> None:
with tempfile.TemporaryDirectory(prefix="msm-migration-test-") as directory:
@@ -52,7 +61,7 @@ class DatabaseMigrationTests(unittest.TestCase):
engine = create_engine(url)
try:
with engine.connect() as connection:
current = MigrationContext.configure(connection).get_current_revision()
current = database_migration_heads(connection)
inspector = inspect(connection)
columns = {
column["name"]
@@ -93,7 +102,8 @@ class DatabaseMigrationTests(unittest.TestCase):
WHERE role.slug = 'system_owner' AND account.is_active = 1
"""
)).scalar_one()
self.assertEqual(current, result.current_revision)
self.assertEqual(current, configured_migration_heads(url))
self.assertEqual(result.current_revision, ",".join(sorted(current)))
self.assertIn("user_lock_state", columns)
self.assertIn("user_locked_at", columns)
self.assertIn("user_locked_by_user_id", columns)
@@ -212,10 +222,9 @@ class DatabaseMigrationTests(unittest.TestCase):
engine = create_engine(url)
try:
with engine.connect() as connection:
self.assertEqual(
MigrationContext.configure(connection).get_current_revision(),
result.current_revision,
)
current = database_migration_heads(connection)
self.assertEqual(current, configured_migration_heads(url))
self.assertEqual(result.current_revision, ",".join(sorted(current)))
profile_columns = {
column["name"]
for column in inspect(connection).get_columns("mail_server_profiles")

File diff suppressed because it is too large Load Diff