This commit is contained in:
1 parent
125eab3cd4
commit
045d3a8fd3
8 files changed
+375
-65
No files matched your search
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise destructive-import guards without connecting to any real database."""
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "docker/init-db.sh"
|
||||
BASE = ["in_daper", "in_soal", "in_jawab"]
|
||||
ALL = BASE + ["rec_admin", "rec_exam_attempt", "rec_exam_event", "rec_candidate_login_guard"]
|
||||
|
||||
FAKE_MYSQL = r'''#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["FAKE_MYSQL_STATE"])
|
||||
state = json.loads(path.read_text())
|
||||
args = sys.argv[1:]
|
||||
query = args[args.index("-e") + 1] if "-e" in args else None
|
||||
if query is None:
|
||||
sys.stdin.read()
|
||||
state["imports"] = state.get("imports", 0) + 1
|
||||
state["tables"] += state.get("import_tables", [])
|
||||
path.write_text(json.dumps(state))
|
||||
if state.get("fail_import"):
|
||||
print("Synthetic import failure", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif "information_schema.tables" in query:
|
||||
tables = state["tables"]
|
||||
if "table_name = 'rec_bootstrap_state'" in query:
|
||||
print(int("rec_bootstrap_state" in tables))
|
||||
elif "table_name IN" in query:
|
||||
names = re.findall(r"'([^']+)'", query.split("table_name IN", 1)[1])
|
||||
print(sum(name in tables for name in names))
|
||||
else:
|
||||
print(len(tables))
|
||||
elif query.startswith("SELECT status"):
|
||||
print(state.get("status", ""))
|
||||
elif query.startswith("CREATE TABLE rec_bootstrap_state"):
|
||||
if "rec_bootstrap_state" in state["tables"]:
|
||||
sys.exit(1)
|
||||
state["tables"].append("rec_bootstrap_state")
|
||||
state["status"] = "importing"
|
||||
path.write_text(json.dumps(state))
|
||||
elif query.startswith("UPDATE rec_bootstrap_state"):
|
||||
state["status"] = "imported"
|
||||
path.write_text(json.dumps(state))
|
||||
else:
|
||||
print("Unexpected query: " + query, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
'''
|
||||
|
||||
|
||||
class InitializationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
client = self.root / "mysql"
|
||||
client.write_text(FAKE_MYSQL)
|
||||
client.chmod(0o700)
|
||||
self.state_path = self.root / "state.json"
|
||||
self.dump = self.root / "initial.sql"
|
||||
self.dump.write_text("-- Synthetic SQL; never loads project personal data.\n")
|
||||
self.env = dict(os.environ, PATH=f"{self.root}:{os.environ['PATH']}",
|
||||
MYSQL_HOST="test-db", MYSQL_USER="test-user",
|
||||
MYSQL_DATABASE="test-db", MYSQL_PASSWORD=secrets.token_hex(16),
|
||||
FAKE_MYSQL_STATE=str(self.state_path), DB_INIT_SQL_FILE=str(self.dump))
|
||||
|
||||
def state(self):
|
||||
try:
|
||||
return json.loads(self.state_path.read_text())
|
||||
except (OSError, ValueError) as error:
|
||||
self.fail(f"Invalid mock database state: {error}")
|
||||
|
||||
def configure(self, tables=None, **options):
|
||||
self.state_path.write_text(json.dumps(dict(tables=tables or [], import_tables=ALL, **options)))
|
||||
|
||||
def run_init(self):
|
||||
return subprocess.run(["sh", str(SCRIPT)], env=self.env, capture_output=True, text=True)
|
||||
|
||||
def test_empty_database_imports_once(self):
|
||||
self.configure()
|
||||
first = self.run_init()
|
||||
self.assertEqual(first.returncode, 0, first.stderr)
|
||||
self.assertEqual(self.state()["status"], "imported")
|
||||
second = self.run_init()
|
||||
self.assertEqual(second.returncode, 0, second.stderr)
|
||||
self.assertEqual(self.state()["imports"], 1)
|
||||
|
||||
def test_existing_complete_database_skips_import(self):
|
||||
self.configure(ALL)
|
||||
result = self.run_init()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertNotIn("imports", self.state())
|
||||
self.assertEqual(self.state()["tables"], ALL)
|
||||
|
||||
def test_partial_database_refuses_import(self):
|
||||
self.configure(BASE)
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertNotIn("imports", self.state())
|
||||
|
||||
def test_unrelated_database_refuses_import(self):
|
||||
self.configure(["unrelated_table"])
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertNotIn("imports", self.state())
|
||||
|
||||
def test_failed_import_blocks_automatic_retry(self):
|
||||
self.configure(fail_import=True)
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertEqual(self.state()["status"], "importing")
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertEqual(self.state()["imports"], 1)
|
||||
|
||||
def test_missing_dump_leaves_database_untouched(self):
|
||||
self.configure()
|
||||
self.dump.unlink()
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertEqual(self.state()["tables"], [])
|
||||
|
||||
def test_empty_dump_leaves_database_untouched(self):
|
||||
self.configure()
|
||||
self.dump.write_text("")
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertEqual(self.state()["tables"], [])
|
||||
|
||||
def test_dump_cannot_replace_the_safety_marker(self):
|
||||
self.configure()
|
||||
self.dump.write_text("DROP TABLE IF EXISTS `REC_BOOTSTRAP_STATE`;\n")
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertEqual(self.state()["tables"], [])
|
||||
self.assertNotIn("imports", self.state())
|
||||
|
||||
def test_dump_missing_base_tables_is_not_marked_imported(self):
|
||||
self.configure()
|
||||
state = self.state()
|
||||
state["import_tables"] = ["in_daper"]
|
||||
self.state_path.write_text(json.dumps(state))
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertEqual(self.state()["status"], "importing")
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertEqual(self.state()["imports"], 1)
|
||||
|
||||
def test_imported_marker_with_missing_tables_is_refused(self):
|
||||
self.configure(["rec_bootstrap_state", "in_daper"], status="imported")
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertNotIn("imports", self.state())
|
||||
|
||||
def test_marker_with_missing_status_is_refused(self):
|
||||
self.configure(["rec_bootstrap_state"])
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertNotIn("imports", self.state())
|
||||
|
||||
def test_missing_password_is_refused_before_access(self):
|
||||
self.configure()
|
||||
del self.env["MYSQL_PASSWORD"]
|
||||
self.assertNotEqual(self.run_init().returncode, 0)
|
||||
self.assertEqual(self.state()["tables"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user