package apperror
import (
"errors"
"fmt"
"sync"
"testing"
)
func TestDefineRejectsInvalidDefinitions(t *testing.T) {
tests := []struct {
name string
code Code
message string
}{
{name: "empty code", message: "message"},
{name: "missing domain", code: "reason", message: "message"},
{name: "uppercase", code: "Sandbox.reason", message: "message"},
{name: "empty message", code: "sandbox.reason"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("Define() did not panic")
}
}()
_ = Define(InvalidArgument, tt.code, tt.message)
})
}
}
func TestWrapPreservesClassificationAndCause(t *testing.T) {
prototype := Define(NotFound, "sandbox.not_found", "sandbox not found")
cause := errors.New("database path and internal detail")
err := fmt.Errorf("get sandbox: %w", prototype.Wrap(cause))
var appErr *Error
if !errors.As(err, &appErr) {
t.Fatal("errors.As() did not find application error")
}
if appErr.Kind() != NotFound || appErr.Code() != "sandbox.not_found" || appErr.PublicMessage() != "sandbox not found" {
t.Fatalf("application error = %#v", appErr)
}
if !errors.Is(err, prototype) || !errors.Is(err, cause) {
t.Fatalf("error chain does not retain prototype and cause: %v", err)
}
}
func TestInstancesDoNotMutatePrototype(t *testing.T) {
prototype := Define(InvalidArgument, "request.invalid_body", "invalid request body")
first := prototype.WrapMessage(errors.New("first"), "reviewed message")
second := prototype.Wrap(nil)
if first == prototype || second == prototype || first == second {
t.Fatal("constructors returned shared instances")
}
if first.PublicMessage() != "reviewed message" {
t.Fatalf("first message = %q", first.PublicMessage())
}
if second.PublicMessage() != "invalid request body" || prototype.PublicMessage() != "invalid request body" {
t.Fatal("prototype was mutated")
}
if second.Unwrap() != nil {
t.Fatalf("Wrap(nil) cause = %v", second.Unwrap())
}
if got := prototype.WrapMessage(errors.New("empty"), " ").PublicMessage(); got != prototype.PublicMessage() {
t.Fatalf("empty WrapMessage() message = %q, want %q", got, prototype.PublicMessage())
}
}
func TestPrototypeCanBeWrappedConcurrently(t *testing.T) {
prototype := Define(Conflict, "sandbox.concurrent_update", "sandbox update conflicted")
const workers = 64
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func(i int) {
defer wg.Done()
wrapped := prototype.WrapMessage(fmt.Errorf("cause %d", i), fmt.Sprintf("message %d", i))
if wrapped.Code() != prototype.Code() || wrapped.PublicMessage() == prototype.PublicMessage() {
t.Errorf("wrapped error = %#v", wrapped)
}
}(i)
}
wg.Wait()
if prototype.PublicMessage() != "sandbox update conflicted" || prototype.Unwrap() != nil {
t.Fatalf("prototype was mutated: %#v", prototype)
}
}