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:
Tomas DvorakandDevin committed 2026-09-24 22:58:55 +02:00
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");
});
});