test_market_mobile_menu_sql.py 6.6 KB

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