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()) } }