"""M1 schema integration test. Asserts that the migration has produced the expected tables, FK relations, CHECK constraints, partial indexes, and that the alembic_version row is at head. Skips automatically when Postgres is unreachable. """ from __future__ import annotations import pytest from sqlalchemy import inspect, text EXPECTED_TABLES = { # Auth / RBAC "users", "groups", "permissions", "user_groups", "group_permissions", "invitations", "invitation_groups", "refresh_tokens", # MITRE "mitre_tactics", "mitre_techniques", "mitre_subtechniques", "mitre_technique_tactics", # Templates "test_templates", "test_template_mitre_tags", "scenario_templates", "scenario_template_tests", # Missions "missions", "mission_members", "mission_scenarios", "mission_tests", "mission_test_mitre_tags", "mission_categories", # Evidence / settings / notifications "evidence_files", "settings", "detection_levels", "notifications", # Alembic bookkeeping "alembic_version", } # Tables that MUST carry a `deleted_at` column (soft delete). SOFT_DELETE_TABLES = { "users", "groups", "test_templates", "scenario_templates", "missions", "mission_scenarios", "mission_tests", "mission_categories", "evidence_files", } # Tables that MUST carry the standard `created_at` + `updated_at` pair. TIMESTAMP_TABLES = { "users", "groups", "test_templates", "scenario_templates", "missions", "mission_scenarios", "mission_tests", "mission_categories", "evidence_files", } # Spot-checked FK pairs (child_table, child_col, parent_table). EXPECTED_FKS = { ("evidence_files", "mission_test_id", "mission_tests"), ("evidence_files", "uploaded_by_user_id", "users"), ("mission_members", "mission_id", "missions"), ("mission_members", "user_id", "users"), ("mission_scenarios", "mission_id", "missions"), ("mission_tests", "scenario_id", "mission_scenarios"), ("mission_tests", "detection_level_id", "detection_levels"), ("group_permissions", "group_id", "groups"), ("group_permissions", "permission_id", "permissions"), ("user_groups", "user_id", "users"), ("user_groups", "group_id", "groups"), ("refresh_tokens", "user_id", "users"), ("notifications", "user_id", "users"), ("mitre_subtechniques", "technique_id", "mitre_techniques"), } # CHECK constraint names we expect to see (namespace 'public' only). # `mission_test_mitre_tags` deliberately lacks the exactly_one_mitre_fk check # because it is denormalised — see app/models/mission.py docstring. EXPECTED_CHECKS = { "ck_missions_status_valid", "ck_missions_visibility_mode_valid", "ck_mission_tests_state_valid", "ck_mission_tests_snapshot_opsec_level_valid", "ck_test_templates_opsec_level_valid", "ck_mission_members_role_hint_valid", "ck_test_template_mitre_tags_mitre_kind_valid", "ck_test_template_mitre_tags_exactly_one_mitre_fk", "ck_mission_test_mitre_tags_mitre_kind_valid", } @pytest.fixture(scope="module") def insp(db_engine_or_skip): return inspect(db_engine_or_skip) def test_all_expected_tables_exist(insp): actual = set(insp.get_table_names(schema="public")) missing = EXPECTED_TABLES - actual assert not missing, f"missing tables: {sorted(missing)}" def test_soft_delete_columns_present(insp): for tbl in sorted(SOFT_DELETE_TABLES): cols = {c["name"] for c in insp.get_columns(tbl)} assert "deleted_at" in cols, f"{tbl} missing deleted_at" def test_standard_timestamp_columns_present(insp): for tbl in sorted(TIMESTAMP_TABLES): cols = {c["name"] for c in insp.get_columns(tbl)} assert "created_at" in cols, f"{tbl} missing created_at" assert "updated_at" in cols, f"{tbl} missing updated_at" def test_partial_index_for_soft_delete(db_engine_or_skip): """Each soft-delete table must carry an `ix_