63ede5c237
- refactor: move HTTP status and DTO concerns out of model and service layers into API and handler code. - feat: add admin service boundary and route auth through services instead of direct repository access. - test: add architecture and error tests covering package boundaries, redaction, and service behavior. - docs: record the protocol-neutral boundary decision and update architecture and roadmap notes.
102 lines
3.5 KiB
Go
102 lines
3.5 KiB
Go
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
|
|
}
|