Mig 098 (t-paliad-209, ohm) crash-looped paliad.de prod for ~2h: §6.1 assertion regex `^[a-z_]+\.[a-z_]+\.[a-z_]+\.[a-z_]+(\..*)?$` rejects EPA rule codes that carry the statutory rule number in the suffix — e.g. `epa.opp.boa.r106`, `epa.grant.exa.r71_3`, `epa.opp.opd.r116`, `epa.opp.opd.r79_further`, `epa.opp.boa.entsch2`, `epa.opp.boa.r116`. Migration's UPDATE step succeeds against these rows; the transactional assertion blows them up; rollback leaves the migration tracker dirty at version 98 and the container refuses to start. Patch: allow `[a-z_0-9]` per segment instead of `[a-z_]` in both the SQL assertion (mig 098 §6.1) and the matching Go shape regex (submission_codes_shape_test.go). Same change in both spots so the runtime sanity test stays aligned with the SQL invariant. Manual recovery already applied: forced `paliad.paliad_schema_migrations.version` back to 97 with `dirty=false` so the next deploy retries mig 098 from scratch against the patched file. No data state changed (mig 098 ran inside a transaction and fully rolled back — snapshot table, prefix UPDATE, and column rename all reverted). go build ./... clean. TestProceedingCodeShapeRegexStandalone green.
119 lines
3.9 KiB
Go
119 lines
3.9 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"regexp"
|
|
"testing"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/lib/pq"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/db"
|
|
)
|
|
|
|
// submissionCodeShapeRegex is the proceeding-code-prefixed shape
|
|
// installed by mig 098 (t-paliad-209): the proceeding's 3-segment code
|
|
// (`^[a-z_0-9]+\.[a-z_0-9]+\.[a-z_0-9]+\.`) followed by at least one
|
|
// suffix segment (and optional further dot-separated segments). The
|
|
// regex allows digits so EPA suffixes like `r106` / `r71_3` / `r116`
|
|
// (statutory rule numbers in the suffix) pass alongside canonical
|
|
// dotted-word codes. Underscores cover the legacy archived bucket
|
|
// (`_archived_…`) and hand-seeded test rules. Mirrors the assertion in
|
|
// mig 098 §6.1.
|
|
var submissionCodeShapeRegex = regexp.MustCompile(
|
|
`^[a-z_0-9]+\.[a-z_0-9]+\.[a-z_0-9]+\.[a-z_0-9]+(\..*)?$`)
|
|
|
|
// TestSubmissionCodeShape walks every active+published row in
|
|
// paliad.deadline_rules and asserts that submission_code matches the
|
|
// 4+-segment proceeding-code-prefixed shape ratified for t-paliad-209.
|
|
// Sibling of TestProceedingCodeShape — same pattern, same goal: catch
|
|
// drift between the migration's hard invariant and runtime state.
|
|
//
|
|
// Archived rows (proceeding `_archived_litigation`) are exempted; mig
|
|
// 098's §6.1 assertion does the same by gating on lifecycle_state =
|
|
// 'published'. Their codes get the archived prefix and the wider shape
|
|
// they end up with sits outside the 4+-segment canonical form by
|
|
// design.
|
|
//
|
|
// Skipped when TEST_DATABASE_URL is unset, mirroring the pattern in
|
|
// proceeding_codes_shape_test.go.
|
|
func TestSubmissionCodeShape(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()
|
|
|
|
var rows []struct {
|
|
ID string `db:"id"`
|
|
SubmissionCode *string `db:"submission_code"`
|
|
}
|
|
if err := pool.SelectContext(ctx, &rows,
|
|
`SELECT dr.id::text AS id, dr.submission_code
|
|
FROM paliad.deadline_rules dr
|
|
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
|
|
WHERE dr.is_active = true
|
|
AND dr.lifecycle_state = 'published'
|
|
AND pt.category = 'fristenrechner'
|
|
ORDER BY dr.id`); err != nil {
|
|
t.Fatalf("load active+published deadline_rules rows: %v", err)
|
|
}
|
|
if len(rows) == 0 {
|
|
t.Fatal("no active+published fristenrechner deadline_rules — mig 098 likely not applied")
|
|
}
|
|
for _, r := range rows {
|
|
if r.SubmissionCode == nil {
|
|
t.Errorf("deadline_rules[id=%s] submission_code is NULL", r.ID)
|
|
continue
|
|
}
|
|
if !submissionCodeShapeRegex.MatchString(*r.SubmissionCode) {
|
|
t.Errorf("deadline_rules[id=%s] submission_code=%q does not match shape %s",
|
|
r.ID, *r.SubmissionCode, submissionCodeShapeRegex.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSubmissionCodeShapeRegexStandalone exercises the regex without a
|
|
// DB so the shape rule is verified on every `go test ./...` run.
|
|
func TestSubmissionCodeShapeRegexStandalone(t *testing.T) {
|
|
good := []string{
|
|
"upc.inf.cfi.soc",
|
|
"upc.inf.cfi.sod",
|
|
"upc.inf.cfi.def_to_ccr",
|
|
"upc.rev.cfi.app",
|
|
"de.inf.lg.klage",
|
|
"de.inf.bgh.revision",
|
|
"de.null.bgh.berufung",
|
|
"dpma.appeal.bpatg.begruendung",
|
|
"epa.opp.opd.beschwerde_begr",
|
|
}
|
|
for _, code := range good {
|
|
if !submissionCodeShapeRegex.MatchString(code) {
|
|
t.Errorf("good code %q rejected by submission-code shape regex", code)
|
|
}
|
|
}
|
|
bad := []string{
|
|
"inf.soc", // pre-mig-098: 2 segments
|
|
"upc.inf", // 2 segments
|
|
"upc.inf.cfi", // proceeding code shape, not a submission code
|
|
"UPC.INF.CFI.SOC", // uppercase
|
|
"upc-inf-cfi-soc", // dashes
|
|
"",
|
|
}
|
|
for _, code := range bad {
|
|
if submissionCodeShapeRegex.MatchString(code) {
|
|
t.Errorf("bad code %q accepted by submission-code shape regex", code)
|
|
}
|
|
}
|
|
}
|