test_substore_operator.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import json
  2. import pathlib
  3. import subprocess
  4. import unittest
  5. PROJECT_ROOT = pathlib.Path(__file__).resolve().parent.parent
  6. class TestSubStoreOperatorBugs(unittest.TestCase):
  7. """验证 substore/operator_template.js 中的类型守卫、域名正则校验与时效降级"""
  8. def _eval_node(self, script):
  9. js_file = PROJECT_ROOT / "substore" / "operator_template.js"
  10. node_runner = f"""
  11. const fs = require('fs');
  12. const vm = require('vm');
  13. const code = fs.readFileSync({json.dumps(str(js_file))}, 'utf8');
  14. const sandbox = {{ console, Date, Number, String, Array, Object, RegExp, JSON, Error, setTimeout, clearTimeout }};
  15. vm.createContext(sandbox);
  16. vm.runInContext(code, sandbox);
  17. {script}
  18. """
  19. proc = subprocess.run(["node", "-e", node_runner], capture_output=True, text=True)
  20. return proc
  21. def test_p1_extract_value_object_stringification_hazard_prevented(self):
  22. """[P1 修复验证] 验证 extractValue() 拒绝非字符串对象,杜绝 '[object object]' 注入"""
  23. script = """
  24. try {
  25. const obj = { domain: { host: "cf.example.com" } };
  26. sandbox.extractValue(obj, 'server');
  27. console.log(JSON.stringify({ ok: true }));
  28. } catch (e) {
  29. console.log(JSON.stringify({ ok: false, error: e.message }));
  30. }
  31. """
  32. proc = self._eval_node(script)
  33. self.assertEqual(proc.returncode, 0, f"Node 执行出错: {proc.stderr}")
  34. res = json.loads(proc.stdout)
  35. self.assertFalse(res["ok"], "非字符串对象必须被拒绝")
  36. self.assertIn("empty value field", res["error"])
  37. def test_p1_extract_value_host_validation_enforced(self):
  38. """[P1 修复验证] 验证 extractValue() 严格校验域名与 IP 格式,拒绝非法字符与报错页面"""
  39. script = """
  40. const results = {};
  41. // 1. HTML 报错应被拒绝
  42. try {
  43. sandbox.extractValue({ domain: "<html>502 Bad Gateway</html>" }, 'server');
  44. results.html = "allowed";
  45. } catch (e) {
  46. results.html = "rejected";
  47. }
  48. // 2. 空格/非法字符应被拒绝
  49. try {
  50. sandbox.extractValue({ domain: "bad domain with spaces" }, 'server');
  51. results.spaces = "allowed";
  52. } catch (e) {
  53. results.spaces = "rejected";
  54. }
  55. // 3. 合法域名应正常提取
  56. try {
  57. results.valid_domain = sandbox.extractValue({ domain: "SAAS.SIN.FAN" }, 'server');
  58. } catch (e) {
  59. results.valid_domain = "failed: " + e.message;
  60. }
  61. // 4. 合法 IPv4 应正常提取
  62. try {
  63. results.valid_ip = sandbox.extractValue({ ip: "104.23.141.57" }, 'server');
  64. } catch (e) {
  65. results.valid_ip = "failed: " + e.message;
  66. }
  67. console.log(JSON.stringify(results));
  68. """
  69. proc = self._eval_node(script)
  70. self.assertEqual(proc.returncode, 0, f"Node 执行出错: {proc.stderr}")
  71. res = json.loads(proc.stdout)
  72. self.assertEqual(res["html"], "rejected", "HTML 标签必须被拒绝")
  73. self.assertEqual(res["spaces"], "rejected", "包含空格的非法 host 必须被拒绝")
  74. self.assertEqual(res["valid_domain"], "saas.sin.fan", "合法域名小写化正常提取")
  75. self.assertEqual(res["valid_ip"], "104.23.141.57", "合法 IP 正常提取")
  76. def test_p2_operator_template_checks_staleness(self):
  77. """[P2 修复验证] 验证 operator_template.js 校验 updated_at 时效,超时自动判定为 stale"""
  78. script = """
  79. vm.runInContext(`
  80. const now = Date.now();
  81. const freshDate = new Date(now - 3600 * 1000).toISOString(); // 1 小时前
  82. const staleDate = new Date(now - 100000 * 1000).toISOString(); // 27 小时前 (> 86400s)
  83. const freshResult = isRouterDataFresh({ updated_at: freshDate });
  84. const staleResult = isRouterDataFresh({ updated_at: staleDate });
  85. console.log(JSON.stringify({ freshResult, staleResult, maxAge: LOCAL_MAX_AGE_SEC }));
  86. `, sandbox);
  87. """
  88. proc = self._eval_node(script)
  89. self.assertEqual(proc.returncode, 0, f"Node 执行出错: {proc.stderr}")
  90. res = json.loads(proc.stdout)
  91. self.assertTrue(res["freshResult"], "1 小时前数据判定为 fresh")
  92. self.assertFalse(res["staleResult"], "27 小时前数据必须判定为 stale 并触发自动降级")
  93. self.assertEqual(res["maxAge"], 86400, "LOCAL_MAX_AGE_SEC 默认为 86400 秒")
  94. if __name__ == "__main__":
  95. unittest.main()