package postgres import ( "context" "github.com/jackc/pgx/v5/pgxpool" "os" "testing" ) func TestSourceAuditPersistsOperations(t *testing.T) { url := isolatedIntegrationDatabaseURL(t, os.Getenv("TEST_DATABASE_URL")) if url == "" { t.Skip("TEST_DATABASE_URL is not set") } ctx := context.Background() pool, err := pgxpool.New(ctx, url) if err != nil { t.Fatal(err) } defer pool.Close() if err := ApplyMigrations(ctx, pool); err != nil { t.Fatal(err) } var exists bool if err := pool.QueryRow(ctx, `SELECT to_regclass('source_configuration_events') IS NOT NULL`).Scan(&exists); err != nil { t.Fatal(err) } if !exists { t.Fatal("configuration operations have no persistent audit log") } _, err = pool.Exec(ctx, `INSERT INTO source_connections(id,source_type,name,enabled,updated_by) VALUES ('audit-test','dingtalk','测试知识库',true,'tester'); UPDATE source_connections SET enabled=false,updated_by='tester',updated_at=now() WHERE id='audit-test'; UPDATE source_connections SET enabled=true,updated_by='tester',updated_at=now() WHERE id='audit-test'; UPDATE source_connections SET schedule='{"refresh_interval_seconds":3600}',updated_by='tester',updated_at=now() WHERE id='audit-test'; UPDATE source_connections SET archived_at=now(),archived_by='tester',enabled=false,updated_by='tester',updated_at=now() WHERE id='audit-test'; UPDATE source_connections SET archived_at=archived_at,updated_at=now() WHERE id='audit-test'; UPDATE source_connections SET archived_at=NULL,archived_by='',updated_by='tester',updated_at=now() WHERE id='audit-test'; UPDATE source_connections SET last_scheduled_at=now(),next_refresh_at=now(),updated_at=now() WHERE id='audit-test';`) if err != nil { t.Fatal(err) } var actions []string if err := pool.QueryRow(ctx, `SELECT array_agg(action ORDER BY id) FROM source_configuration_events WHERE connection_id='audit-test'`).Scan(&actions); err != nil { t.Fatal(err) } store := NewStore(pool) page, err := store.ListSourceAudit(ctx, 2, 0) if err != nil { t.Fatal(err) } if len(page.Items) != 2 || page.Total < 6 || page.Items[0].Action != "restored" { t.Fatalf("bad audit page %+v", page) } next, err := store.ListSourceAudit(ctx, 2, 2) if err != nil || len(next.Items) != 2 || next.Items[0].ID == page.Items[0].ID { t.Fatalf("bad second page %+v %v", next, err) } tx, err := pool.Begin(ctx) if err != nil { t.Fatal(err) } if _, err = tx.Exec(ctx, `UPDATE source_connections SET enabled=true WHERE id='audit-test'`); err != nil { t.Fatal(err) } if err = tx.Rollback(ctx); err != nil { t.Fatal(err) } after, err := store.ListSourceAudit(ctx, 100, 0) if err != nil || after.Total != page.Total { t.Fatalf("rolled back changes must not appear %+v %v", after, err) } want := []string{"created", "paused", "resumed", "settings_updated", "archived", "restored"} if len(actions) != len(want) { t.Fatalf("audit actions=%v want=%v", actions, want) } for i := range want { if actions[i] != want[i] { t.Fatalf("actions=%v", actions) } } var wrong int if err := pool.QueryRow(ctx, `SELECT count(*) FROM source_configuration_events WHERE connection_id='audit-test' AND actor<>'tester'`).Scan(&wrong); err != nil || wrong != 0 { t.Fatalf("incorrect actors %d %v", wrong, err) } }