mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 07:41:23 +00:00
test: regression coverage for error mapping and client error extraction
- handlers: pgx.ErrNoRows→404, InputError→400 with message preserved, pgconn 23505→409 without constraint/SQLSTATE leak, unknown→500 - services: export NewInputError so error construction is testable - frontend: vitest for errorMessage helper — server reason preferred, status text and fallbacks verified Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
89f3d84234
commit
a71baa49cd
3 files changed
+78
No files matched your search
@@ -4,9 +4,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
|
||||||
|
"github.com/tdvorak/primora/apps/backend/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestContext(rawQuery string) (*gin.Context, *httptest.ResponseRecorder) {
|
func newTestContext(rawQuery string) (*gin.Context, *httptest.ResponseRecorder) {
|
||||||
@@ -104,4 +109,46 @@ func TestHandleErrorStatusMapping(t *testing.T) {
|
|||||||
t.Fatalf("unexpected status: %d", recorder.Code)
|
t.Fatalf("unexpected status: %d", recorder.Code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("maps missing rows to not found", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, recorder := newTestContext("")
|
||||||
|
handler.handleError(ctx, pgx.ErrNoRows)
|
||||||
|
if recorder.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("unexpected status: %d", recorder.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("maps InputError to bad request with message", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, recorder := newTestContext("")
|
||||||
|
handler.handleError(ctx, services.NewInputError("webhook url must be https"))
|
||||||
|
if recorder.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("unexpected status: %d", recorder.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(recorder.Body.String(), "webhook url must be https") {
|
||||||
|
t.Fatalf("expected reason in body, got %s", recorder.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("maps unique violation to conflict without leaking", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, recorder := newTestContext("")
|
||||||
|
handler.handleError(ctx, &pgconn.PgError{Code: "23505", Message: "duplicate key value violates unique constraint \"projects_slug_key\""})
|
||||||
|
if recorder.Code != http.StatusConflict {
|
||||||
|
t.Fatalf("unexpected status: %d", recorder.Code)
|
||||||
|
}
|
||||||
|
if strings.Contains(recorder.Body.String(), "projects_slug_key") || strings.Contains(recorder.Body.String(), "23505") {
|
||||||
|
t.Fatalf("raw constraint leaked: %s", recorder.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("maps unknown pg error to internal error", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, recorder := newTestContext("")
|
||||||
|
handler.handleError(ctx, &pgconn.PgError{Code: "XX000", Message: "internal"})
|
||||||
|
if recorder.Code != http.StatusInternalServerError {
|
||||||
|
t.Fatalf("unexpected status: %d", recorder.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -1534,6 +1534,8 @@ type InputError struct{ msg string }
|
|||||||
|
|
||||||
func (e *InputError) Error() string { return e.msg }
|
func (e *InputError) Error() string { return e.msg }
|
||||||
|
|
||||||
|
func NewInputError(msg string) error { return &InputError{msg: msg} }
|
||||||
|
|
||||||
func inputErrorf(format string, args ...any) error {
|
func inputErrorf(format string, args ...any) error {
|
||||||
return &InputError{msg: fmt.Sprintf(format, args...)}
|
return &InputError{msg: fmt.Sprintf(format, args...)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { errorMessage } from "../api";
|
||||||
|
|
||||||
|
describe("errorMessage", () => {
|
||||||
|
it("prefers the server's reason over the HTTP status text", () => {
|
||||||
|
const apiError = {
|
||||||
|
message: "Bad Request",
|
||||||
|
body: { error: { message: "webhook url must be https (http allowed only for private/self-hosted targets)" } },
|
||||||
|
};
|
||||||
|
expect(errorMessage(apiError)).toBe(
|
||||||
|
"webhook url must be https (http allowed only for private/self-hosted targets)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to error.message when no body reason exists", () => {
|
||||||
|
expect(errorMessage(new Error("network down"))).toBe("network down");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the fallback for non-error values", () => {
|
||||||
|
expect(errorMessage(null, "Nope")).toBe("Nope");
|
||||||
|
expect(errorMessage(undefined)).toBe("Request failed");
|
||||||
|
expect(errorMessage("string failure")).toBe("Request failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores empty body and message strings", () => {
|
||||||
|
expect(errorMessage({ message: "", body: { error: { message: "" } } }, "Empty")).toBe("Empty");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user