Files
minstrel/internal/api/errors_test.go
T
bvandeusen 1cc7eb6cad refactor(server/api): writeErr accepts error; migrate admin handlers (1/3)
Rewrite writeErr(w, err) to wrap *apierror.Error via apierror.From,
preserving the existing {"error": {"code", "message"}} wire envelope.
Add writeErrWithLog helper for 500-class errors that need an operator
log line. Migrate all 13 admin_*.go handler files (~76 call sites) to
the new signature; T3 will sweep the remaining api package.

The old 4-arg writeErr is removed, so non-admin call sites in
internal/api will not compile until T3 lands. This is by design — T2
and T3 are paired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:48:25 -04:00

120 lines
3.2 KiB
Go

package api
import (
"encoding/json"
"errors"
"fmt"
"net/http/httptest"
"testing"
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
)
func decodeEnvelope(t *testing.T, body []byte) errorBody {
t.Helper()
var env errorBody
if err := json.Unmarshal(body, &env); err != nil {
t.Fatalf("decode envelope: %v (body=%s)", err, string(body))
}
return env
}
func TestWriteErr_APIError(t *testing.T) {
rec := httptest.NewRecorder()
writeErr(rec, apierror.NotFound("track"))
if rec.Code != 404 {
t.Fatalf("status: want 404, got %d", rec.Code)
}
env := decodeEnvelope(t, rec.Body.Bytes())
if env.Error.Code != "track_not_found" {
t.Errorf("code: want track_not_found, got %q", env.Error.Code)
}
if env.Error.Message != "track not found" {
t.Errorf("message: want %q, got %q", "track not found", env.Error.Message)
}
}
func TestWriteErr_Sentinel(t *testing.T) {
rec := httptest.NewRecorder()
writeErr(rec, apierror.ErrUnauthorized)
if rec.Code != 401 {
t.Fatalf("status: want 401, got %d", rec.Code)
}
env := decodeEnvelope(t, rec.Body.Bytes())
if env.Error.Code != "unauthorized" {
t.Errorf("code: want unauthorized, got %q", env.Error.Code)
}
if env.Error.Message != "authentication required" {
t.Errorf("message: want %q, got %q", "authentication required", env.Error.Message)
}
}
func TestWriteErr_PlainError(t *testing.T) {
rec := httptest.NewRecorder()
writeErr(rec, errors.New("boom"))
if rec.Code != 500 {
t.Fatalf("status: want 500, got %d", rec.Code)
}
env := decodeEnvelope(t, rec.Body.Bytes())
if env.Error.Code != "server_error" {
t.Errorf("code: want server_error, got %q", env.Error.Code)
}
if env.Error.Message != "internal server error" {
t.Errorf("message: want %q, got %q", "internal server error", env.Error.Message)
}
}
func TestWriteErr_WrappedAPIError(t *testing.T) {
rec := httptest.NewRecorder()
writeErr(rec, fmt.Errorf("ctx: %w", apierror.BadRequest("bad", "no good")))
if rec.Code != 400 {
t.Fatalf("status: want 400, got %d", rec.Code)
}
env := decodeEnvelope(t, rec.Body.Bytes())
if env.Error.Code != "bad" {
t.Errorf("code: want bad, got %q", env.Error.Code)
}
if env.Error.Message != "no good" {
t.Errorf("message: want %q, got %q", "no good", env.Error.Message)
}
}
func TestWriteErr_EnvelopeShape(t *testing.T) {
rec := httptest.NewRecorder()
writeErr(rec, apierror.BadRequest("x", "y"))
var raw map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &raw); err != nil {
t.Fatalf("decode raw: %v", err)
}
if len(raw) != 1 {
t.Fatalf("top-level keys: want 1, got %d (%v)", len(raw), raw)
}
inner, ok := raw["error"].(map[string]any)
if !ok {
t.Fatalf("error: want object, got %T", raw["error"])
}
if len(inner) != 2 {
t.Fatalf("inner keys: want 2, got %d (%v)", len(inner), inner)
}
if _, ok := inner["code"]; !ok {
t.Errorf("inner.code missing")
}
if _, ok := inner["message"]; !ok {
t.Errorf("inner.message missing")
}
}
func TestWriteErr_ContentType(t *testing.T) {
rec := httptest.NewRecorder()
writeErr(rec, apierror.BadRequest("x", "y"))
if got := rec.Header().Get("Content-Type"); got != "application/json" {
t.Errorf("Content-Type: want application/json, got %q", got)
}
}