Release v0.1.8
This commit is contained in:
@@ -133,6 +133,15 @@ class CoreEventTests(unittest.TestCase):
|
||||
self.assertEqual("request-123", response.json()["correlation_id"])
|
||||
self.assertEqual("request-123", response.headers["X-Correlation-ID"])
|
||||
|
||||
def test_app_rejects_wildcard_cors_with_credentials(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "wildcard origin"):
|
||||
create_govoplan_app(
|
||||
title="event test",
|
||||
version="test",
|
||||
registry=PlatformRegistry(),
|
||||
cors_origins=("*",),
|
||||
)
|
||||
|
||||
def test_audit_event_persists_trace_details_from_event_context(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-audit-trace-"))
|
||||
try:
|
||||
@@ -260,6 +269,8 @@ class CoreEventTests(unittest.TestCase):
|
||||
self.assertEqual("queued", payload["outcome"])
|
||||
self.assertEqual({"correlation_id": "corr-1", "causation_id": "cause-1"}, payload["_trace"])
|
||||
self.assertEqual("<redacted>", payload["options"]["api_key"])
|
||||
self.assertEqual("<redacted>", audit_operation_context(options={"bearer_token": "secret"})["options"]["bearer_token"])
|
||||
self.assertEqual("<redacted>", audit_operation_context(options={"clientSecret": "secret"})["options"]["clientSecret"])
|
||||
self.assertNotIn("token", payload["_trace"])
|
||||
self.assertNotIn("empty", payload)
|
||||
|
||||
|
||||
@@ -9,12 +9,8 @@ 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
|
||||
from govoplan_core.core.change_sequence import ChangeSequenceEntry
|
||||
from govoplan_core.db.migrations import (
|
||||
REVISION_AUTH_RBAC,
|
||||
REVISION_CORE_CHANGE_SEQUENCE,
|
||||
REVISION_FILE_FOLDERS,
|
||||
REVISION_HIERARCHICAL_SETTINGS,
|
||||
alembic_config,
|
||||
migrate_database,
|
||||
reconcile_change_sequence_retention_floor_drift,
|
||||
@@ -22,8 +18,20 @@ 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 configured_migration_heads(
|
||||
database_url: str,
|
||||
*,
|
||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||
migration_track: str = "release",
|
||||
) -> set[str]:
|
||||
script = ScriptDirectory.from_config(
|
||||
alembic_config(
|
||||
database_url=database_url,
|
||||
enabled_modules=enabled_modules,
|
||||
migration_track=migration_track,
|
||||
),
|
||||
)
|
||||
return {revision.revision for revision in script.get_revisions("heads")}
|
||||
|
||||
|
||||
def database_migration_heads(connection) -> set[str]:
|
||||
@@ -31,16 +39,26 @@ def database_migration_heads(connection) -> set[str]:
|
||||
|
||||
|
||||
class DatabaseMigrationTests(unittest.TestCase):
|
||||
def test_migration_tracks_use_separate_version_locations(self) -> None:
|
||||
release_locations = alembic_config(database_url="sqlite:////tmp/govoplan-release.db").get_main_option("version_locations")
|
||||
dev_locations = alembic_config(
|
||||
database_url="sqlite:////tmp/govoplan-dev.db",
|
||||
migration_track="dev",
|
||||
).get_main_option("version_locations")
|
||||
|
||||
self.assertIn("alembic/versions", release_locations)
|
||||
self.assertNotIn("alembic/dev_versions", release_locations)
|
||||
self.assertIn("alembic/dev_versions", dev_locations)
|
||||
self.assertIn("migrations/dev_versions", dev_locations)
|
||||
|
||||
def test_repairs_missing_change_sequence_retention_floor(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="msm-change-sequence-drift-test-") as directory:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-change-sequence-drift-test-") as directory:
|
||||
database = Path(directory) / "change-sequence-drift.db"
|
||||
url = f"sqlite:///{database}"
|
||||
command.upgrade(alembic_config(database_url=url, enabled_modules=()), REVISION_CORE_CHANGE_SEQUENCE)
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text("DROP TABLE core_change_sequence_retention_floor"))
|
||||
ChangeSequenceEntry.__table__.create(bind=connection)
|
||||
with engine.connect() as connection:
|
||||
tables = set(inspect(connection).get_table_names())
|
||||
self.assertIn("core_change_sequence", tables)
|
||||
@@ -63,7 +81,7 @@ class DatabaseMigrationTests(unittest.TestCase):
|
||||
engine.dispose()
|
||||
|
||||
def test_repairs_namespace_tables_when_database_was_stamped_ahead(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="msm-namespace-drift-test-") as directory:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-namespace-drift-test-") as directory:
|
||||
database = Path(directory) / "namespace-drift.db"
|
||||
url = f"sqlite:///{database}"
|
||||
engine = create_engine(url)
|
||||
@@ -92,437 +110,185 @@ class DatabaseMigrationTests(unittest.TestCase):
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_repairs_create_all_schema_drift_and_upgrades_to_head(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="msm-migration-test-") as directory:
|
||||
database = Path(directory) / "legacy.db"
|
||||
def test_core_and_access_baselines_apply_to_fresh_database(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-core-baseline-test-") as directory:
|
||||
database = Path(directory) / "core.db"
|
||||
url = f"sqlite:///{database}"
|
||||
|
||||
# Reproduce the historical development database: Alembic was run
|
||||
# through auth/RBAC, then create_all() created later file tables
|
||||
# without advancing alembic_version and without altering the
|
||||
# already-existing campaign_versions table.
|
||||
command.upgrade(alembic_config(database_url=url), REVISION_AUTH_RBAC)
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
Base.metadata.create_all(bind=engine)
|
||||
with engine.connect() as connection:
|
||||
self.assertEqual(
|
||||
MigrationContext.configure(connection).get_current_revision(),
|
||||
REVISION_AUTH_RBAC,
|
||||
)
|
||||
self.assertIn("file_blobs", inspect(connection).get_table_names())
|
||||
self.assertNotIn(
|
||||
"user_lock_state",
|
||||
{column["name"] for column in inspect(connection).get_columns("campaign_versions")},
|
||||
)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
result = migrate_database(database_url=url)
|
||||
self.assertEqual(result.previous_revision, REVISION_AUTH_RBAC)
|
||||
self.assertEqual(result.reconciled_revision, REVISION_FILE_FOLDERS)
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
current = database_migration_heads(connection)
|
||||
inspector = inspect(connection)
|
||||
columns = {
|
||||
column["name"]
|
||||
for column in inspector.get_columns("campaign_versions")
|
||||
}
|
||||
folder_indexes = {index["name"] for index in inspector.get_indexes("file_folders")}
|
||||
job_columns = {column["name"] for column in inspector.get_columns("campaign_jobs")}
|
||||
job_indexes = {index["name"] for index in inspector.get_indexes("campaign_jobs")}
|
||||
attempt_columns = {column["name"] for column in inspector.get_columns("send_attempts")}
|
||||
tables = set(inspector.get_table_names())
|
||||
tenant_columns = {column["name"] for column in inspector.get_columns("core_scopes")}
|
||||
user_columns = {column["name"] for column in inspector.get_columns("access_users")}
|
||||
session_columns = {column["name"] for column in inspector.get_columns("access_auth_sessions")}
|
||||
group_columns = {column["name"] for column in inspector.get_columns("access_groups")}
|
||||
role_columns = {column["name"] for column in inspector.get_columns("access_roles")}
|
||||
role_indexes = {index["name"] for index in inspector.get_indexes("access_roles")}
|
||||
campaign_columns = {column["name"] for column in inspector.get_columns("campaigns")}
|
||||
audit_columns = {column["name"] for column in inspector.get_columns("audit_log")}
|
||||
audit_indexes = {index["name"] for index in inspector.get_indexes("audit_log")}
|
||||
system_role_flags = {
|
||||
row["slug"]: bool(row["is_builtin"])
|
||||
for row in connection.execute(text(
|
||||
"SELECT slug, is_builtin FROM access_roles WHERE tenant_id IS NULL AND slug IN ('system_owner', 'system_admin', 'system_auditor')"
|
||||
)).mappings().all()
|
||||
}
|
||||
system_settings_columns = {column["name"] for column in inspector.get_columns("core_system_settings")}
|
||||
governance_assignment_columns = {column["name"] for column in inspector.get_columns("admin_governance_template_assignments")}
|
||||
import_profile_columns = {column["name"] for column in inspector.get_columns("campaign_recipient_import_mapping_profiles")}
|
||||
import_profile_indexes = {index["name"] for index in inspector.get_indexes("campaign_recipient_import_mapping_profiles")}
|
||||
account_count = connection.execute(text("SELECT COUNT(*) FROM access_accounts")).scalar_one()
|
||||
user_count = connection.execute(text("SELECT COUNT(*) FROM access_users")).scalar_one()
|
||||
system_owner_count = connection.execute(text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM access_system_role_assignments assignment
|
||||
JOIN access_roles role ON role.id = assignment.role_id
|
||||
JOIN access_accounts account ON account.id = assignment.account_id
|
||||
WHERE role.slug = 'system_owner' AND account.is_active = 1
|
||||
"""
|
||||
)).scalar_one()
|
||||
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)
|
||||
self.assertIn("uq_file_folders_active_user_path", folder_indexes)
|
||||
self.assertIn("uq_file_folders_active_group_path", folder_indexes)
|
||||
self.assertIn("execution_snapshot", columns)
|
||||
self.assertIn("execution_snapshot_hash", columns)
|
||||
self.assertIn("claimed_at", job_columns)
|
||||
self.assertIn("claim_token", job_columns)
|
||||
self.assertIn("smtp_started_at", job_columns)
|
||||
self.assertIn("outcome_unknown_at", job_columns)
|
||||
self.assertIn("eml_sha256", job_columns)
|
||||
self.assertIn("ix_campaign_jobs_claim_token", job_indexes)
|
||||
self.assertIn("ix_campaign_jobs_eml_sha256", job_indexes)
|
||||
self.assertIn("status", attempt_columns)
|
||||
self.assertIn("claim_token", attempt_columns)
|
||||
self.assertIn("access_accounts", tables)
|
||||
self.assertIn("access_system_role_assignments", tables)
|
||||
self.assertIn("core_scopes", tables)
|
||||
self.assertNotIn("tenancy_tenants", tables)
|
||||
self.assertIn("core_system_settings", tables)
|
||||
self.assertIn("admin_governance_templates", tables)
|
||||
self.assertIn("admin_governance_template_assignments", tables)
|
||||
self.assertIn("campaign_shares", tables)
|
||||
self.assertIn("campaign_recipient_import_mapping_profiles", tables)
|
||||
self.assertIn("owner_user_id", campaign_columns)
|
||||
self.assertIn("owner_group_id", campaign_columns)
|
||||
self.assertIn("ordered_header_fingerprint", import_profile_columns)
|
||||
self.assertIn("unordered_header_fingerprint", import_profile_columns)
|
||||
self.assertIn("mappings", import_profile_columns)
|
||||
self.assertIn("ix_recipient_import_profiles_ordered_fp", import_profile_indexes)
|
||||
self.assertIn("scope", audit_columns)
|
||||
self.assertIn("ix_audit_log_scope", audit_indexes)
|
||||
self.assertIn("ix_audit_log_scope_created_at", audit_indexes)
|
||||
self.assertIn("ix_audit_log_tenant_scope_created_at", audit_indexes)
|
||||
self.assertTrue(system_role_flags["system_owner"])
|
||||
self.assertFalse(system_role_flags["system_admin"])
|
||||
self.assertFalse(system_role_flags["system_auditor"])
|
||||
self.assertIn("allow_custom_groups", tenant_columns)
|
||||
self.assertIn("allow_custom_roles", tenant_columns)
|
||||
self.assertIn("allow_api_keys", tenant_columns)
|
||||
self.assertIn("description", tenant_columns)
|
||||
self.assertIn("default_locale", tenant_columns)
|
||||
self.assertIn("settings", tenant_columns)
|
||||
self.assertIn("settings", user_columns)
|
||||
self.assertIn("settings", group_columns)
|
||||
self.assertIn("settings", campaign_columns)
|
||||
self.assertIn("account_id", user_columns)
|
||||
self.assertIn("account_id", session_columns)
|
||||
self.assertIn("description", group_columns)
|
||||
self.assertIn("is_active", group_columns)
|
||||
self.assertIn("system_template_id", group_columns)
|
||||
self.assertIn("system_required", group_columns)
|
||||
self.assertIn("description", role_columns)
|
||||
self.assertIn("is_builtin", role_columns)
|
||||
self.assertIn("is_assignable", role_columns)
|
||||
self.assertIn("system_template_id", role_columns)
|
||||
self.assertIn("system_required", role_columns)
|
||||
self.assertIn("allow_tenant_custom_groups", system_settings_columns)
|
||||
self.assertIn("allow_tenant_custom_roles", system_settings_columns)
|
||||
self.assertIn("allow_tenant_api_keys", system_settings_columns)
|
||||
self.assertIn("mode", governance_assignment_columns)
|
||||
self.assertIn("uq_roles_system_slug", role_indexes)
|
||||
self.assertEqual(account_count, user_count)
|
||||
self.assertEqual(system_owner_count, 1 if user_count else 0)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_module_migrations_apply_after_core_scope_table_rename(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="msm-late-module-migration-test-") as directory:
|
||||
database = Path(directory) / "late-modules.db"
|
||||
url = f"sqlite:///{database}"
|
||||
command.upgrade(alembic_config(database_url=url, enabled_modules=()), "heads")
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
tables = set(inspect(connection).get_table_names())
|
||||
self.assertIn("core_scopes", tables)
|
||||
self.assertNotIn("tenancy_tenants", tables)
|
||||
inspector = inspect(connection)
|
||||
tables = set(inspector.get_table_names())
|
||||
role_flags = {
|
||||
row["slug"]: bool(row["is_builtin"])
|
||||
for row in connection.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT slug, is_builtin
|
||||
FROM access_roles
|
||||
WHERE tenant_id IS NULL
|
||||
AND slug IN ('system_owner', 'system_admin', 'system_auditor')
|
||||
"""
|
||||
),
|
||||
).mappings()
|
||||
}
|
||||
system_settings_count = connection.execute(
|
||||
text("SELECT COUNT(*) FROM core_system_settings WHERE id = 'global'"),
|
||||
).scalar_one()
|
||||
current = database_migration_heads(connection)
|
||||
|
||||
self.assertEqual(current, configured_migration_heads(url, enabled_modules=()))
|
||||
self.assertIn("core_scopes", tables)
|
||||
self.assertNotIn("tenancy_tenants", tables)
|
||||
self.assertIn("access_accounts", tables)
|
||||
self.assertIn("access_users", tables)
|
||||
self.assertIn("access_identities", tables)
|
||||
self.assertIn("access_function_assignments", tables)
|
||||
self.assertIn("audit_log", tables)
|
||||
self.assertIn("audit_outbox_events", tables)
|
||||
self.assertIn("file_assets", tables)
|
||||
self.assertIn("mail_server_profiles", tables)
|
||||
self.assertEqual(system_settings_count, 1)
|
||||
self.assertEqual(
|
||||
role_flags,
|
||||
{"system_owner": True, "system_admin": False, "system_auditor": False},
|
||||
)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
migrate_database(database_url=url)
|
||||
def test_default_module_baselines_apply_to_fresh_database(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-default-baseline-test-") as directory:
|
||||
database = Path(directory) / "default.db"
|
||||
url = f"sqlite:///{database}"
|
||||
|
||||
result = migrate_database(database_url=url)
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
inspector = inspect(connection)
|
||||
mail_policy_fks = inspector.get_foreign_keys("mail_profile_policies")
|
||||
referred_tables = {fk["referred_table"] for fk in mail_policy_fks}
|
||||
self.assertIn("core_scopes", referred_tables)
|
||||
self.assertNotIn("tenancy_tenants", referred_tables)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_repairs_current_schema_stamped_at_file_folders(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="msm-current-schema-marker-test-") as directory:
|
||||
database = Path(directory) / "current-schema-marker.db"
|
||||
url = f"sqlite:///{database}"
|
||||
command.upgrade(alembic_config(database_url=url), REVISION_HIERARCHICAL_SETTINGS)
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO mail_server_profiles
|
||||
(id, tenant_id, scope_type, scope_id, name, slug, description, is_active,
|
||||
smtp_config, smtp_password_encrypted, imap_config, imap_password_encrypted,
|
||||
created_by_user_id, updated_by_user_id, created_at, updated_at)
|
||||
VALUES
|
||||
('profile-1', NULL, 'system', 'system', 'System Mail', 'system-mail', NULL, 1,
|
||||
:smtp_config, NULL, :imap_config, NULL,
|
||||
NULL, NULL, '2026-06-25 15:00:00', '2026-06-25 15:00:00')
|
||||
"""
|
||||
),
|
||||
{
|
||||
"smtp_config": '{"host":"smtp.example.test","port":587,"security":"starttls","username":"smtp-user","enabled":true}',
|
||||
"imap_config": '{"host":"imap.example.test","port":993,"security":"ssl","username":"imap-user","enabled":true}',
|
||||
},
|
||||
)
|
||||
connection.execute(
|
||||
text("UPDATE alembic_version SET version_num = :revision"),
|
||||
{"revision": REVISION_FILE_FOLDERS},
|
||||
)
|
||||
with engine.connect() as connection:
|
||||
self.assertEqual(
|
||||
MigrationContext.configure(connection).get_current_revision(),
|
||||
REVISION_FILE_FOLDERS,
|
||||
)
|
||||
profile_columns = {
|
||||
column["name"]
|
||||
for column in inspect(connection).get_columns("mail_server_profiles")
|
||||
}
|
||||
self.assertNotIn("smtp_username", profile_columns)
|
||||
self.assertNotIn("imap_username", profile_columns)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
result = migrate_database(database_url=url)
|
||||
self.assertEqual(result.previous_revision, REVISION_FILE_FOLDERS)
|
||||
self.assertEqual(result.reconciled_revision, REVISION_HIERARCHICAL_SETTINGS)
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
tables = set(inspector.get_table_names())
|
||||
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")
|
||||
mail_policy_fks = inspector.get_foreign_keys("mail_profile_policies")
|
||||
mail_referred_tables = {fk["referred_table"] for fk in mail_policy_fks}
|
||||
folder_indexes = {index["name"] for index in inspector.get_indexes("file_folders")}
|
||||
import_profile_indexes = {
|
||||
index["name"]
|
||||
for index in inspector.get_indexes("campaign_recipient_import_mapping_profiles")
|
||||
}
|
||||
profile = connection.execute(text(
|
||||
"""
|
||||
SELECT smtp_username, smtp_config, imap_username, imap_config
|
||||
FROM mail_server_profiles
|
||||
WHERE id = 'profile-1'
|
||||
"""
|
||||
)).mappings().one()
|
||||
self.assertIn("smtp_username", profile_columns)
|
||||
self.assertIn("imap_username", profile_columns)
|
||||
self.assertEqual(profile["smtp_username"], "smtp-user")
|
||||
self.assertEqual(profile["imap_username"], "imap-user")
|
||||
self.assertNotIn("username", profile["smtp_config"])
|
||||
self.assertNotIn("enabled", profile["smtp_config"])
|
||||
self.assertNotIn("username", profile["imap_config"])
|
||||
self.assertNotIn("enabled", profile["imap_config"])
|
||||
|
||||
self.assertIsNone(result.previous_revision)
|
||||
self.assertIsNone(result.reconciled_revision)
|
||||
self.assertEqual(current, configured_migration_heads(url))
|
||||
self.assertEqual(result.current_revision, ",".join(sorted(current)))
|
||||
self.assertIn("calendar_sync_credentials", tables)
|
||||
self.assertIn("campaign_recipient_import_mapping_profiles", tables)
|
||||
self.assertIn("file_connector_credentials", tables)
|
||||
self.assertIn("file_connector_policies", tables)
|
||||
self.assertIn("identity_identities", tables)
|
||||
self.assertIn("identity_account_links", tables)
|
||||
self.assertIn("mail_profile_policies", tables)
|
||||
self.assertIn("organizations_functions", tables)
|
||||
self.assertIn("core_scopes", mail_referred_tables)
|
||||
self.assertIn("uq_file_folders_active_user_path", folder_indexes)
|
||||
self.assertIn("uq_file_folders_active_group_path", folder_indexes)
|
||||
self.assertIn("ix_recipient_import_profiles_ordered_fp", import_profile_indexes)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_migrates_legacy_login_identity_and_bootstraps_system_owner(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="msm-account-migration-test-") as directory:
|
||||
database = Path(directory) / "accounts.db"
|
||||
def test_dev_migration_track_applies_detailed_chain_to_fresh_database(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-dev-track-migration-test-") as directory:
|
||||
database = Path(directory) / "dev-track.db"
|
||||
url = f"sqlite:///{database}"
|
||||
command.upgrade(alembic_config(database_url=url), "7b8c9d0e1f2a")
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
timestamps = {"created_at": "2026-06-14 12:00:00", "updated_at": "2026-06-14 12:00:00"}
|
||||
connection.execute(text(
|
||||
"""
|
||||
INSERT INTO tenancy_tenants (id, slug, name, is_active, created_at, updated_at)
|
||||
VALUES ('tenant-1', 'legacy', 'Legacy', 1, :created_at, :updated_at)
|
||||
"""
|
||||
), timestamps)
|
||||
connection.execute(text(
|
||||
"""
|
||||
INSERT INTO access_users
|
||||
(id, tenant_id, email, display_name, is_active, is_tenant_admin,
|
||||
auth_provider, password_hash, last_login_at, created_at, updated_at)
|
||||
VALUES
|
||||
('user-1', 'tenant-1', 'Owner@Example.Local', 'Legacy Owner', 1, 1,
|
||||
'local', 'legacy-password-hash', NULL, :created_at, :updated_at)
|
||||
"""
|
||||
), timestamps)
|
||||
connection.execute(text(
|
||||
"""
|
||||
INSERT INTO access_auth_sessions
|
||||
(id, tenant_id, user_id, token_hash, expires_at, last_seen_at, revoked_at,
|
||||
user_agent, ip_address, created_at, updated_at)
|
||||
VALUES
|
||||
('session-1', 'tenant-1', 'user-1', 'token-hash', '2026-06-15 12:00:00',
|
||||
NULL, NULL, NULL, NULL, :created_at, :updated_at)
|
||||
"""
|
||||
), timestamps)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
migrate_database(database_url=url, reconcile_legacy_schema=False)
|
||||
result = migrate_database(database_url=url, migration_track="dev")
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
tables = set(inspect(connection).get_table_names())
|
||||
account = connection.execute(text(
|
||||
"SELECT id, normalized_email, password_hash FROM access_accounts"
|
||||
)).mappings().one()
|
||||
membership = connection.execute(text(
|
||||
"SELECT account_id FROM access_users WHERE id = 'user-1'"
|
||||
)).mappings().one()
|
||||
migrated_session = connection.execute(text(
|
||||
"SELECT account_id FROM access_auth_sessions WHERE id = 'session-1'"
|
||||
)).mappings().one()
|
||||
owner_assignment = connection.execute(text(
|
||||
"""
|
||||
SELECT role.slug
|
||||
FROM access_user_role_assignments assignment
|
||||
JOIN access_roles role ON role.id = assignment.role_id
|
||||
WHERE assignment.user_id = 'user-1'
|
||||
"""
|
||||
)).scalar_one()
|
||||
owner_permissions = connection.execute(text(
|
||||
"SELECT permissions FROM access_roles WHERE tenant_id = 'tenant-1' AND slug = 'owner'"
|
||||
)).scalar_one()
|
||||
system_assignment = connection.execute(text(
|
||||
"""
|
||||
SELECT role.slug
|
||||
FROM access_system_role_assignments assignment
|
||||
JOIN access_roles role ON role.id = assignment.role_id
|
||||
WHERE assignment.account_id = :account_id
|
||||
"""
|
||||
), {"account_id": account["id"]}).scalar_one()
|
||||
system_role_flags = {
|
||||
row["slug"]: bool(row["is_builtin"])
|
||||
for row in connection.execute(text(
|
||||
"SELECT slug, is_builtin FROM access_roles WHERE tenant_id IS NULL AND slug IN ('system_owner', 'system_admin', 'system_auditor')"
|
||||
)).mappings().all()
|
||||
}
|
||||
scope_slug = connection.execute(text(
|
||||
"SELECT slug FROM core_scopes WHERE id = 'tenant-1'"
|
||||
)).scalar_one()
|
||||
current = database_migration_heads(connection)
|
||||
|
||||
self.assertEqual(current, configured_migration_heads(url, migration_track="dev"))
|
||||
self.assertEqual(result.current_revision, ",".join(sorted(current)))
|
||||
self.assertIn("0f1e2d3c4b5a", current)
|
||||
self.assertIn("audit_outbox_events", tables)
|
||||
self.assertIn("file_connector_profiles", tables)
|
||||
self.assertIn("core_scopes", tables)
|
||||
self.assertNotIn("tenancy_tenants", tables)
|
||||
self.assertEqual(account["normalized_email"], "owner@example.local")
|
||||
self.assertEqual(account["password_hash"], "legacy-password-hash")
|
||||
self.assertEqual(scope_slug, "legacy")
|
||||
self.assertEqual(membership["account_id"], account["id"])
|
||||
self.assertEqual(migrated_session["account_id"], account["id"])
|
||||
self.assertEqual(owner_assignment, "owner")
|
||||
self.assertIn("tenant:*", owner_permissions)
|
||||
self.assertEqual(system_assignment, "system_owner")
|
||||
self.assertTrue(system_role_flags["system_owner"])
|
||||
self.assertFalse(system_role_flags["system_admin"])
|
||||
self.assertFalse(system_role_flags["system_auditor"])
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_backfills_explicit_system_and_tenant_audit_scopes(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="msm-audit-scope-migration-test-") as directory:
|
||||
database = Path(directory) / "audit-scope.db"
|
||||
def test_migrate_database_removes_stale_dependency_parent_version_rows(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-stale-head-baseline-test-") as directory:
|
||||
database = Path(directory) / "stale-heads.db"
|
||||
url = f"sqlite:///{database}"
|
||||
command.upgrade(alembic_config(database_url=url), "a0b1c2d3e4f5")
|
||||
|
||||
migrate_database(database_url=url)
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
timestamps = {
|
||||
"created_at": "2026-06-15 12:00:00",
|
||||
"updated_at": "2026-06-15 12:00:00",
|
||||
}
|
||||
connection.execute(text(
|
||||
"""
|
||||
INSERT INTO audit_log
|
||||
(id, tenant_id, user_id, api_key_id, action, object_type, object_id, details, created_at, updated_at)
|
||||
VALUES
|
||||
('audit-system', NULL, NULL, NULL, 'system_settings.updated', 'core_system_settings', 'system', '{}', :created_at, :updated_at),
|
||||
('audit-tenant', NULL, NULL, NULL, 'user.updated', 'user', 'user-1', '{}', :created_at, :updated_at)
|
||||
"""
|
||||
), timestamps)
|
||||
connection.execute(
|
||||
text("INSERT INTO alembic_version (version_num) VALUES (:revision)"),
|
||||
{"revision": "4f2a9c8e7b6d"},
|
||||
)
|
||||
with engine.connect() as connection:
|
||||
self.assertIn("4f2a9c8e7b6d", database_migration_heads(connection))
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
migrate_database(database_url=url, reconcile_legacy_schema=False)
|
||||
result = migrate_database(database_url=url)
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
scopes = dict(connection.execute(text(
|
||||
"SELECT id, scope FROM audit_log WHERE id IN ('audit-system', 'audit-tenant')"
|
||||
)).all())
|
||||
self.assertEqual(scopes["audit-system"], "system")
|
||||
self.assertEqual(scopes["audit-tenant"], "tenant")
|
||||
current = database_migration_heads(connection)
|
||||
|
||||
self.assertEqual(current, configured_migration_heads(url))
|
||||
self.assertNotIn("4f2a9c8e7b6d", current)
|
||||
self.assertEqual(result.current_revision, ",".join(sorted(current)))
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_deduplicates_active_folder_paths_before_unique_indexes(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="msm-folder-migration-test-") as directory:
|
||||
database = Path(directory) / "duplicates.db"
|
||||
def test_full_product_baselines_apply_with_idm(self) -> None:
|
||||
enabled_modules = [
|
||||
"tenancy",
|
||||
"organizations",
|
||||
"identity",
|
||||
"access",
|
||||
"admin",
|
||||
"dashboard",
|
||||
"policy",
|
||||
"audit",
|
||||
"files",
|
||||
"mail",
|
||||
"campaigns",
|
||||
"calendar",
|
||||
"docs",
|
||||
"ops",
|
||||
"idm",
|
||||
]
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-full-baseline-test-") as directory:
|
||||
database = Path(directory) / "full.db"
|
||||
url = f"sqlite:///{database}"
|
||||
command.upgrade(alembic_config(database_url=url), "5f6a7b8c9d0e")
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
parameters = {
|
||||
"tenant_id": "tenant-1",
|
||||
"owner_user_id": "user-1",
|
||||
"path": "archive",
|
||||
"created_at": "2026-06-13 20:00:00",
|
||||
"updated_at": "2026-06-13 20:00:00",
|
||||
}
|
||||
connection.execute(text(
|
||||
"""
|
||||
INSERT INTO file_folders
|
||||
(id, tenant_id, owner_type, owner_user_id, owner_group_id, path,
|
||||
created_by_user_id, deleted_at, metadata, created_at, updated_at)
|
||||
VALUES
|
||||
('folder-1', :tenant_id, 'user', :owner_user_id, NULL, :path,
|
||||
NULL, NULL, '{}', :created_at, :updated_at),
|
||||
('folder-2', :tenant_id, 'user', :owner_user_id, NULL, :path,
|
||||
NULL, NULL, '{}', :created_at, :updated_at)
|
||||
"""
|
||||
), parameters)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
migrate_database(database_url=url, reconcile_legacy_schema=False)
|
||||
result = migrate_database(database_url=url, enabled_modules=enabled_modules)
|
||||
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
active_count = connection.execute(text(
|
||||
"SELECT COUNT(*) FROM file_folders WHERE path = 'archive' AND deleted_at IS NULL"
|
||||
)).scalar_one()
|
||||
deleted_count = connection.execute(text(
|
||||
"SELECT COUNT(*) FROM file_folders WHERE path = 'archive' AND deleted_at IS NOT NULL"
|
||||
)).scalar_one()
|
||||
folder_indexes = {index["name"] for index in inspect(connection).get_indexes("file_folders")}
|
||||
self.assertEqual(active_count, 1)
|
||||
self.assertEqual(deleted_count, 1)
|
||||
self.assertIn("uq_file_folders_active_user_path", folder_indexes)
|
||||
self.assertIn("uq_file_folders_active_group_path", folder_indexes)
|
||||
tables = set(inspect(connection).get_table_names())
|
||||
current = database_migration_heads(connection)
|
||||
|
||||
self.assertEqual(current, configured_migration_heads(url, enabled_modules=enabled_modules))
|
||||
self.assertEqual(result.current_revision, ",".join(sorted(current)))
|
||||
self.assertIn("idm_tenant_settings", tables)
|
||||
self.assertIn("idm_organization_function_assignments", tables)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -43,7 +43,7 @@ class DevserverSmokeTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr or result.stdout)
|
||||
db_path = runtime_root / "runtime" / "multimailer-dev.db"
|
||||
db_path = runtime_root / "runtime" / "govoplan-dev.db"
|
||||
self.assertTrue(db_path.exists(), result.stdout)
|
||||
self.assertGreater(db_path.stat().st_size, 0, result.stdout)
|
||||
self.assertIn(f"Runtime root: {runtime_root}", result.stdout)
|
||||
|
||||
@@ -2520,9 +2520,11 @@ finally:
|
||||
"pathlib.Path(os.environ['GOVOPLAN_DATABASE_BACKUP_METADATA']).write_text(json.dumps({"
|
||||
"'snapshot': 'external', "
|
||||
"'pgtools_url': os.environ.get('GOVOPLAN_DATABASE_URL_PGTOOLS')"
|
||||
"}), encoding='utf-8')"
|
||||
"}), encoding='utf-8'); "
|
||||
"print(os.environ.get('GOVOPLAN_DATABASE_URL'))"
|
||||
)
|
||||
backup_command = f"{sys.executable} -c {shlex.quote(backup_script)}"
|
||||
database_url = "postgresql+psycopg://govoplan:db-secret@db.example.invalid/govoplan"
|
||||
|
||||
with database.session() as session:
|
||||
save_maintenance_mode(session, MaintenanceMode(enabled=True))
|
||||
@@ -2540,7 +2542,7 @@ finally:
|
||||
available=available_module_manifests(),
|
||||
current_enabled=("tenancy", "access"),
|
||||
desired_enabled=("tenancy", "access"),
|
||||
database_url="postgresql+psycopg://db.example.invalid/govoplan",
|
||||
database_url=database_url,
|
||||
runtime_dir=root / "installer",
|
||||
migrate_database=True,
|
||||
database_backup_command=backup_command,
|
||||
@@ -2548,11 +2550,16 @@ finally:
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
record = json.loads(result.record_path.read_text(encoding="utf-8"))
|
||||
record_text = result.record_path.read_text(encoding="utf-8")
|
||||
self.assertNotIn("db-secret", record_text)
|
||||
record = json.loads(record_text)
|
||||
database_backup = record["snapshot"]["database_backup"]
|
||||
self.assertEqual("external", database_backup["type"])
|
||||
self.assertEqual("postgresql+psycopg://govoplan:***@db.example.invalid/govoplan", database_backup["database_url"])
|
||||
self.assertEqual("external", database_backup["metadata"]["snapshot"])
|
||||
self.assertEqual("postgresql://db.example.invalid/govoplan", database_backup["metadata"]["pgtools_url"])
|
||||
self.assertEqual("postgresql://govoplan:***@db.example.invalid/govoplan", database_backup["metadata"]["pgtools_url"])
|
||||
self.assertEqual("postgresql+psycopg://govoplan:***@db.example.invalid/govoplan", database_backup["stdout"].strip())
|
||||
self.assertEqual(database_url, (result.record_path.parent / database_backup["database_url_secret"]).read_text(encoding="utf-8"))
|
||||
self.assertEqual("backup", (result.record_path.parent / "database.external.backup").read_text(encoding="utf-8"))
|
||||
|
||||
def test_module_installer_external_database_restore_check_runs_before_migrations(self) -> None:
|
||||
@@ -2613,14 +2620,17 @@ finally:
|
||||
"os.environ.get('GOVOPLAN_DATABASE_URL', ''), encoding='utf-8')"
|
||||
)
|
||||
restore_command = f"{sys.executable} -c {shlex.quote(restore_script)}"
|
||||
database_url = "postgresql://restore-user:restore-secret@db.example.invalid/govoplan"
|
||||
(run_dir / "database.external.backup").write_text("backup", encoding="utf-8")
|
||||
(run_dir / "database-url.secret").write_text(database_url, encoding="utf-8")
|
||||
(run_dir / "record.json").write_text(json.dumps({
|
||||
"run_id": run_id,
|
||||
"plan": [],
|
||||
"snapshot": {
|
||||
"database_backup": {
|
||||
"type": "external",
|
||||
"database_url": "postgresql://db.example.invalid/govoplan",
|
||||
"database_url": "postgresql://restore-user:***@db.example.invalid/govoplan",
|
||||
"database_url_secret": "database-url.secret",
|
||||
"backup_path": "database.external.backup",
|
||||
"metadata_path": "database-backup-metadata.json",
|
||||
"restore_command": restore_command,
|
||||
@@ -2631,8 +2641,9 @@ finally:
|
||||
result = rollback_module_install_run(run_id=run_id, runtime_dir=runtime_dir, npm_bin="npm")
|
||||
|
||||
self.assertEqual("rolled-back", result.status)
|
||||
self.assertEqual("postgresql://db.example.invalid/govoplan", (run_dir / "restore.marker").read_text(encoding="utf-8"))
|
||||
self.assertEqual(database_url, (run_dir / "restore.marker").read_text(encoding="utf-8"))
|
||||
record = json.loads((run_dir / "record.json").read_text(encoding="utf-8"))
|
||||
self.assertNotIn("restore-secret", json.dumps(record))
|
||||
self.assertTrue(record["rollback_database_restore"]["restored"])
|
||||
|
||||
def test_module_installer_request_queue_round_trips(self) -> None:
|
||||
@@ -2987,14 +2998,14 @@ finally:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"scripts/generate-release-catalog.py",
|
||||
str(Path(__file__).resolve().parents[2] / "govoplan" / "tools" / "generate-release-catalog.py"),
|
||||
"--version",
|
||||
"0.1.7",
|
||||
"--catalog-output",
|
||||
str(catalog_path),
|
||||
],
|
||||
cwd=str(Path(__file__).resolve().parents[1]),
|
||||
env=os.environ.copy(),
|
||||
env={**os.environ.copy(), "GOVOPLAN_CORE_ROOT": str(Path(__file__).resolve().parents[1])},
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
|
||||
@@ -115,6 +115,16 @@ class PolicyContractTests(unittest.TestCase):
|
||||
self.assertTrue(allowed.allowed, allowed.to_dict())
|
||||
self.assertEqual(allowed.secret_handling, "reference_only")
|
||||
|
||||
compound_secret = plan_configuration_change(
|
||||
"files.connector_profiles",
|
||||
actor_scopes=("tenant:*",),
|
||||
value={"auth": {"bearer_token": "plain-secret"}, "credential_ref": "env:FILES_TOKEN"},
|
||||
dry_run=True,
|
||||
approval_count=2,
|
||||
)
|
||||
self.assertFalse(compound_secret.allowed)
|
||||
self.assertIn("secret_reference_required", compound_secret.blockers)
|
||||
|
||||
env_only = plan_configuration_change("DATABASE_URL", actor_scopes=("system:*",), value="postgresql://example")
|
||||
self.assertFalse(env_only.allowed)
|
||||
self.assertIn("deployment_managed", env_only.blockers)
|
||||
|
||||
187
tests/test_release_doctor.py
Normal file
187
tests/test_release_doctor.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "release-doctor.py"
|
||||
|
||||
|
||||
def load_doctor_module():
|
||||
spec = importlib.util.spec_from_file_location("release_doctor", SCRIPT)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load {SCRIPT}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class ReleaseDoctorTests(unittest.TestCase):
|
||||
def test_release_repo_names_include_catalog_and_migration_baseline_owners(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
|
||||
names = set(doctor.release_repo_names(ROOT.parent))
|
||||
|
||||
self.assertIn("govoplan-core", names)
|
||||
self.assertIn("govoplan-files", names)
|
||||
self.assertIn("govoplan-idm", names)
|
||||
|
||||
def test_repo_findings_detect_dirty_repositories(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
repo = doctor.RepoState(
|
||||
name="govoplan-files",
|
||||
path="/tmp/govoplan-files",
|
||||
exists=True,
|
||||
dirty_entries=(" M pyproject.toml",),
|
||||
pyproject_version="0.1.7",
|
||||
local_tag_exists=True,
|
||||
)
|
||||
|
||||
findings = doctor.repo_findings((repo,), target_version="0.1.7", target_tag="v0.1.7")
|
||||
|
||||
self.assertEqual("blocker", findings[0].severity)
|
||||
self.assertEqual("repo-dirty", findings[0].check_id)
|
||||
self.assertIn("git status --short", [item.command for item in findings[0].commands])
|
||||
|
||||
def test_dubious_ownership_errors_suggest_safe_directory_command(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
repo = doctor.RepoState(
|
||||
name="govoplan-files",
|
||||
path="/tmp/govoplan-files",
|
||||
exists=True,
|
||||
safe_directory_required=True,
|
||||
safe_directory_command="git config --global --add safe.directory /tmp/govoplan-files",
|
||||
errors=("fatal: detected dubious ownership in repository",),
|
||||
)
|
||||
|
||||
findings = doctor.repo_findings((repo,), target_version="0.1.7", target_tag="v0.1.7")
|
||||
|
||||
self.assertEqual("repo-safe-directory", findings[0].check_id)
|
||||
self.assertEqual("blocker", findings[0].severity)
|
||||
self.assertTrue(findings[0].commands[0].mutating)
|
||||
self.assertIn("safe.directory", findings[0].commands[0].command)
|
||||
|
||||
def test_dubious_ownership_detection_matches_git_error(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
|
||||
self.assertTrue(
|
||||
doctor.is_dubious_ownership_error(
|
||||
"fatal: detected dubious ownership in repository at '/workspace/repo'\n"
|
||||
"git config --global --add safe.directory /workspace/repo"
|
||||
)
|
||||
)
|
||||
|
||||
def test_release_migration_strict_errors_suggest_recording_baseline(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
|
||||
findings = doctor.migration_findings(
|
||||
{
|
||||
"release": {
|
||||
"ok": True,
|
||||
"graph_errors": [],
|
||||
"strict_errors": ["current migration heads are not recorded"],
|
||||
},
|
||||
"dev": {"ok": True, "graph_errors": [], "strict_errors": []},
|
||||
},
|
||||
target_version="0.2.0",
|
||||
)
|
||||
|
||||
self.assertEqual("migration-release-baseline", findings[0].check_id)
|
||||
self.assertEqual("blocker", findings[0].severity)
|
||||
commands = [item.command for item in findings[0].commands]
|
||||
self.assertTrue(any("--record-release 0.2.0" in item for item in commands))
|
||||
|
||||
def test_catalog_findings_detect_missing_signatures(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
|
||||
findings = doctor.catalog_findings(
|
||||
{
|
||||
"catalog_exists": True,
|
||||
"catalog_path": "/tmp/stable.json",
|
||||
"core_release_version": "0.1.7",
|
||||
"signature_count": 0,
|
||||
"keyring_exists": True,
|
||||
"key_count": 1,
|
||||
},
|
||||
target_version="0.1.7",
|
||||
online=False,
|
||||
)
|
||||
|
||||
self.assertEqual("catalog-signatures", findings[0].check_id)
|
||||
self.assertEqual("blocker", findings[0].severity)
|
||||
|
||||
def test_suggested_commands_are_deduplicated(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
suggested = doctor.SuggestedCommand(title="Status", command="git status --short", cwd="/tmp/repo")
|
||||
report = doctor.DoctorReport(
|
||||
generated_at="2026-07-11T00:00:00Z",
|
||||
workspace_root="/tmp",
|
||||
target_version="0.1.7",
|
||||
target_tag="v0.1.7",
|
||||
online=False,
|
||||
overall_status="blocked",
|
||||
repos=(),
|
||||
findings=(
|
||||
doctor.Finding("a", "blocker", "A", "", (suggested,)),
|
||||
doctor.Finding("b", "warning", "B", "", (suggested,)),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual((suggested,), doctor.collect_suggested_commands(report))
|
||||
|
||||
def test_default_report_is_concise_and_keeps_details_out(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
repo = doctor.RepoState(
|
||||
name="govoplan-files",
|
||||
path="/tmp/govoplan-files",
|
||||
exists=True,
|
||||
dirty_entries=(" M pyproject.toml",),
|
||||
)
|
||||
report = doctor.DoctorReport(
|
||||
generated_at="2026-07-11T00:00:00Z",
|
||||
workspace_root="/tmp",
|
||||
target_version="0.1.7",
|
||||
target_tag="v0.1.7",
|
||||
online=False,
|
||||
overall_status="blocked",
|
||||
repos=(repo,),
|
||||
findings=(doctor.Finding("repo-dirty", "blocker", "govoplan-files has uncommitted changes", "M pyproject.toml"),),
|
||||
)
|
||||
|
||||
concise = doctor.render_text_report(report, detailed=False, include_commands=False)
|
||||
detailed = doctor.render_text_report(report, detailed=True, include_commands=False)
|
||||
|
||||
self.assertIn("dirty=1", concise)
|
||||
self.assertNotIn("M pyproject.toml", concise)
|
||||
self.assertIn("M pyproject.toml", detailed)
|
||||
|
||||
def test_interactive_actions_offer_dirty_bulk_commit_push(self) -> None:
|
||||
doctor = load_doctor_module()
|
||||
repo = doctor.RepoState(
|
||||
name="govoplan-files",
|
||||
path="/tmp/govoplan-files",
|
||||
exists=True,
|
||||
dirty_entries=(" M pyproject.toml",),
|
||||
)
|
||||
report = doctor.DoctorReport(
|
||||
generated_at="2026-07-11T00:00:00Z",
|
||||
workspace_root="/tmp",
|
||||
target_version="0.1.7",
|
||||
target_tag="v0.1.7",
|
||||
online=False,
|
||||
overall_status="blocked",
|
||||
repos=(repo,),
|
||||
findings=(),
|
||||
)
|
||||
|
||||
actions = dict(doctor.interactive_actions(report))
|
||||
|
||||
self.assertIn("commit-push-dirty", actions)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -33,6 +33,7 @@ from typing import Sequence, Union
|
||||
|
||||
revision: str = "1234"
|
||||
down_revision: Union[str, None] = "base"
|
||||
depends_on: Union[str, None] = "core"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
""",
|
||||
encoding="utf-8",
|
||||
@@ -43,13 +44,14 @@ branch_labels: Union[str, Sequence[str], None] = None
|
||||
self.assertIsNotNone(migration)
|
||||
self.assertEqual(migration.revision, "1234")
|
||||
self.assertEqual(migration.down_revisions, ("base",))
|
||||
self.assertEqual(migration.depends_on, ("core",))
|
||||
self.assertEqual(migration.branch_labels, ())
|
||||
|
||||
def test_release_baseline_matches_current_heads_in_strict_report(self) -> None:
|
||||
audit = load_audit_module()
|
||||
migrations = [
|
||||
audit.Migration("govoplan-core", Path("base.py"), "base", (), ()),
|
||||
audit.Migration("govoplan-core", Path("head.py"), "head", ("base",), ()),
|
||||
audit.Migration("govoplan-core", Path("base.py"), "base", (), (), ()),
|
||||
audit.Migration("govoplan-core", Path("head.py"), "head", ("base",), (), ()),
|
||||
]
|
||||
report = audit.build_report(
|
||||
migrations,
|
||||
@@ -73,8 +75,8 @@ branch_labels: Union[str, Sequence[str], None] = None
|
||||
def test_owner_heads_are_accepted_for_subset_installs(self) -> None:
|
||||
audit = load_audit_module()
|
||||
migrations = [
|
||||
audit.Migration("govoplan-core", Path("base.py"), "base", (), ()),
|
||||
audit.Migration("govoplan-core", Path("core_head.py"), "core-head", ("base",), ()),
|
||||
audit.Migration("govoplan-core", Path("base.py"), "base", (), (), ()),
|
||||
audit.Migration("govoplan-core", Path("core_head.py"), "core-head", ("base",), (), ()),
|
||||
]
|
||||
report = audit.build_report(
|
||||
migrations,
|
||||
@@ -98,9 +100,9 @@ branch_labels: Union[str, Sequence[str], None] = None
|
||||
def test_records_current_release_baseline(self) -> None:
|
||||
audit = load_audit_module()
|
||||
migrations = [
|
||||
audit.Migration("govoplan-core", Path("base.py"), "base", (), ()),
|
||||
audit.Migration("govoplan-core", Path("core_head.py"), "core-head", ("base",), ()),
|
||||
audit.Migration("govoplan-files", Path("files_head.py"), "files-head", ("core-head",), ()),
|
||||
audit.Migration("govoplan-core", Path("base.py"), "base", (), (), ()),
|
||||
audit.Migration("govoplan-core", Path("core_head.py"), "core-head", ("base",), (), ()),
|
||||
audit.Migration("govoplan-files", Path("files_head.py"), "files-head", ("core-head",), (), ()),
|
||||
]
|
||||
report = audit.build_report(
|
||||
migrations,
|
||||
@@ -125,7 +127,7 @@ branch_labels: Union[str, Sequence[str], None] = None
|
||||
def test_missing_down_revision_is_a_graph_error(self) -> None:
|
||||
audit = load_audit_module()
|
||||
migrations = [
|
||||
audit.Migration("govoplan-core", Path("head.py"), "head", ("missing",), ()),
|
||||
audit.Migration("govoplan-core", Path("head.py"), "head", ("missing",), (), ()),
|
||||
]
|
||||
report = audit.build_report(
|
||||
migrations,
|
||||
@@ -136,6 +138,53 @@ branch_labels: Union[str, Sequence[str], None] = None
|
||||
|
||||
self.assertIn("references missing down_revision", report["graph_errors"][0])
|
||||
|
||||
def test_depends_on_participates_in_graph_validation_and_heads(self) -> None:
|
||||
audit = load_audit_module()
|
||||
migrations = [
|
||||
audit.Migration("govoplan-core", Path("core.py"), "core", (), (), ()),
|
||||
audit.Migration("govoplan-files", Path("files.py"), "files", (), ("core",), ()),
|
||||
]
|
||||
report = audit.build_report(
|
||||
migrations,
|
||||
baseline={"version": 1, "releases": []},
|
||||
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||
workspace_root=Path("/workspace"),
|
||||
)
|
||||
|
||||
self.assertEqual(report["graph_errors"], [])
|
||||
self.assertEqual(report["current_heads"], ["files"])
|
||||
|
||||
def test_missing_depends_on_is_a_graph_error(self) -> None:
|
||||
audit = load_audit_module()
|
||||
migrations = [
|
||||
audit.Migration("govoplan-files", Path("files.py"), "files", (), ("missing",), ()),
|
||||
]
|
||||
report = audit.build_report(
|
||||
migrations,
|
||||
baseline={"version": 1, "releases": []},
|
||||
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||
workspace_root=Path("/workspace"),
|
||||
)
|
||||
|
||||
self.assertIn("references missing depends_on", report["graph_errors"][0])
|
||||
|
||||
def test_dev_track_does_not_emit_release_baseline_warnings(self) -> None:
|
||||
audit = load_audit_module()
|
||||
migrations = [
|
||||
audit.Migration("govoplan-core", Path("base.py"), "base", (), (), ()),
|
||||
audit.Migration("govoplan-core", Path("head.py"), "head", ("base",), (), ()),
|
||||
]
|
||||
report = audit.build_report(
|
||||
migrations,
|
||||
baseline={"version": 1, "releases": []},
|
||||
baseline_file=Path("docs/migration-release-baselines.json"),
|
||||
workspace_root=Path("/workspace"),
|
||||
track="dev",
|
||||
)
|
||||
|
||||
self.assertEqual(report["track"], "dev")
|
||||
self.assertEqual(report["strict_errors"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user