Files
member-console/internal/web/dberrors_test.go
T
cgalo5758 c71115b51a db-error-translation: field error translator, 9 leak site fixes, lint enforcement
Add web.FieldErrorsFromDB — a constraint-name-to-field translator that converts
PostgreSQL constraint violations into 422 FieldErrors (per the §6 error contract)
instead of leaking raw driver text to users.

- New web/dberrors.go: FieldErrorsFromDB + ConstraintMessages map
- New web/dberrors_test.go: constraint hits, per-class fallbacks (23505/23503/23514/22001),
  nil map, non-Postgres, 23P01 exclusion requires named entry
- Fix 9 leak sites: workspace creation, enrollment (IssueGrant/ExtendGrant/
  CreateNonPlanGrant/RevokeGrantAndTransition), org type default-plan update,
  entitlement set CRUD, product create/update
- New lint rule raw-error-render: flags err.Error() on any line touching a UI sink
  (render*, fireErrorToast, fireSuccessToast, http.Error), exempts slog lines
- Docs: add §4 db-write-failure contract and §9 anti-pattern row

Closes: openspec/changes/db-error-translation
2026-07-02 16:19:03 -05:00

121 lines
4.0 KiB
Go

package web
import (
"errors"
"fmt"
"testing"
"github.com/jackc/pgx/v5/pgconn"
)
func TestFieldErrorsFromDB_ConstraintNameHit(t *testing.T) {
msgs := ConstraintMessages{
"workspaces_org_id_slug_key": {Field: "slug", Message: "A workspace with this slug already exists in this organization."},
}
// Wrapped, as sqlc/database/sql returns it in practice.
err := fmt.Errorf("exec insert: %w", &pgconn.PgError{
Code: "23505",
ConstraintName: "workspaces_org_id_slug_key",
Message: `duplicate key value violates unique constraint "workspaces_org_id_slug_key"`,
})
fe, ok := FieldErrorsFromDB(err, msgs)
if !ok {
t.Fatalf("FieldErrorsFromDB ok = false, want true")
}
if got := fe.Get("slug"); got != "A workspace with this slug already exists in this organization." {
t.Errorf("fe.Get(slug) = %q, want the mapped message", got)
}
if len(fe) != 1 {
t.Errorf("len(fe) = %d, want exactly one entry", len(fe))
}
// The named match must win over the per-class fallback.
if fe.Has("") {
t.Errorf("form-level fallback populated alongside a named constraint hit: %q", fe.Get(""))
}
}
func TestFieldErrorsFromDB_ClassFallback(t *testing.T) {
cases := []struct {
code string
want string
}{
{"23505", "A record with these values already exists."},
{"23503", "A referenced record no longer exists or is still in use."},
{"23514", "This combination of values is not allowed."},
{"22001", "One of the values is too long."},
}
for _, tc := range cases {
t.Run(tc.code, func(t *testing.T) {
err := &pgconn.PgError{Code: tc.code, ConstraintName: "not_in_the_map"}
fe, ok := FieldErrorsFromDB(err, ConstraintMessages{
"some_other_constraint": {Field: "name", Message: "unrelated"},
})
if !ok {
t.Fatalf("ok = false, want true for SQLSTATE %s", tc.code)
}
if got := fe.Get(""); got != tc.want {
t.Errorf("fe.Get(\"\") = %q, want %q", got, tc.want)
}
if fe.Has("name") {
t.Errorf("unrelated named mapping leaked into the result")
}
})
}
}
func TestFieldErrorsFromDB_NilMapStillFallsBack(t *testing.T) {
fe, ok := FieldErrorsFromDB(&pgconn.PgError{Code: "23503"}, nil)
if !ok {
t.Fatalf("ok = false, want true — nil ConstraintMessages must still allow class fallback")
}
if fe.Get("") == "" {
t.Errorf("expected a form-level fallback message, got none")
}
}
func TestFieldErrorsFromDB_NotOK(t *testing.T) {
cases := []struct {
name string
err error
}{
{"non-pg error", errors.New("dial tcp 127.0.0.1:5432: connection refused")},
{"wrapped non-pg error", fmt.Errorf("query: %w", errors.New("context canceled"))},
{"pg error with unrecognized class", &pgconn.PgError{Code: "42703", Message: `column "nope" does not exist`}},
{"nil-ish sql error text mentioning SQLSTATE", errors.New("ERROR: something (SQLSTATE 23505)")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fe, ok := FieldErrorsFromDB(tc.err, ConstraintMessages{
"chk_grants_recipient": {Field: "", Message: "A grant must name exactly one recipient."},
})
if ok {
t.Fatalf("ok = true, want false (fe=%v) — caller must slog + render a generic message", fe)
}
if fe != nil {
t.Errorf("fe = %v, want nil on ok=false", fe)
}
})
}
}
func TestFieldErrorsFromDB_ExclusionConstraintNeedsNamedEntry(t *testing.T) {
// SQLSTATE 23P01 (exclusion_violation) has no per-class fallback: without
// a named entry it must return ok=false, and with one the name must win.
exclErr := &pgconn.PgError{Code: "23P01", ConstraintName: "excl_pool_provision_ladders_active_overlap"}
if _, ok := FieldErrorsFromDB(exclErr, nil); ok {
t.Fatalf("ok = true for unmapped 23P01, want false")
}
fe, ok := FieldErrorsFromDB(exclErr, ConstraintMessages{
"excl_pool_provision_ladders_active_overlap": {Field: "product_id", Message: "This pool already has an active plan on that ladder."},
})
if !ok {
t.Fatalf("ok = false for mapped 23P01, want true")
}
if got := fe.Get("product_id"); got != "This pool already has an active plan on that ladder." {
t.Errorf("fe.Get(product_id) = %q, want the mapped message", got)
}
}