package store_test import ( "context" "encoding/json" "testing" "github.com/netcell-it/deklarix/internal/store" ) func openTestStore(t *testing.T) *store.Store { t.Helper() url := testDatabaseURL(t) if err := store.Migrate(url); err != nil { t.Fatalf("Migrate: %v", err) } s, err := store.Open(context.Background(), url) if err != nil { t.Fatalf("Open: %v", err) } t.Cleanup(s.Close) return s } // testAccountID legt einen Mandanten an und liefert dessen ID — jede // Submission braucht seit der Auth-Migration einen Account. func testAccountID(t *testing.T, s *store.Store) string { t.Helper() acc, err := s.CreateAccount(context.Background(), "Test-Mandant") if err != nil { t.Fatalf("CreateAccount: %v", err) } return acc.ID } func TestSubmissionCRUD(t *testing.T) { s := openTestStore(t) ctx := context.Background() accID := testAccountID(t, s) created, err := s.CreateSubmission(ctx, accID, "instagram", "reel", "Werbung fuer ein Produkt") if err != nil { t.Fatalf("CreateSubmission: %v", err) } if created.Status != "draft" { t.Fatalf("Status = %q, want draft", created.Status) } got, err := s.GetSubmission(ctx, created.ID) if err != nil { t.Fatalf("GetSubmission: %v", err) } if got.Platform != "instagram" || got.Caption != "Werbung fuer ein Produkt" { t.Fatalf("GetSubmission = %+v, unerwartete Werte", got) } if err := s.SetSubmissionStatus(ctx, created.ID, "checked"); err != nil { t.Fatalf("SetSubmissionStatus: %v", err) } got, err = s.GetSubmission(ctx, created.ID) if err != nil { t.Fatalf("GetSubmission nach Statuswechsel: %v", err) } if got.Status != "checked" { t.Fatalf("Status nach SetSubmissionStatus = %q, want checked", got.Status) } } func TestGetSubmissionNotFound(t *testing.T) { s := openTestStore(t) if _, err := s.GetSubmission(context.Background(), "00000000-0000-0000-0000-000000000000"); err == nil { t.Fatal("expected error for a nonexistent submission, got nil") } } func TestSetSubmissionStatusNotFound(t *testing.T) { s := openTestStore(t) err := s.SetSubmissionStatus(context.Background(), "00000000-0000-0000-0000-000000000000", "checked") if err == nil { t.Fatal("expected error when updating a nonexistent submission, got nil") } } func TestExtractionCRUD(t *testing.T) { s := openTestStore(t) ctx := context.Background() accID := testAccountID(t, s) sub, err := s.CreateSubmission(ctx, accID, "tiktok", "video", "...") if err != nil { t.Fatalf("CreateSubmission: %v", err) } payload := []byte(`{"gegenleistung":"bezahlt"}`) ext, err := s.CreateExtraction(ctx, sub.ID, payload, "claude-sonnet-5", "v1") if err != nil { t.Fatalf("CreateExtraction: %v", err) } // JSONB normalisiert die Textform (z. B. Leerzeichen nach ':'), der Inhalt // muss aber semantisch identisch bleiben — kein Byte-Vergleich. var gotPayload, wantPayload map[string]any if err := json.Unmarshal(ext.Payload, &gotPayload); err != nil { t.Fatalf("unmarshal stored payload: %v", err) } if err := json.Unmarshal(payload, &wantPayload); err != nil { t.Fatalf("unmarshal input payload: %v", err) } if gotPayload["gegenleistung"] != wantPayload["gegenleistung"] { t.Fatalf("stored payload = %v, want %v", gotPayload, wantPayload) } got, err := s.GetLatestExtraction(ctx, sub.ID) if err != nil { t.Fatalf("GetLatestExtraction: %v", err) } if got.ID != ext.ID { t.Fatalf("GetLatestExtraction returned a different row than CreateExtraction") } // Eine zweite Extraktion (erneute Pruefung) muss die "latest" sein. payload2 := []byte(`{"gegenleistung":"keine"}`) ext2, err := s.CreateExtraction(ctx, sub.ID, payload2, "claude-sonnet-5", "v1") if err != nil { t.Fatalf("CreateExtraction (2): %v", err) } got, err = s.GetLatestExtraction(ctx, sub.ID) if err != nil { t.Fatalf("GetLatestExtraction (2): %v", err) } if got.ID != ext2.ID { t.Fatalf("GetLatestExtraction did not return the most recent extraction") } } func TestFindingCRUDAndSupersedes(t *testing.T) { s := openTestStore(t) ctx := context.Background() accID := testAccountID(t, s) sub, err := s.CreateSubmission(ctx, accID, "instagram", "reel", "...") if err != nil { t.Fatalf("CreateSubmission: %v", err) } old, err := s.CreateFinding(ctx, sub.ID, nil, "WK-004", 1, "hoch", "alte Fassung", "alte Korrektur", []string{"§ 5a Abs. 4 UWG"}) if err != nil { t.Fatalf("CreateFinding (old): %v", err) } current, err := s.ListCurrentFindings(ctx, sub.ID) if err != nil { t.Fatalf("ListCurrentFindings: %v", err) } if len(current) != 1 || current[0].ID != old.ID { t.Fatalf("expected exactly the old finding before any correction, got %+v", current) } if len(current[0].Sources) != 1 || current[0].Sources[0] != "§ 5a Abs. 4 UWG" { t.Fatalf("Sources = %v, want [§ 5a Abs. 4 UWG]", current[0].Sources) } // Korrektur: neue Zeile, die die alte per supersedes ersetzt. _, err = s.Pool.Exec(ctx, ` INSERT INTO finding (submission_id, rule_id, rule_version, severity, title, fix, sources, supersedes) VALUES ($1, 'WK-004', 2, 'hoch', 'korrigierte Fassung', 'neue Korrektur', '{}', $2) `, sub.ID, old.ID) if err != nil { t.Fatalf("insert superseding finding: %v", err) } current, err = s.ListCurrentFindings(ctx, sub.ID) if err != nil { t.Fatalf("ListCurrentFindings nach Korrektur: %v", err) } if len(current) != 1 { t.Fatalf("expected exactly one current finding after a correction, got %d: %+v", len(current), current) } if current[0].Title != "korrigierte Fassung" { t.Fatalf("expected the corrected finding to be current, got %+v", current[0]) } } func TestEvidencePackageCRUD(t *testing.T) { s := openTestStore(t) ctx := context.Background() accID := testAccountID(t, s) sub, err := s.CreateSubmission(ctx, accID, "instagram", "reel", "...") if err != nil { t.Fatalf("CreateSubmission: %v", err) } token := []byte("fake-rfc3161-token-bytes") pkg, err := s.CreateEvidencePackage(ctx, sub.ID, "/var/lib/deklarix/dossiers/x.pdf", "deadbeef", token) if err != nil { t.Fatalf("CreateEvidencePackage: %v", err) } got, err := s.GetLatestEvidencePackage(ctx, sub.ID) if err != nil { t.Fatalf("GetLatestEvidencePackage: %v", err) } if got.ID != pkg.ID || string(got.TimestampToken) != string(token) { t.Fatalf("GetLatestEvidencePackage = %+v, unerwartete Werte", got) } // Append-only: ein UPDATE muss vom Trigger abgelehnt werden. _, err = s.Pool.Exec(ctx, `UPDATE evidence_package SET sha256 = 'geaendert' WHERE id = $1`, pkg.ID) if err == nil { t.Fatal("expected UPDATE on evidence_package to be rejected, but it succeeded") } }