package storage import ( "context" "os" "testing" "time" ) // TestCSPReportsIntegration is skipped unless OTEL_CLICKHOUSE_TEST_ADDR // points at a disposable ClickHouse (native port 9000). It exercises the // real SQL: schema creation, typed insert and the view's queries. func TestCSPReportsIntegration(t *testing.T) { addr := os.Getenv("OTEL_CLICKHOUSE_TEST_ADDR") if addr == "" { t.Skip("set OTEL_CLICKHOUSE_TEST_ADDR=host:9000 (with a disposable DB) to run") } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() db := "otel_csp_test_" + time.Now().UTC().Format("150405") s, err := NewClickHouseStore(ctx, ClickHouseConfig{Addr: addr, Database: db, Username: os.Getenv("OTEL_CLICKHOUSE_TEST_USER"), Password: os.Getenv("OTEL_CLICKHOUSE_TEST_PASSWORD")}) if err != nil { t.Skipf("ClickHouse not reachable at %s: %v", addr, err) } defer func() { _ = s.conn.Exec(ctx, "DROP DATABASE IF EXISTS "+db) _ = s.Close() }() now := time.Now().UTC().Truncate(time.Millisecond) reports := []CSPReport{ {ID: "r1", Timestamp: now, Site: "app.k6n.net", DocumentURL: "https://app.k6n.net/p", BlockedURL: "https://ads.example/x.js", EffectiveDirective: "script-src", Disposition: "enforce", StatusCode: 200, Sample: "evil()", ReportType: "csp-report", UserAgent: "UA/1", SourceIP: "203.0.113.1"}, {ID: "r2", Timestamp: now.Add(-time.Minute), Site: "docs.tornberg.me", DocumentURL: "https://docs.tornberg.me/", BlockedURL: "inline", EffectiveDirective: "style-src", Disposition: "report", ReportType: "csp-violation"}, } for _, r := range reports { if err := s.AppendCSPReport(ctx, r); err != nil { t.Fatalf("append: %v", err) } } got, err := s.CSPReports(ctx, CSPReportsQuery{Limit: 10}) if err != nil { t.Fatalf("list: %v", err) } if len(got) != 2 || got[0].ID != "r1" { t.Fatalf("list: got %+v", got) } if got[0].Site != "app.k6n.net" || got[0].StatusCode != 200 || got[0].Sample != "evil()" || got[0].Timestamp != now { t.Errorf("round-trip mismatch: %+v", got[0]) } if got, err = s.CSPReports(ctx, CSPReportsQuery{Site: "docs.tornberg.me"}); err != nil || len(got) != 1 { t.Errorf("site filter: %v %+v", err, got) } if got, err = s.CSPReports(ctx, CSPReportsQuery{Directive: "style-src"}); err != nil || len(got) != 1 || got[0].ID != "r2" { t.Errorf("directive filter: %v %+v", err, got) } if got, err = s.CSPReports(ctx, CSPReportsQuery{Search: "ads.example"}); err != nil || len(got) != 1 { t.Errorf("search filter: %v %+v", err, got) } sum, err := s.CSPSummary(ctx, 24) if err != nil { t.Fatalf("summary: %v", err) } if sum.Total != 2 || sum.Sites != 2 { t.Errorf("summary: %+v", sum) } if len(sum.ByDirective) != 2 { t.Errorf("summary directives: %+v", sum.ByDirective) } if len(sum.ByBlockedURL) != 2 || sum.ByBlockedURL[0].Name != "ads.example" { t.Errorf("summary blocked hosts: %+v", sum.ByBlockedURL) } if len(sum.BySite) != 2 || sum.BySite[0].Count != 1 { t.Errorf("summary sites: %+v", sum.BySite) } }