package api import ( "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/dhao2001/mygo/internal/model" ) func TestError(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.GET("/error", func(c *gin.Context) { Error(c, http.StatusBadRequest, "invalid request") }) req := httptest.NewRequest(http.MethodGet, "/error", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) } var body ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("unmarshal response: %v", err) } if body.Error.Message != "invalid request" { t.Errorf("message = %q, want %q", body.Error.Message, "invalid request") } if body.Error.LogID != "" { t.Errorf("log_id = %q, want empty", body.Error.LogID) } } func TestRespondErrorMapsDomainKinds(t *testing.T) { tests := []struct { name string err error wantStatus int wantLogID bool }{ {"invalid argument", model.NewInvalidArgumentError("bad input"), http.StatusBadRequest, false}, {"unauthenticated", model.NewUnauthenticatedError("invalid token"), http.StatusUnauthorized, false}, {"permission denied", model.NewPermissionDeniedError("access denied", nil), http.StatusForbidden, false}, {"not found", model.NewNotFoundError("missing", nil), http.StatusNotFound, false}, {"conflict", model.NewConflictError("duplicate"), http.StatusConflict, false}, {"payload too large", model.NewPayloadTooLargeError("too large", nil), http.StatusRequestEntityTooLarge, false}, {"internal", model.NewInternalError("lookup", errors.New("database offline")), http.StatusInternalServerError, true}, {"unknown", errors.New("escaped error"), http.StatusInternalServerError, true}, {"permission denied sentinel cause", model.NewPermissionDeniedError("access denied", model.ErrForbidden), http.StatusForbidden, false}, {"not found sentinel cause", model.NewNotFoundError("missing", model.ErrNotFound), http.StatusNotFound, false}, {"payload too large sentinel cause", model.NewPayloadTooLargeError("too large", model.ErrUploadTooLarge), http.StatusRequestEntityTooLarge, false}, {"client with diagnostic cause", model.NewPermissionDeniedError("access denied", errors.New("policy backend failed")), http.StatusForbidden, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rec := exerciseRespondError(tt.err) if rec.Code != tt.wantStatus { t.Fatalf("status = %d, want %d; body = %s", rec.Code, tt.wantStatus, rec.Body.String()) } var body ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("unmarshal response: %v", err) } if tt.wantLogID && body.Error.LogID == "" { t.Fatalf("log_id is empty, want populated; body = %s", rec.Body.String()) } if !tt.wantLogID && body.Error.LogID != "" { t.Fatalf("log_id = %q, want empty", body.Error.LogID) } if rec.Code >= http.StatusInternalServerError && body.Error.Message != "internal server error" { t.Fatalf("message = %q, want sanitized internal message", body.Error.Message) } }) } } func exerciseRespondError(err error) *httptest.ResponseRecorder { gin.SetMode(gin.TestMode) router := gin.New() router.GET("/error", func(c *gin.Context) { RespondError(c, err) }) req := httptest.NewRequest(http.MethodGet, "/error", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) return rec }