audit_test.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. package audit
  2. import (
  3. "context"
  4. "os"
  5. "testing"
  6. "github.com/kairo/haven/internal/db"
  7. )
  8. func TestRecordRequiresDB(t *testing.T) {
  9. // 未初始化数据库时 record 应返回错误,而 Record 只记日志不 panic
  10. w := NewWriter(nil)
  11. if err := w.record(context.Background(), Event{EventType: EventLogout}); err == nil {
  12. t.Error("未初始化时应返回错误")
  13. }
  14. w.Record(context.Background(), Event{EventType: EventLogout})
  15. }
  16. // 集成测试:需要真实的 PostgreSQL(已跑 0001_init.up.sql)。
  17. //
  18. // HAVEN_TEST_DB_DSN='postgres://haven:...@127.0.0.1:5432/haven_dev?sslmode=disable' \
  19. // go test ./internal/audit/ -v
  20. func openTestWriter(t *testing.T) (*Writer, func()) {
  21. t.Helper()
  22. dsn := os.Getenv("HAVEN_TEST_DB_DSN")
  23. if dsn == "" {
  24. t.Skip("未设置 HAVEN_TEST_DB_DSN,跳过数据库集成测试")
  25. }
  26. conn, err := db.Open(context.Background(), dsn)
  27. if err != nil {
  28. t.Fatalf("连接测试库失败: %v", err)
  29. }
  30. if _, err := conn.Exec(`TRUNCATE audit_events CASCADE`); err != nil {
  31. t.Fatalf("清理审计表失败: %v", err)
  32. }
  33. return NewWriter(conn), func() { conn.Close() }
  34. }
  35. func TestRecordAppendsEvents(t *testing.T) {
  36. w, closeDB := openTestWriter(t)
  37. defer closeDB()
  38. ctx := context.Background()
  39. // 无对象事件:target_type 应写成字符串 'null'
  40. w.Record(ctx, Event{
  41. ActorUsername: "kairo",
  42. EventType: EventLoginSuccess,
  43. Metadata: map[string]any{"via": "password+totp"},
  44. IP: "127.0.0.1",
  45. UserAgent: "test-agent",
  46. })
  47. if err := w.record(ctx, Event{
  48. ActorUsername: "kairo",
  49. EventType: EventLogout,
  50. }); err != nil {
  51. t.Fatalf("record 返回错误: %v", err)
  52. }
  53. var count int
  54. var targetType string
  55. var ip string
  56. if err := w.db.QueryRow(`SELECT count(*) FROM audit_events`).Scan(&count); err != nil {
  57. t.Fatal(err)
  58. }
  59. if count != 2 {
  60. t.Errorf("应写入 2 条事件,实际 %d", count)
  61. }
  62. if err := w.db.QueryRow(
  63. `SELECT target_type, ip FROM audit_events WHERE event_type = $1`, EventLoginSuccess).
  64. Scan(&targetType, &ip); err != nil {
  65. t.Fatal(err)
  66. }
  67. if targetType != TargetNull {
  68. t.Errorf("无对象的 target_type 应为 %q,实际 %q", TargetNull, targetType)
  69. }
  70. if ip != "127.0.0.1" {
  71. t.Errorf("ip = %q", ip)
  72. }
  73. }
  74. func TestRecordMetadataJSON(t *testing.T) {
  75. w, closeDB := openTestWriter(t)
  76. defer closeDB()
  77. if err := w.record(context.Background(), Event{
  78. EventType: EventTokenCreated,
  79. TargetType: TargetToken,
  80. ActorUsername: "kairo",
  81. Metadata: map[string]any{
  82. "name": "cli",
  83. "scope": "read",
  84. "token_prefix": "haven_ro",
  85. },
  86. }); err != nil {
  87. t.Fatalf("record 返回错误: %v", err)
  88. }
  89. var name, scope string
  90. if err := w.db.QueryRow(
  91. `SELECT metadata->>'name', metadata->>'scope' FROM audit_events WHERE event_type = $1`,
  92. EventTokenCreated).Scan(&name, &scope); err != nil {
  93. t.Fatal(err)
  94. }
  95. if name != "cli" || scope != "read" {
  96. t.Errorf("metadata 解析异常: name=%q scope=%q", name, scope)
  97. }
  98. }
  99. func TestRecordRejectsUnknownEventType(t *testing.T) {
  100. w, closeDB := openTestWriter(t)
  101. defer closeDB()
  102. // CHECK 约束兜底:文档未列的事件类型必须写不进去(audit.md 第三节「新增事件类型必须同步更新 CHECK」)
  103. if err := w.record(context.Background(), Event{EventType: "bogus_event"}); err == nil {
  104. t.Error("未定义的事件类型应被 CHECK 约束拒绝")
  105. }
  106. }
苏ICP备2026082406号
苏公网安备 待审核