Skip to content

Commit 1e9cf30

Browse files
Merge issue-31-ci-test-db-fix: CI test database guards (#31)
2 parents af51069 + ab5f472 commit 1e9cf30

4 files changed

Lines changed: 56 additions & 13 deletions

File tree

_docs/architecture/shared-primitives.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ dispatch models remain owned by their named `core` or `jobs` modules. Domain cod
7171
services instead of importing presentation or worker code.
7272

7373
PostgreSQL row and statement triggers reject application `UPDATE`, `DELETE`, and `TRUNCATE` of
74-
append-only audit evidence. The test settings module explicitly opts into omitting the truncate
75-
trigger, and the migration accepts that opt-in only for a Django-generated database whose name has
76-
the `test_` prefix, so `TransactionTestCase` can flush without weakening a deployed database.
74+
append-only audit evidence. The code-owned test settings module explicitly opts into omitting the
75+
truncate trigger, and the migration accepts that opt-in only for the ephemeral CI database
76+
`dtc_test` or a Django-generated database whose name has the `test_` prefix. A database name alone
77+
never enables the exception, so `TransactionTestCase` can flush without weakening a deployed
78+
database.
7779
Production maintenance must use an explicitly reviewed privileged procedure. These triggers harden
7880
normal application and operator paths, but they do not protect against a database table owner who
7981
deliberately drops or disables them.

core/migrations/0001_initial.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
),
1717
)
1818

19+
APPEND_ONLY_EXPLICIT_TEST_DATABASE_NAMES = frozenset({"dtc_test"})
20+
21+
22+
def is_append_only_test_database(database_name):
23+
return (
24+
database_name in APPEND_ONLY_EXPLICIT_TEST_DATABASE_NAMES
25+
or database_name.startswith("test_")
26+
)
27+
1928

2029
def install_append_only_guards(apps, schema_editor):
2130
del apps
@@ -26,7 +35,7 @@ def install_append_only_guards(apps, schema_editor):
2635
allow_test_flush = bool(
2736
getattr(settings, "CORE_ALLOW_APPEND_ONLY_TEST_FLUSH", False)
2837
)
29-
if allow_test_flush and not database_name.startswith("test_"):
38+
if allow_test_flush and not is_append_only_test_database(database_name):
3039
raise RuntimeError("append-only test flush requires a Django test database")
3140
for table, function, retention_column in APPEND_ONLY_GUARDS:
3241
trigger = f"{function}_trigger"

core/tests/test_shared_primitives.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -688,19 +688,50 @@ def test_migration_test_flush_exception_requires_explicit_setting_and_test_name(
688688
source = migration.read_text()
689689
self.assertIn("BEFORE TRUNCATE", source)
690690
self.assertIn("CORE_ALLOW_APPEND_ONLY_TEST_FLUSH", source)
691+
self.assertIn('frozenset({"dtc_test"})', source)
691692
self.assertIn('database_name.startswith("test_")', source)
692693
self.assertIn("truncate_trigger", source)
693694

694695
migration_module = import_module("core.migrations.0001_initial")
695-
schema_editor = mock.Mock()
696-
schema_editor.connection.vendor = "postgresql"
697-
schema_editor.connection.settings_dict = {"NAME": "production"}
698-
with (
699-
override_settings(CORE_ALLOW_APPEND_ONLY_TEST_FLUSH=True),
700-
self.assertRaisesRegex(RuntimeError, "requires a Django test database"),
696+
697+
def schema_editor_for(database_name: str):
698+
schema_editor = mock.Mock()
699+
schema_editor.connection.vendor = "postgresql"
700+
schema_editor.connection.settings_dict = {"NAME": database_name}
701+
schema_editor.connection.ops.quote_name.side_effect = lambda value: f'"{value}"'
702+
return schema_editor
703+
704+
for database_name in ("dtc_test", "test_dtc_test", "test_dtc_test_1"):
705+
with self.subTest(database_name=database_name):
706+
schema_editor = schema_editor_for(database_name)
707+
with override_settings(CORE_ALLOW_APPEND_ONLY_TEST_FLUSH=True):
708+
migration_module.install_append_only_guards(None, schema_editor)
709+
statements = "\n".join(
710+
str(call.args[0]) for call in schema_editor.execute.call_args_list
711+
)
712+
self.assertNotIn("BEFORE TRUNCATE", statements)
713+
self.assertIn("BEFORE UPDATE OR DELETE", statements)
714+
715+
for database_name in (
716+
"production",
717+
"website_test",
718+
"dtc_test_1",
719+
"dtc_test_backup",
701720
):
721+
with self.subTest(database_name=database_name):
722+
schema_editor = schema_editor_for(database_name)
723+
with (
724+
override_settings(CORE_ALLOW_APPEND_ONLY_TEST_FLUSH=True),
725+
self.assertRaisesRegex(RuntimeError, "requires a Django test database"),
726+
):
727+
migration_module.install_append_only_guards(None, schema_editor)
728+
schema_editor.execute.assert_not_called()
729+
730+
schema_editor = schema_editor_for("dtc_test")
731+
with override_settings(CORE_ALLOW_APPEND_ONLY_TEST_FLUSH=False):
702732
migration_module.install_append_only_guards(None, schema_editor)
703-
schema_editor.execute.assert_not_called()
733+
statements = "\n".join(str(call.args[0]) for call in schema_editor.execute.call_args_list)
734+
self.assertIn("BEFORE TRUNCATE", statements)
704735

705736
@skipUnlessDBFeature("has_select_for_update")
706737
def test_postgresql_production_truncate_guards_reject_and_reapply(self) -> None:

website/settings/test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
NOINDEX = True
2727
OBSERVABILITY_EVENT_BACKENDS = ["noop"]
2828
# Django's TransactionTestCase flushes tables with TRUNCATE. The core migration
29-
# also requires the connection's generated database name to start with `test_`,
30-
# so this code-owned opt-in cannot weaken a deployed database accidentally.
29+
# also requires either Django's generated `test_*` name or the explicitly
30+
# provisioned CI database `dtc_test`, so this code-owned opt-in cannot weaken a
31+
# deployed database merely because of its name.
3132
CORE_ALLOW_APPEND_ONLY_TEST_FLUSH = True
3233
Q_CLUSTER = {**Q_CLUSTER, "sync": True} # noqa: F405
3334
MIDDLEWARE = [

0 commit comments

Comments
 (0)