Files
paliad/internal/services/projection_counterclaim_test.go
mAi 216abbfc98 feat(t-paliad-206): switch Go layer to lowercase dot-form proceeding codes
Sweeps internal/services + internal/handlers + internal/models to use
the new proceeding codes landed by mig 096. Stable Code* constants
live in internal/services/proceeding_mapping.go so a future rename
needs to touch one file.

Substantive changes:
- proceeding_mapping.go gains ResolveCounterclaimRouting() — the
  cascade resolver that routes upc.ccr.cfi (illustrative peer) back
  to upc.inf.cfi with with_ccr=true as default flag (design doc S1).
- deadline_search_service.go forum-bucket map updated; upc.ccr.cfi
  added to upc_cfi since it is a CFI peer.
- project_service.go CreateCounterclaim default lookup parameterised
  so the SQL string carries the constant, not a literal.
- proceeding_codes_shape_test.go: new file. Validates the shape
  regex standalone (always runs) and walks live DB rows asserting
  every active fristenrechner row matches the new shape + every
  stable Code* constant resolves to exactly one active row.

Comments and test fixtures throughout the Go tree updated to the
new shape. Tests pass under `go test ./internal/... -short`.
2026-05-18 12:13:24 +02:00

305 lines
12 KiB
Go

package services
// Live-DB integration test for the counterclaim sub-project shape
// (t-paliad-174 SmartTimeline Slice 3). Skipped without TEST_DATABASE_URL,
// matching the convention of the other live tests in this package.
//
// The test exercises the end-to-end shape:
// 1. CreateCounterclaim atomically creates child + flips our_side +
// writes audit rows on parent AND child + sets counterclaim_of.
// 2. parent_id of the child equals parent's parent_id (sibling-under-
// patent placement).
// 3. ProjectionService.For on the parent surfaces a parallel-track
// counterclaim event; AvailableTracks lists the new track.
// 4. ProjectionService.For on the child surfaces the parent's events
// with track="parent_context:<parent_id>".
// 5. Two-level CCR chains are rejected at the schema level.
import (
"context"
"errors"
"os"
"strings"
"testing"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"mgit.msbls.de/m/paliad/internal/db"
)
func TestCreateCounterclaim_Live(t *testing.T) {
url := os.Getenv("TEST_DATABASE_URL")
if url == "" {
t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
}
if err := db.ApplyMigrations(url); err != nil {
t.Fatalf("apply migrations: %v", err)
}
pool, err := sqlx.Connect("postgres", url)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer pool.Close()
ctx := context.Background()
userID := uuid.New()
patentID := uuid.New() // sibling parent: the patent hub
caseID := uuid.New() // the parent case (upc.inf.cfi)
// Resolve upc.inf.cfi + upc.rev.cfi ids once. We need real ids from
// the proceeding_types seed because they're NOT NULL on the test row.
var upcInf, upcRev int
if err := pool.GetContext(ctx, &upcInf,
`SELECT id FROM paliad.proceeding_types WHERE code = $1`,
CodeUPCInfringement); err != nil {
t.Fatalf("resolve %s: %v", CodeUPCInfringement, err)
}
if err := pool.GetContext(ctx, &upcRev,
`SELECT id FROM paliad.proceeding_types WHERE code = $1`,
CodeUPCRevocation); err != nil {
t.Fatalf("resolve %s: %v", CodeUPCRevocation, err)
}
cleanup := func() {
// Delete CCR children first (FK to caseID via counterclaim_of is
// ON DELETE SET NULL but the child rows still hold parent_id =
// patentID — clear them via a parent_id sweep).
pool.ExecContext(ctx, `DELETE FROM paliad.project_events WHERE project_id IN (
SELECT id FROM paliad.projects WHERE counterclaim_of = $1)`, caseID)
pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id IN (
SELECT id FROM paliad.projects WHERE counterclaim_of = $1)`, caseID)
pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE counterclaim_of = $1`, caseID)
pool.ExecContext(ctx, `DELETE FROM paliad.project_events WHERE project_id IN ($1, $2)`, caseID, patentID)
pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id IN ($1, $2)`, caseID, patentID)
pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE id IN ($1, $2)`, caseID, patentID)
pool.ExecContext(ctx, `DELETE FROM paliad.users WHERE id = $1`, userID)
pool.ExecContext(ctx, `DELETE FROM auth.users WHERE id = $1`, userID)
}
cleanup()
defer cleanup()
if _, err := pool.ExecContext(ctx,
`INSERT INTO auth.users (id, email) VALUES ($1, 'ccr-test@hlc.com')`,
userID); err != nil {
t.Fatalf("seed auth.users: %v", err)
}
if _, err := pool.ExecContext(ctx,
`INSERT INTO paliad.users (id, email, display_name, office, global_role, lang)
VALUES ($1, 'ccr-test@hlc.com', 'CCR Test', 'munich', 'global_admin', 'de')`,
userID); err != nil {
t.Fatalf("seed paliad.users: %v", err)
}
// Parent patent hub.
if _, err := pool.ExecContext(ctx,
`INSERT INTO paliad.projects (id, type, path, title, patent_number, status, created_by)
VALUES ($1, 'patent', $1::text, 'EP3456789 — Test Patent', 'EP3456789', 'active', $2)`,
patentID, userID); err != nil {
t.Fatalf("seed patent: %v", err)
}
if _, err := pool.ExecContext(ctx,
`INSERT INTO paliad.project_teams (project_id, user_id, role, responsibility, inherited, added_by)
VALUES ($1, $2, 'lead', 'lead', false, $2)`,
patentID, userID); err != nil {
t.Fatalf("seed patent team: %v", err)
}
// Child case (upc.inf.cfi) under the patent.
if _, err := pool.ExecContext(ctx,
`INSERT INTO paliad.projects
(id, type, parent_id, path, title, status, created_by,
proceeding_type_id, our_side)
VALUES ($1, 'case', $2, $2::text || '.' || $1::text,
'UPC-CFI München — Klage', 'active', $3, $4, 'claimant')`,
caseID, patentID, userID, upcInf); err != nil {
t.Fatalf("seed case: %v", err)
}
if _, err := pool.ExecContext(ctx,
`INSERT INTO paliad.project_teams (project_id, user_id, role, responsibility, inherited, added_by)
VALUES ($1, $2, 'lead', 'lead', false, $2)`,
caseID, userID); err != nil {
t.Fatalf("seed case team: %v", err)
}
users := NewUserService(pool)
projects := NewProjectService(pool, users)
eventTypes := NewEventTypeService(pool, users)
deadlines := NewDeadlineService(pool, projects, eventTypes)
appointments := NewAppointmentService(pool, projects)
rules := NewDeadlineRuleService(pool)
holidays := NewHolidayService(pool)
courts := NewCourtService(pool)
fristen := NewFristenrechnerService(rules, holidays, courts)
projection := NewProjectionService(pool, projects, deadlines, appointments, fristen, rules)
t.Run("CreateCounterclaim flips our_side, places sibling, audits both", func(t *testing.T) {
child, err := projects.CreateCounterclaim(ctx, userID, caseID, CounterclaimOpts{})
if err != nil {
t.Fatalf("CreateCounterclaim: %v", err)
}
defer pool.ExecContext(ctx, `DELETE FROM paliad.project_events WHERE project_id IN ($1, $2)`, child.ID, caseID)
defer pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id = $1`, child.ID)
defer pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE id = $1`, child.ID)
// 1. counterclaim_of points at the parent.
if child.CounterclaimOf == nil || *child.CounterclaimOf != caseID {
t.Errorf("child.CounterclaimOf = %v, want %v", child.CounterclaimOf, caseID)
}
// 2. parent_id = parent's parent_id = patent hub (sibling-under-patent).
if child.ParentID == nil || *child.ParentID != patentID {
t.Errorf("child.ParentID = %v, want %v (sibling under patent)", child.ParentID, patentID)
}
// 3. our_side flipped: parent claimant → child defendant.
if child.OurSide == nil || *child.OurSide != "defendant" {
t.Errorf("child.OurSide = %v, want defendant", child.OurSide)
}
// 4. Default proceeding_type_id resolved to upc.rev.cfi.
if child.ProceedingTypeID == nil || *child.ProceedingTypeID != upcRev {
t.Errorf("child.ProceedingTypeID = %v, want upc.rev.cfi (%d)", child.ProceedingTypeID, upcRev)
}
// 5. Auto-suggested title carries the patent reference + suffix.
if !strings.Contains(child.Title, "EP3456789") || !strings.Contains(child.Title, "Widerklage") {
t.Errorf("child.Title = %q, want it to contain EP3456789 and Widerklage", child.Title)
}
// 6. Audit rows on BOTH parent and child with timeline_kind='milestone'.
var parentAudit, childAudit int
if err := pool.GetContext(ctx, &parentAudit,
`SELECT count(*) FROM paliad.project_events
WHERE project_id = $1 AND event_type = 'counterclaim_created'
AND timeline_kind = 'milestone'`, caseID); err != nil {
t.Fatalf("count parent audit: %v", err)
}
if parentAudit != 1 {
t.Errorf("parent counterclaim_created rows = %d, want 1", parentAudit)
}
if err := pool.GetContext(ctx, &childAudit,
`SELECT count(*) FROM paliad.project_events
WHERE project_id = $1 AND event_type = 'counterclaim_created'
AND timeline_kind = 'milestone'`, child.ID); err != nil {
t.Fatalf("count child audit: %v", err)
}
if childAudit != 1 {
t.Errorf("child counterclaim_created rows = %d, want 1", childAudit)
}
})
t.Run("ProjectionService.For on parent surfaces counterclaim track", func(t *testing.T) {
child, err := projects.CreateCounterclaim(ctx, userID, caseID, CounterclaimOpts{})
if err != nil {
t.Fatalf("CreateCounterclaim: %v", err)
}
defer pool.ExecContext(ctx, `DELETE FROM paliad.project_events WHERE project_id IN ($1, $2)`, child.ID, caseID)
defer pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id = $1`, child.ID)
defer pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE id = $1`, child.ID)
rows, meta, err := projection.For(ctx, userID, caseID, ProjectionOpts{})
if err != nil {
t.Fatalf("projection.For parent: %v", err)
}
// AvailableTracks contains parent + the new counterclaim track.
expectTrack := "counterclaim:" + child.ID.String()
var sawCounterclaimTrack bool
for _, t := range meta.AvailableTracks {
if t == expectTrack {
sawCounterclaimTrack = true
break
}
}
if !sawCounterclaimTrack {
t.Errorf("AvailableTracks = %v, want to contain %q", meta.AvailableTracks, expectTrack)
}
// At least one row carries the counterclaim track + the
// SubProjectID = child.ID.
var countCCR int
for _, r := range rows {
if r.Track == expectTrack {
countCCR++
if r.SubProjectID == nil || *r.SubProjectID != child.ID {
t.Errorf("ccr-track row missing SubProjectID = child.ID")
}
}
}
if countCCR == 0 {
t.Errorf("expected at least one row on counterclaim track, saw 0 (rows=%d)", len(rows))
}
})
t.Run("ProjectionService.For on child surfaces parent_context track", func(t *testing.T) {
child, err := projects.CreateCounterclaim(ctx, userID, caseID, CounterclaimOpts{})
if err != nil {
t.Fatalf("CreateCounterclaim: %v", err)
}
defer pool.ExecContext(ctx, `DELETE FROM paliad.project_events WHERE project_id IN ($1, $2)`, child.ID, caseID)
defer pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id = $1`, child.ID)
defer pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE id = $1`, child.ID)
rows, meta, err := projection.For(ctx, userID, child.ID, ProjectionOpts{})
if err != nil {
t.Fatalf("projection.For child: %v", err)
}
expectTrack := "parent_context:" + caseID.String()
var sawParentContext bool
for _, t := range meta.AvailableTracks {
if t == expectTrack {
sawParentContext = true
break
}
}
if !sawParentContext {
t.Errorf("AvailableTracks = %v, want to contain %q", meta.AvailableTracks, expectTrack)
}
var countParentCtx int
for _, r := range rows {
if r.Track == expectTrack {
countParentCtx++
if r.SubProjectID == nil || *r.SubProjectID != caseID {
t.Errorf("parent_context row missing SubProjectID = parent.ID")
}
}
}
if countParentCtx == 0 {
t.Errorf("expected at least one parent_context row, saw 0 (rows=%d)", len(rows))
}
})
t.Run("Two-level CCR chains are rejected at the schema level", func(t *testing.T) {
child, err := projects.CreateCounterclaim(ctx, userID, caseID, CounterclaimOpts{})
if err != nil {
t.Fatalf("CreateCounterclaim: %v", err)
}
defer pool.ExecContext(ctx, `DELETE FROM paliad.project_events WHERE project_id IN ($1, $2)`, child.ID, caseID)
defer pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id = $1`, child.ID)
defer pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE id = $1`, child.ID)
// Trying to create a CCR against the CCR child = two-level chain.
// CreateCounterclaim guards with an early ErrInvalidInput before
// hitting the trigger; verify the early guard fires.
_, err = projects.CreateCounterclaim(ctx, userID, child.ID, CounterclaimOpts{})
if err == nil {
t.Fatal("expected error for two-level CCR chain, got nil")
}
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("expected ErrInvalidInput, got %v", err)
}
// Also pin the schema-level trigger guard: a direct INSERT
// pointing at a row that already has counterclaim_of NOT NULL
// must be rejected by paliad.projects_no_two_level_ccr.
grandchild := uuid.New()
_, err = pool.ExecContext(ctx,
`INSERT INTO paliad.projects
(id, type, parent_id, path, title, status, created_by, counterclaim_of)
VALUES ($1, 'case', $2, $1::text, 'Grandchild CCR', 'active', $3, $4)`,
grandchild, patentID, userID, child.ID)
if err == nil {
pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE id = $1`, grandchild)
t.Fatal("expected schema trigger to reject grandchild CCR insert, got success")
}
if !strings.Contains(err.Error(), "two-level counterclaim") {
t.Errorf("trigger error message: %v (want two-level counterclaim)", err)
}
})
}