feat: add participant CRUD and per-account submission listing
Participant (the Verantwortungsmatrix — who briefed, who approved) is not append-only like finding/extraction/evidence_package; getting a role wrong and correcting it isn't rewriting evidence, so full CRUD is legitimate here: Create/List/Get/Update/Delete. UpdateParticipant sets approved_at the first time freigegeben flips to true and never moves it again on subsequent no-op updates — it marks when approval first happened, not "last touched". ListSubmissionsForAccount is the query the upcoming archive overview needs: every submission for a tenant plus a findings count and highest severity, computed with the same anti-join ListCurrentFindings already uses for "currently valid" findings. Also fixed a real bug this surfaced: CreateFinding let a nil Sources slice reach a NOT NULL TEXT[] column, which Postgres rejects with an unhelpful constraint error instead of a clear message. It now normalizes nil to an empty slice before inserting — matters for any future rule that ships without a fundstelle entry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
133
internal/store/participant_test.go
Normal file
133
internal/store/participant_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/netcell-it/deklarix/internal/store"
|
||||
)
|
||||
|
||||
func TestParticipantCRUD(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)
|
||||
}
|
||||
|
||||
p, err := s.CreateParticipant(ctx, sub.ID, "creator", "Max Mustermann", false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateParticipant: %v", err)
|
||||
}
|
||||
if p.ApprovedAt != nil {
|
||||
t.Fatalf("ApprovedAt should be nil when freigegeben=false, got %v", p.ApprovedAt)
|
||||
}
|
||||
|
||||
list, err := s.ListParticipants(ctx, sub.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListParticipants: %v", err)
|
||||
}
|
||||
if len(list) != 1 || list[0].ID != p.ID {
|
||||
t.Fatalf("ListParticipants = %+v, want exactly the created participant", list)
|
||||
}
|
||||
|
||||
got, err := s.GetParticipant(ctx, p.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetParticipant: %v", err)
|
||||
}
|
||||
if got.SubmissionID != sub.ID {
|
||||
t.Fatalf("GetParticipant.SubmissionID = %q, want %q", got.SubmissionID, sub.ID)
|
||||
}
|
||||
|
||||
updated, err := s.UpdateParticipant(ctx, p.ID, true, true)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateParticipant: %v", err)
|
||||
}
|
||||
if !updated.Vorgegeben || !updated.Freigegeben {
|
||||
t.Fatalf("UpdateParticipant did not apply new flags: %+v", updated)
|
||||
}
|
||||
if updated.ApprovedAt == nil {
|
||||
t.Fatal("expected ApprovedAt to be set once freigegeben became true")
|
||||
}
|
||||
firstApproval := *updated.ApprovedAt
|
||||
|
||||
// Ein erneutes Update (weiterhin freigegeben) darf approved_at nicht
|
||||
// verschieben — es haelt fest, wann ZUERST freigegeben wurde.
|
||||
updated2, err := s.UpdateParticipant(ctx, p.ID, true, true)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateParticipant (2): %v", err)
|
||||
}
|
||||
if !updated2.ApprovedAt.Equal(firstApproval) {
|
||||
t.Fatalf("ApprovedAt changed on a no-op update: %v -> %v", firstApproval, *updated2.ApprovedAt)
|
||||
}
|
||||
|
||||
if err := s.DeleteParticipant(ctx, p.ID); err != nil {
|
||||
t.Fatalf("DeleteParticipant: %v", err)
|
||||
}
|
||||
if _, err := s.GetParticipant(ctx, p.ID); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("err after delete = %v, want store.ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateParticipantNotFound(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
_, err := s.UpdateParticipant(context.Background(), "00000000-0000-0000-0000-000000000000", true, true)
|
||||
if !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("err = %v, want store.ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteParticipantNotFound(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
err := s.DeleteParticipant(context.Background(), "00000000-0000-0000-0000-000000000000")
|
||||
if !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("err = %v, want store.ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSubmissionsForAccount(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
ctx := context.Background()
|
||||
accID := testAccountID(t, s)
|
||||
|
||||
subNoFindings, err := s.CreateSubmission(ctx, accID, "instagram", "reel", "organic")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSubmission: %v", err)
|
||||
}
|
||||
subWithFinding, err := s.CreateSubmission(ctx, accID, "tiktok", "video", "unmarked ad")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSubmission: %v", err)
|
||||
}
|
||||
// sources bewusst nil statt []string{} — CreateFinding muss das
|
||||
// selbst abfangen (siehe Kommentar dort), nicht der Aufrufer.
|
||||
if _, err := s.CreateFinding(ctx, subWithFinding.ID, nil, "WK-001", 1, "hoch", "t", "f", nil); err != nil {
|
||||
t.Fatalf("CreateFinding: %v", err)
|
||||
}
|
||||
|
||||
// Andere Mandanten duerfen nicht auftauchen.
|
||||
otherAccID := testAccountID(t, s)
|
||||
if _, err := s.CreateSubmission(ctx, otherAccID, "instagram", "reel", "anderer Mandant"); err != nil {
|
||||
t.Fatalf("CreateSubmission (other account): %v", err)
|
||||
}
|
||||
|
||||
list, err := s.ListSubmissionsForAccount(ctx, accID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSubmissionsForAccount: %v", err)
|
||||
}
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("expected 2 submissions for this account, got %d: %+v", len(list), list)
|
||||
}
|
||||
|
||||
byID := map[string]store.SubmissionSummary{}
|
||||
for _, s := range list {
|
||||
byID[s.ID] = s
|
||||
}
|
||||
if byID[subNoFindings.ID].FindingCount != 0 || byID[subNoFindings.ID].HighestSeverity != "" {
|
||||
t.Errorf("subNoFindings summary = %+v, want 0 findings and no severity", byID[subNoFindings.ID])
|
||||
}
|
||||
if byID[subWithFinding.ID].FindingCount != 1 || byID[subWithFinding.ID].HighestSeverity != "hoch" {
|
||||
t.Errorf("subWithFinding summary = %+v, want 1 finding, severity hoch", byID[subWithFinding.ID])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user