test_approval_audit.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """人工审批审计记录测试。"""
  2. from __future__ import annotations
  3. from datetime import datetime, timedelta, timezone
  4. from pathlib import Path
  5. from tempfile import TemporaryDirectory
  6. import unittest
  7. from vendor_guard.approval_audit import (
  8. ApprovalAuditRecord,
  9. append_approval_audit,
  10. resolve_report_path,
  11. )
  12. class ApprovalAuditTests(unittest.TestCase):
  13. def setUp(self) -> None:
  14. self.temp_dir = TemporaryDirectory()
  15. self.addCleanup(self.temp_dir.cleanup)
  16. self.project_root = Path(self.temp_dir.name)
  17. self.report_path = self.project_root / "reports" / "ACME" / "onboarding-report.md"
  18. self.report_path.parent.mkdir(parents=True)
  19. self.report_path.write_text("# ACME 报告\n", encoding="utf-8")
  20. def _record(self, *, human_decision: str = "approve") -> ApprovalAuditRecord:
  21. status = "提交工具已执行" if human_decision == "approve" else "未执行(人工拒绝)"
  22. return ApprovalAuditRecord(
  23. record_id="approval-001",
  24. vendor_name="ACME",
  25. proposed_decision="conditional",
  26. human_decision=human_decision,
  27. handled_at=datetime(
  28. 2026,
  29. 7,
  30. 23,
  31. 15,
  32. 42,
  33. 18,
  34. tzinfo=timezone(timedelta(hours=8)),
  35. ),
  36. operator="course-user",
  37. thread_id="vendor-acme-001",
  38. trace_url="https://smith.example/r/trace-id",
  39. execution_status=status,
  40. )
  41. def test_resolves_virtual_report_path_inside_project(self) -> None:
  42. resolved = resolve_report_path(
  43. self.project_root,
  44. "/reports/ACME/onboarding-report.md",
  45. )
  46. self.assertEqual(resolved, self.report_path.resolve())
  47. def test_rejects_path_outside_reports_directory(self) -> None:
  48. with self.assertRaisesRegex(ValueError, "reports"):
  49. resolve_report_path(self.project_root, "/reports/../.env")
  50. def test_approved_record_contains_audit_fields(self) -> None:
  51. append_approval_audit(self.report_path, self._record())
  52. report = self.report_path.read_text(encoding="utf-8")
  53. self.assertIn("## 人工审批记录", report)
  54. self.assertIn("| 建议准入决定 | conditional |", report)
  55. self.assertIn("| 人工处理结果 | 同意执行提交 |", report)
  56. self.assertIn("2026-07-23T15:42:18+08:00", report)
  57. self.assertIn("| 操作人(本机账户,未经身份认证) | course-user |", report)
  58. self.assertIn("https://smith.example/r/trace-id", report)
  59. self.assertIn("| 提交状态 | 提交工具已执行 |", report)
  60. def test_rejected_record_says_submission_was_not_executed(self) -> None:
  61. self.report_path.write_text(
  62. "# ACME 报告\n\n"
  63. "> ⏳ **本报告已提交人工审批,审批结果将由 CLI 写入。**\n",
  64. encoding="utf-8",
  65. )
  66. append_approval_audit(
  67. self.report_path,
  68. self._record(human_decision="reject"),
  69. )
  70. report = self.report_path.read_text(encoding="utf-8")
  71. self.assertIn("| 人工处理结果 | 拒绝执行提交 |", report)
  72. self.assertIn("| 提交状态 | 未执行(人工拒绝) |", report)
  73. self.assertIn("人工审批已完成:拒绝执行提交", report)
  74. self.assertNotIn("审批结果将", report)
  75. def test_same_record_is_not_appended_twice(self) -> None:
  76. record = self._record()
  77. self.assertTrue(append_approval_audit(self.report_path, record))
  78. self.assertFalse(append_approval_audit(self.report_path, record))
  79. report = self.report_path.read_text(encoding="utf-8")
  80. self.assertEqual(report.count("## 人工审批记录"), 1)
  81. if __name__ == "__main__":
  82. unittest.main()