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`.
305 lines
12 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|