Files
member-console/internal/lint/rawerror_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

87 lines
2.7 KiB
Go

package lint
import (
"os"
"path/filepath"
"testing"
)
// writeFixture drops src at name under dir, creating parents.
func writeFixture(t *testing.T, dir, name, src string) string {
t.Helper()
path := filepath.Join(dir, name)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
return path
}
func TestRuleRawErrorRender(t *testing.T) {
dir := t.TempDir()
leaky := writeFixture(t, dir, "handlers.go", `package server
func (h *H) bad(err error, attErr error) {
h.renderFooPage(w, r, "", "Failed to save: "+err.Error()) // line 4: flag
fireErrorToast(w, attErr.Error()) // line 5: flag (suffixed identifier)
http.Error(w, err.Error(), 500) // line 6: flag
h.Logger.Error("failed", slog.Any("error", err)) // line 7: ok — slog only
slog.Error("failed to save: " + err.Error()) // line 8: ok — slog carve-out
return fmt.Errorf("wrap: %w", err) // line 9: ok — no UI sink
msg := err.Error() // line 10: ok — no UI sink on this line
h.renderFooPage(w, r, "", "Failed to save. See the logs.") // line 11: ok — generic text only
fireSuccessToast(w, "saved "+err.Error()) // line 12: flag (success toast is a sink too)
}
`)
writeFixture(t, dir, "handlers_test.go", `package server
func TestX(t *testing.T) {
h.renderFooPage(w, r, "", "boom: "+err.Error()) // tests are exempt
}
`)
got := ruleRawErrorRender(dir)
wantLines := map[int]bool{4: true, 5: true, 6: true, 12: true}
if len(got) != len(wantLines) {
t.Fatalf("got %d violations, want %d: %+v", len(got), len(wantLines), got)
}
for _, viol := range got {
if viol.File != leaky {
t.Errorf("violation in %s, want only %s", viol.File, leaky)
}
if viol.Rule != "raw-error-render" {
t.Errorf("rule = %q, want raw-error-render", viol.Rule)
}
if !wantLines[viol.Line] {
t.Errorf("unexpected violation at line %d", viol.Line)
}
delete(wantLines, viol.Line)
}
for line := range wantLines {
t.Errorf("missing violation at line %d", line)
}
}
func TestRuleRawErrorRender_CleanDir(t *testing.T) {
dir := t.TempDir()
writeFixture(t, dir, "clean.go", `package server
func (h *H) good(err error) {
if fe, ok := web.FieldErrorsFromDB(err, nil); ok {
h.renderFooFormErrors(w, r, fe)
return
}
h.Logger.Error("failed to save", slog.Any("error", err))
h.renderFooPage(w, r, "", "Failed to save. Details are in the server logs.")
}
`)
if got := ruleRawErrorRender(dir); len(got) != 0 {
t.Fatalf("got %d violations on clean fixture, want 0: %+v", len(got), got)
}
}