Migration 0002 replaces finding.message with title/fix/sources (a Postgres text[]). A finding needs to render into the dossier the way it looked at the moment it was raised — referencing the current rules/*.yaml by rule_id+version isn't safe once that file is edited for a later version, since old wording isn't kept around as a separate live file. Added as a new migration rather than editing 0001, since that's already applied on the test server. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
197 lines
6.0 KiB
Go
197 lines
6.0 KiB
Go
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
|
|
}
|
|
|
|
func TestSubmissionCRUD(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
created, err := s.CreateSubmission(ctx, "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()
|
|
|
|
sub, err := s.CreateSubmission(ctx, "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()
|
|
|
|
sub, err := s.CreateSubmission(ctx, "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()
|
|
|
|
sub, err := s.CreateSubmission(ctx, "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")
|
|
}
|
|
}
|