template-monorepo/internal/library/errors/errors_test.go

189 lines
4.8 KiB
Go
Raw Normal View History

2026-05-19 11:00:28 +00:00
package errs_test
import (
"errors"
"net/http"
"testing"
errs "gateway/internal/library/errors"
"gateway/internal/library/errors/code"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var app = errs.For(code.Facade)
func TestNewValidation(t *testing.T) {
t.Parallel()
_, err := errs.New(code.Scope(100), code.InputInvalidFormat, 0, "bad scope")
if !errors.Is(err, errs.ErrInvalidCode) {
t.Fatalf("expected ErrInvalidCode, got %v", err)
}
_, err = errs.New(code.Facade, code.Category(1000), 0, "bad category")
if !errors.Is(err, errs.ErrInvalidCode) {
t.Fatalf("expected ErrInvalidCode, got %v", err)
}
}
func TestBuilderCode(t *testing.T) {
t.Parallel()
e := app.ResNotFound("user", "42")
if e.DisplayCode() != "10301000" {
t.Fatalf("DisplayCode = %s, want 10301000", e.DisplayCode())
}
if e.HTTPStatus() != http.StatusNotFound {
t.Fatalf("HTTPStatus = %d, want 404", e.HTTPStatus())
}
}
func TestWithCauseImmutable(t *testing.T) {
t.Parallel()
base := app.DBError("db fail")
wrapped := base.WithCause(errors.New("pq: connection refused"))
if base.Unwrap() != nil {
t.Fatal("WithCause must not mutate original error")
}
if wrapped.Unwrap() == nil {
t.Fatal("wrapped error should have cause")
}
}
func TestWithScope(t *testing.T) {
t.Parallel()
e := app.ResNotFound("x")
moved, err := e.WithScope(code.LocalAPI)
if err != nil {
t.Fatal(err)
}
if moved.DisplayCode() != "11301000" {
t.Fatalf("DisplayCode = %s, want 11301000", moved.DisplayCode())
}
if e.DisplayCode() != "10301000" {
t.Fatal("WithScope must not mutate original error")
}
}
func TestIsUsesFullCode(t *testing.T) {
t.Parallel()
a := app.ResNotFound("a")
b := errs.For(code.LocalAPI).ResNotFound("b")
if errors.Is(a, b) {
t.Fatal("same category/detail but different scope must not match")
}
if !errors.Is(a, app.ResNotFound("c")) {
t.Fatal("same full code should match via errors.Is")
}
}
func TestFromCodeRoundTrip(t *testing.T) {
t.Parallel()
raw := uint32(10301123)
e, err := errs.FromCode(raw)
if err != nil {
t.Fatal(err)
}
if e.Code() != raw {
t.Fatalf("Code = %d, want %d", e.Code(), raw)
}
}
func TestGRPCStatusBuiltin(t *testing.T) {
t.Parallel()
st := status.New(codes.NotFound, "missing")
e, err := errs.FromGRPCError(st.Err(), code.Facade)
if err != nil {
t.Fatal(err)
}
if e.Category() != code.CatGRPC {
t.Fatalf("Category = %d, want %d", e.Category(), code.CatGRPC)
}
if e.Detail() != code.Detail(codes.NotFound) {
t.Fatalf("Detail = %d, want %d", e.Detail(), codes.NotFound)
}
if e.Scope() != code.Facade {
t.Fatalf("Scope = %d, want %d", e.Scope(), code.Facade)
}
}
func TestGRPCRoundTrip(t *testing.T) {
t.Parallel()
orig, err := app.InputInvalidFormat("email invalid").WithDetail(7)
if err != nil {
t.Fatal(err)
}
st := orig.GRPCStatus()
back, err := errs.FromGRPCError(st.Err())
if err != nil {
t.Fatal(err)
}
if back.Code() != orig.Code() {
t.Fatalf("Code = %d, want %d", back.Code(), orig.Code())
}
if back.Error() != orig.Error() {
t.Fatalf("msg = %q, want %q", back.Error(), orig.Error())
}
if back.GRPCCode() != codes.InvalidArgument {
t.Fatalf("GRPCCode = %s, want InvalidArgument", back.GRPCCode())
}
}
func TestCatGRPCHTTPStatus(t *testing.T) {
t.Parallel()
e := errs.MustNew(code.Facade, code.CatGRPC, code.Detail(codes.NotFound), "missing")
if e.HTTPStatus() != http.StatusNotFound {
t.Fatalf("HTTPStatus = %d, want 404", e.HTTPStatus())
}
}
func TestExtendedCategoriesHTTPStatus(t *testing.T) {
t.Parallel()
cases := []struct {
name string
got *errs.Error
status int
}{
{"InputMissingRequired", app.InputMissingRequired("name"), http.StatusBadRequest},
{"InputUnsupportedMedia", app.InputUnsupportedMedia("application/xml"), http.StatusUnsupportedMediaType},
{"InputPayloadTooLarge", app.InputPayloadTooLarge(), http.StatusRequestEntityTooLarge},
{"DBUnavailable", app.DBUnavailable(), http.StatusServiceUnavailable},
{"ResPreconditionFailed", app.ResPreconditionFailed("etag mismatch"), http.StatusPreconditionFailed},
{"ResLocked", app.ResLocked(), http.StatusLocked},
{"AuthMethodNotAllowed", app.AuthMethodNotAllowed("POST"), http.StatusMethodNotAllowed},
{"SysNotImplemented", app.SysNotImplemented(), http.StatusNotImplemented},
{"SysClientTimeout", app.SysClientTimeout(), http.StatusRequestTimeout},
{"SvcRateLimited", app.SvcRateLimited("payment provider"), http.StatusTooManyRequests},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.got.HTTPStatus() != tc.status {
t.Fatalf("HTTPStatus() = %d, want %d (code %s)", tc.got.HTTPStatus(), tc.status, tc.got.DisplayCode())
}
})
}
}
func TestBuilderCustomDetail(t *testing.T) {
t.Parallel()
e := app.Code(code.SysTimeout, 42, "downstream timeout")
if e.Detail() != 42 {
t.Fatalf("Detail = %d, want 42", e.Detail())
}
}