test_market_mobile_menu_sql.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import re
  2. from pathlib import Path
  3. MIGRATION = (
  4. Path(__file__).resolve().parents[2]
  5. / "docs/sql/migrations/2026-10-10-market-mobile-menus.sql"
  6. )
  7. ROLES = {
  8. "specialist": "市场专员",
  9. "manager": "地区经理",
  10. }
  11. MODULES = {
  12. "presign": "预签约订单",
  13. "protocol": "协议单",
  14. "payment-chase": "催缴信息",
  15. "payment": "缴费信息",
  16. "sample": "样本信息",
  17. "freight": "运费信息",
  18. }
  19. def _sql() -> str:
  20. return MIGRATION.read_text(encoding="utf-8")
  21. def _compact(sql: str) -> str:
  22. return re.sub(r"\s+", " ", sql)
  23. def _without_comments(sql: str) -> str:
  24. return re.sub(r"--[^\n]*", "", sql)
  25. def _insert_statements(sql: str) -> list[str]:
  26. return [
  27. _compact(statement)
  28. for statement in _without_comments(sql).split(";")
  29. if re.search(r"\bINSERT\s+INTO\s+sys_menu\b", statement, re.IGNORECASE)
  30. ]
  31. def test_market_mobile_menu_migration_defines_two_empty_path_directories():
  32. sql = _compact(_without_comments(_sql()))
  33. for directory_name in ROLES.values():
  34. assert re.search(
  35. rf"'{directory_name}'\s*,\s*root\.id\s*,\s*'M'\s*,\s*''",
  36. sql,
  37. )
  38. def test_market_mobile_menu_migration_defines_exactly_twelve_fixed_leaves():
  39. sql = _without_comments(_sql())
  40. expected_paths = {
  41. f"/pages/market/webview/index?entry={role}-{module}"
  42. for role in ROLES
  43. for module in MODULES
  44. }
  45. expected_perms = {
  46. f"mini:market:{role}:{module}"
  47. for role in ROLES
  48. for module in MODULES
  49. }
  50. paths = re.findall(
  51. r"['\"](/pages/market/webview/index\?entry=[a-z-]+)['\"]", sql
  52. )
  53. perms = re.findall(r"['\"](mini:market:[a-z-]+:[a-z-]+)['\"]", sql)
  54. assert len(paths) == 12
  55. assert set(paths) == expected_paths
  56. assert len(perms) == 12
  57. assert set(perms) == expected_perms
  58. for role in ROLES:
  59. for module in MODULES:
  60. path = f"/pages/market/webview/index?entry={role}-{module}"
  61. permission = f"mini:market:{role}:{module}"
  62. assert re.search(
  63. rf"'{re.escape(path)}'\s*(?:AS\s+path)?\s*,\s*"
  64. rf"'{re.escape(permission)}'\s*(?:AS\s+perms)?",
  65. sql,
  66. re.IGNORECASE,
  67. )
  68. assert re.search(
  69. r"entry\.menu_name\s*,\s*directory\.id\s*,\s*'C'\s*,\s*entry\.path",
  70. _compact(sql),
  71. )
  72. def test_market_mobile_menu_migration_is_idempotent_and_tenant_aware():
  73. sql = _sql()
  74. compact = _compact(_without_comments(sql))
  75. inserts = _insert_statements(sql)
  76. assert len(inserts) == 3
  77. assert all(re.search(r"\bNOT\s+EXISTS\b", item, re.IGNORECASE) for item in inserts)
  78. assert re.search(r"root\.parent_id\s*=\s*'0'", compact)
  79. assert re.search(r"root\.menu_name\s*=\s*'移动端菜单'", compact)
  80. assert re.search(r"root\.del_flag\s*=\s*0", compact)
  81. assert compact.count("root.tenant_no") >= len(inserts)
  82. assert "'zkxw'" not in sql
  83. assert re.search(r"existing\.parent_id\s*=\s*directory\.id", compact)
  84. assert re.search(r"existing\.path\s*=\s*entry\.path", compact)
  85. assert re.search(r"existing\.menu_type\s*=\s*'C'", compact)
  86. def test_each_insert_uses_the_complete_active_natural_key_without_bare_id_or():
  87. specialist, manager, leaves = _insert_statements(_sql())
  88. for directory, name in ((specialist, "市场专员"), (manager, "地区经理")):
  89. assert "existing.id" not in directory
  90. assert re.search(r"existing\.tenant_no\s*=\s*root\.tenant_no", directory)
  91. assert re.search(r"existing\.parent_id\s*=\s*root\.id", directory)
  92. assert re.search(r"existing\.menu_name\s*=\s*'" + name + r"'", directory)
  93. assert re.search(r"existing\.menu_type\s*=\s*'M'", directory)
  94. assert re.search(r"existing\.path\s*=\s*''", directory)
  95. assert re.search(r"existing\.del_flag\s*=\s*0", directory)
  96. assert "existing.id" not in leaves
  97. assert re.search(r"existing\.tenant_no\s*=\s*root\.tenant_no", leaves)
  98. assert re.search(r"existing\.parent_id\s*=\s*directory\.id", leaves)
  99. assert re.search(r"existing\.menu_name\s*=\s*entry\.menu_name", leaves)
  100. assert re.search(r"existing\.menu_type\s*=\s*'C'", leaves)
  101. assert re.search(r"existing\.path\s*=\s*entry\.path", leaves)
  102. assert re.search(r"existing\.perms\s*=\s*entry\.perms", leaves)
  103. assert re.search(r"existing\.del_flag\s*=\s*0", leaves)
  104. def test_ids_and_directory_join_are_scoped_to_the_expected_tree_location():
  105. specialist, manager, leaves = _insert_statements(_sql())
  106. for directory in (specialist, manager):
  107. id_expression = re.search(
  108. r"\bSELECT\s+(MD5\(.*?\))\s*,\s*'(?:市场专员|地区经理)'",
  109. directory,
  110. ).group(1)
  111. assert "root.tenant_no" in id_expression
  112. assert "root.id" in id_expression
  113. leaf_id_expression = re.search(
  114. r"\bSELECT\s+(MD5\(.*?\))\s*,\s*entry\.menu_name", leaves
  115. ).group(1)
  116. assert "root.tenant_no" in leaf_id_expression
  117. assert "root.id" in leaf_id_expression
  118. assert "directory.id" in leaf_id_expression
  119. assert "entry.path" in leaf_id_expression
  120. assert re.search(r"directory\.path\s*=\s*''", leaves)
  121. def test_same_id_wrong_or_soft_deleted_rows_are_repaired_instead_of_blocking_insert():
  122. specialist, manager, leaves = _insert_statements(_sql())
  123. for statement in (specialist, manager, leaves):
  124. assert re.search(r"\bON\s+DUPLICATE\s+KEY\s+UPDATE\b", statement)
  125. repair = re.split(
  126. r"\bON\s+DUPLICATE\s+KEY\s+UPDATE\b", statement, maxsplit=1
  127. )[1]
  128. for field in (
  129. "tenant_no",
  130. "parent_id",
  131. "menu_name",
  132. "menu_type",
  133. "path",
  134. "perms",
  135. "del_flag",
  136. ):
  137. assert re.search(
  138. rf"\b{field}\s*=\s*VALUES\s*\(\s*{field}\s*\)", repair
  139. ), field
  140. def test_market_mobile_menu_migration_does_not_grant_roles_automatically():
  141. assert "sys_role_menu" not in _without_comments(_sql())