"""Unit tests for the operations helper rules that must hold without a database. The database paths (backup, restore, verify) are exercised by the #15 drill against real MySQL; what is checked here is the logic that decides whether an operation is allowed at all. """ import sys import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) import ops # noqa: E402 class VersionParsingTests(unittest.TestCase): def test_reads_the_version_after_distrib(self): # A client banner's first number is the protocol version, not the server version. self.assertEqual(ops.version_of("mysql Ver 14.14 Distrib 5.7.38, for Win64 (x86_64)"), (5, 7, 38)) self.assertEqual(ops.version_of("mysql Ver 8.4.3 for Win64 on x86_64"), (8, 4, 3)) self.assertIsNone(ops.version_of("")) self.assertIsNone(ops.version_of("no version here")) def test_compares_client_and_server_versions(self): self.assertLess(ops.version_of("Ver 14.14 Distrib 5.7.38"), ops.version_of("8.4.3")) self.assertGreaterEqual(ops.version_of("Ver 8.0.36"), ops.version_of("8.4.3".replace("8.4.3", "8.0.36"))) self.assertGreaterEqual(ops.version_of("8.4.3"), ops.version_of("8.4.3")) class DatabaseNameTests(unittest.TestCase): def test_accepts_lexgo_names(self): for name in ("lexgo_dev", "lexgo_prod", "lexgo_restore_drill"): self.assertEqual(ops.validate_database_name(name, "测试"), name) def test_refuses_system_and_unrelated_names(self): for name in ("mysql", "information_schema", "performance_schema", "sys", "app_prod", ""): with self.assertRaises(SystemExit): ops.validate_database_name(name, "测试") class DumpSafetyTests(unittest.TestCase): """A dump that carries CREATE DATABASE/USE would restore into the wrong schema.""" def _write_dump(self, body): import gzip import tempfile handle = tempfile.NamedTemporaryFile(delete=False, suffix=".sql.gz") handle.close() with gzip.open(handle.name, "wb") as target: target.write(body.encode("utf-8")) self.addCleanup(Path(handle.name).unlink) return Path(handle.name) def test_flags_a_dump_that_switches_database(self): dump = self._write_dump("-- MySQL dump\nCREATE DATABASE /*!32312 IF NOT EXISTS*/ `lexgo_dev`;\nUSE `lexgo_dev`;\nCREATE TABLE t (id INT);\n") self.assertTrue(ops.dump_targets_instead_of_source(dump)) def test_flags_a_dump_with_use_only(self): dump = self._write_dump("-- MySQL dump\nUSE `lexgo_dev`;\nCREATE TABLE t (id INT);\n") self.assertTrue(ops.dump_targets_instead_of_source(dump)) def test_accepts_a_table_only_dump(self): dump = self._write_dump("-- MySQL dump\nDROP TABLE IF EXISTS `lexgo_terms`;\nCREATE TABLE `lexgo_terms` (id INT);\nINSERT INTO `lexgo_terms` VALUES (1);\n") self.assertFalse(ops.dump_targets_instead_of_source(dump)) class ManifestTests(unittest.TestCase): def test_manifest_keys_never_name_a_credential(self): """The manifest is written to disk, so its field names are checked, not its prose.""" import re source = (ROOT / "scripts" / "ops.py").read_text(encoding="utf-8") block = source[source.index(" manifest = {"):source.index('(out_dir / "manifest.json")')] keys = re.findall(r'^\s+"([a-z_]+)":', block, re.M) self.assertIn("schema_version", keys) self.assertIn("row_counts", keys) for key in keys: for banned in ("password", "secret", "token", "credential"): self.assertNotIn(banned, key.lower(), key) def test_audit_tables_declare_no_sensitive_column(self): """The integrity check must keep naming the columns that may not appear in the audit logs.""" self.assertTrue({"password", "token", "body", "content"} <= ops.AUDIT_BANNED_COLUMNS) self.assertIn("lexgo_login_logs", ops.TABLES) self.assertIn("lexgo_operation_logs", ops.TABLES) if __name__ == "__main__": unittest.main()