Files
minstrel/internal/apierror/apierror_test.go
T
bvandeusen 58766dbebe fix(server/api): preserve 500-class messages via apierror.InternalMsg
The PR1-T2 admin handler migration replaced bespoke 500-class messages
("lookup failed", "refetch failed", "trigger failed", "bump failed",
"remove failed", "update failed", "schedule row missing", "re-read
failed", "read failed") with apierror.Internal(err), which forces the
wire Message to "internal server error". That violates the PR1
contract that errEnvelope{Code, Message} must remain byte-identical
for existing clients.

Add apierror.InternalMsg(message, cause) — a 500-class constructor
that preserves the call site's user-facing message while keeping the
cause attached for logging and errors.Is. Re-migrate the 12 admin
sites whose pre-1cc7eb6 source had a non-empty bespoke Message so
they restore the original wire string. Sites whose original Message
was empty stay on Internal(err) (humanization to "internal server
error" is a tolerable improvement, not a regression).

admin_smtp.go's send_failed site (line 129) was already preserved
via a raw &apierror.Error literal in 1cc7eb6 and needs no fix.

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

174 lines
4.1 KiB
Go

package apierror
import (
"errors"
"fmt"
"testing"
)
func TestError_ErrorString_NoCause(t *testing.T) {
e := &Error{Message: "x"}
if got := e.Error(); got != "x" {
t.Fatalf("Error() = %q, want %q", got, "x")
}
}
func TestError_ErrorString_WithCause(t *testing.T) {
e := &Error{Message: "x", Cause: errors.New("y")}
if got := e.Error(); got != "x: y" {
t.Fatalf("Error() = %q, want %q", got, "x: y")
}
}
func TestError_Unwrap(t *testing.T) {
cause := errors.New("boom")
e := &Error{Message: "x", Cause: cause}
if got := e.Unwrap(); got != cause {
t.Fatalf("Unwrap() = %v, want %v", got, cause)
}
e2 := &Error{Message: "x"}
if got := e2.Unwrap(); got != nil {
t.Fatalf("Unwrap() with nil Cause = %v, want nil", got)
}
}
func TestFrom_NilReturnsNil(t *testing.T) {
if got := From(nil); got != nil {
t.Fatalf("From(nil) = %v, want nil", got)
}
}
func TestFrom_PreservesAPIError(t *testing.T) {
orig := NotFound("track")
got := From(orig)
if got != orig {
t.Fatalf("From(orig) returned different pointer: got %p, want %p", got, orig)
}
}
func TestFrom_PreservesWrappedAPIError(t *testing.T) {
inner := NotFound("track")
wrapped := fmt.Errorf("ctx: %w", inner)
got := From(wrapped)
if got == nil {
t.Fatal("From(wrapped) = nil, want *Error")
}
if got.Code != "track_not_found" {
t.Fatalf("got.Code = %q, want %q", got.Code, "track_not_found")
}
}
func TestFrom_WrapsPlainError(t *testing.T) {
plain := errors.New("boom")
got := From(plain)
if got == nil {
t.Fatal("From(plain) = nil, want *Error")
}
if got.Status != 500 {
t.Errorf("Status = %d, want 500", got.Status)
}
if got.Code != "server_error" {
t.Errorf("Code = %q, want %q", got.Code, "server_error")
}
if got.Cause != plain {
t.Errorf("Cause = %v, want %v", got.Cause, plain)
}
}
func TestConstructors_FieldsCorrect(t *testing.T) {
tests := []struct {
name string
err *Error
wantStatus int
wantCode string
wantMessage string
}{
{
name: "NotFound",
err: NotFound("track"),
wantStatus: 404,
wantCode: "track_not_found",
wantMessage: "track not found",
},
{
name: "BadRequest",
err: BadRequest("invalid_input", "invalid input"),
wantStatus: 400,
wantCode: "invalid_input",
wantMessage: "invalid input",
},
{
name: "Conflict",
err: Conflict("already_exists", "already exists"),
wantStatus: 409,
wantCode: "already_exists",
wantMessage: "already exists",
},
{
name: "Forbidden",
err: Forbidden("forbidden", "forbidden"),
wantStatus: 403,
wantCode: "forbidden",
wantMessage: "forbidden",
},
{
name: "Unauthorized",
err: Unauthorized("unauthorized", "authentication required"),
wantStatus: 401,
wantCode: "unauthorized",
wantMessage: "authentication required",
},
{
name: "Internal",
err: Internal(errors.New("boom")),
wantStatus: 500,
wantCode: "server_error",
wantMessage: "internal server error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.err.Status != tt.wantStatus {
t.Errorf("Status = %d, want %d", tt.err.Status, tt.wantStatus)
}
if tt.err.Code != tt.wantCode {
t.Errorf("Code = %q, want %q", tt.err.Code, tt.wantCode)
}
if tt.err.Message != tt.wantMessage {
t.Errorf("Message = %q, want %q", tt.err.Message, tt.wantMessage)
}
})
}
}
func TestInternalMsg(t *testing.T) {
cause := errors.New("db down")
e := InternalMsg("lookup failed", cause)
if e.Status != 500 {
t.Errorf("Status = %d, want 500", e.Status)
}
if e.Code != "server_error" {
t.Errorf("Code = %q, want server_error", e.Code)
}
if e.Message != "lookup failed" {
t.Errorf("Message = %q, want lookup failed", e.Message)
}
if !errors.Is(e, cause) {
t.Errorf("expected Is(cause) to be true")
}
}
func TestSentinels_Identity(t *testing.T) {
wrapped := fmt.Errorf("ctx: %w", ErrNotFound)
var apiErr *Error
if !errors.As(wrapped, &apiErr) {
t.Fatal("errors.As(wrapped, &apiErr) = false, want true")
}
if apiErr != ErrNotFound {
t.Fatalf("recovered error pointer = %p, want %p", apiErr, ErrNotFound)
}
}