Build and publish Docker images / Build and publish images (push) Failing after 25s
334 lines
9.6 KiB
Go
334 lines
9.6 KiB
Go
package response
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func init() {
|
|
gin.SetMode(gin.TestMode)
|
|
}
|
|
|
|
// --- AppError tests ---
|
|
|
|
func TestAppError_Error_WithDetail(t *testing.T) {
|
|
e := &AppError{
|
|
Code: ErrNotFound,
|
|
Message: "resource missing",
|
|
Detail: "ID=42",
|
|
}
|
|
assert.Equal(t, "NOT_FOUND: resource missing (ID=42)", e.Error())
|
|
}
|
|
|
|
func TestAppError_Error_WithoutDetail(t *testing.T) {
|
|
e := &AppError{
|
|
Code: ErrBadRequest,
|
|
Message: "bad input",
|
|
}
|
|
assert.Equal(t, "BAD_REQUEST: bad input", e.Error())
|
|
}
|
|
|
|
func TestNewAppError(t *testing.T) {
|
|
e := NewAppError(ErrValidation, "invalid field", http.StatusBadRequest)
|
|
assert.Equal(t, ErrValidation, e.Code)
|
|
assert.Equal(t, "invalid field", e.Message)
|
|
assert.Equal(t, http.StatusBadRequest, e.Status)
|
|
assert.Empty(t, e.Detail)
|
|
}
|
|
|
|
func TestAppError_WithDetail(t *testing.T) {
|
|
e := NewAppError(ErrValidation, "invalid field", http.StatusBadRequest)
|
|
e2 := e.WithDetail("field 'name' is required")
|
|
assert.Same(t, e, e2) // WithDetail returns the same pointer
|
|
assert.Equal(t, "field 'name' is required", e.Detail)
|
|
}
|
|
|
|
// --- Convenience constructor tests ---
|
|
|
|
func TestErrInternalError(t *testing.T) {
|
|
e := ErrInternalError("something broke")
|
|
assert.Equal(t, ErrInternal, e.Code)
|
|
assert.Equal(t, "something broke", e.Message)
|
|
assert.Equal(t, http.StatusInternalServerError, e.Status)
|
|
}
|
|
|
|
func TestErrNotFoundError(t *testing.T) {
|
|
e := ErrNotFoundError("Account")
|
|
assert.Equal(t, ErrNotFound, e.Code)
|
|
assert.Equal(t, "Account not found", e.Message)
|
|
assert.Equal(t, http.StatusNotFound, e.Status)
|
|
}
|
|
|
|
func TestErrBadRequestError(t *testing.T) {
|
|
e := ErrBadRequestError("invalid body")
|
|
assert.Equal(t, ErrBadRequest, e.Code)
|
|
assert.Equal(t, http.StatusBadRequest, e.Status)
|
|
}
|
|
|
|
func TestErrUnauthorizedError(t *testing.T) {
|
|
e := ErrUnauthorizedError("no token")
|
|
assert.Equal(t, ErrUnauthorized, e.Code)
|
|
assert.Equal(t, http.StatusUnauthorized, e.Status)
|
|
}
|
|
|
|
func TestErrForbiddenError(t *testing.T) {
|
|
e := ErrForbiddenError("no access")
|
|
assert.Equal(t, ErrForbidden, e.Code)
|
|
assert.Equal(t, http.StatusForbidden, e.Status)
|
|
}
|
|
|
|
func TestErrValidationError(t *testing.T) {
|
|
e := ErrValidationError("name required")
|
|
assert.Equal(t, ErrValidation, e.Code)
|
|
assert.Equal(t, http.StatusBadRequest, e.Status)
|
|
}
|
|
|
|
func TestErrConflictError(t *testing.T) {
|
|
e := ErrConflictError("email exists")
|
|
assert.Equal(t, ErrConflict, e.Code)
|
|
assert.Equal(t, http.StatusConflict, e.Status)
|
|
}
|
|
|
|
func TestErrRateLimitError(t *testing.T) {
|
|
e := ErrRateLimitError("too many requests")
|
|
assert.Equal(t, ErrRateLimit, e.Code)
|
|
assert.Equal(t, http.StatusTooManyRequests, e.Status)
|
|
}
|
|
|
|
// --- ErrorToHTTPStatus tests ---
|
|
|
|
func TestErrorToHTTPStatus(t *testing.T) {
|
|
tests := []struct {
|
|
code ErrorCode
|
|
expected int
|
|
}{
|
|
{ErrNotFound, http.StatusNotFound},
|
|
{ErrAccountNotFound, http.StatusNotFound},
|
|
{ErrInboxNotFound, http.StatusNotFound},
|
|
{ErrContactNotFound, http.StatusNotFound},
|
|
{ErrConversationNotFound, http.StatusNotFound},
|
|
{ErrMessageNotFound, http.StatusNotFound},
|
|
{ErrUserNotFound, http.StatusNotFound},
|
|
{ErrBadRequest, http.StatusBadRequest},
|
|
{ErrValidation, http.StatusBadRequest},
|
|
{ErrUnauthorized, http.StatusUnauthorized},
|
|
{ErrForbidden, http.StatusForbidden},
|
|
{ErrChannelNotEnabled, http.StatusForbidden},
|
|
{ErrConflict, http.StatusConflict},
|
|
{ErrDuplicateRecord, http.StatusConflict},
|
|
{ErrChannelInvalid, http.StatusConflict},
|
|
{ErrCaptainAssistantDisabled, http.StatusConflict},
|
|
{ErrRateLimit, http.StatusTooManyRequests},
|
|
{ErrCopilotProviderRateLimited, http.StatusTooManyRequests},
|
|
{ErrCopilotProviderTimeout, http.StatusGatewayTimeout},
|
|
{ErrServiceUnavail, http.StatusServiceUnavailable},
|
|
{ErrCopilotNotConfigured, http.StatusServiceUnavailable},
|
|
{ErrCopilotProviderAuth, http.StatusServiceUnavailable},
|
|
{ErrCopilotProviderUnreachable, http.StatusServiceUnavailable},
|
|
{ErrCopilotModelNotFound, http.StatusServiceUnavailable},
|
|
// Default case
|
|
{ErrInternal, http.StatusInternalServerError},
|
|
{ErrorCode("UNKNOWN_CODE"), http.StatusInternalServerError},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.code), func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, ErrorToHTTPStatus(tt.code))
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- Response helper tests ---
|
|
|
|
func newTestContext() (*gin.Context, *httptest.ResponseRecorder) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
|
return c, w
|
|
}
|
|
|
|
func TestOK(t *testing.T) {
|
|
c, w := newTestContext()
|
|
OK(c, map[string]string{"key": "value"})
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp APIResponse
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.True(t, resp.Success)
|
|
assert.NotNil(t, resp.Data)
|
|
assert.Nil(t, resp.Error)
|
|
assert.Nil(t, resp.Meta)
|
|
}
|
|
|
|
func TestOK_NilData(t *testing.T) {
|
|
c, w := newTestContext()
|
|
OK(c, nil)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp APIResponse
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.True(t, resp.Success)
|
|
// Data is omitted when nil due to omitempty
|
|
}
|
|
|
|
func TestOKWithMeta(t *testing.T) {
|
|
c, w := newTestContext()
|
|
OKWithMeta(c, []string{"a", "b"}, 2, 10, 25)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp APIResponse
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.True(t, resp.Success)
|
|
assert.NotNil(t, resp.Data)
|
|
require.NotNil(t, resp.Meta)
|
|
assert.Equal(t, 2, resp.Meta.Page)
|
|
assert.Equal(t, 10, resp.Meta.PerPage)
|
|
assert.Equal(t, int64(25), resp.Meta.TotalCount)
|
|
assert.Nil(t, resp.Error)
|
|
}
|
|
|
|
func TestCreated(t *testing.T) {
|
|
c, w := newTestContext()
|
|
Created(c, map[string]int{"id": 1})
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var resp APIResponse
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.True(t, resp.Success)
|
|
assert.NotNil(t, resp.Data)
|
|
}
|
|
|
|
func TestNoContent(t *testing.T) {
|
|
// Use a real router since CreateTestContext doesn't fully set up the
|
|
// response writer for Status() calls.
|
|
r := gin.New()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
NoContent(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNoContent, w.Code)
|
|
assert.Empty(t, w.Body.Bytes())
|
|
}
|
|
|
|
func TestAbortWithError(t *testing.T) {
|
|
c, w := newTestContext()
|
|
appErr := NewAppError(ErrNotFound, "resource missing", http.StatusNotFound).WithDetail("ID=99")
|
|
AbortWithError(c, appErr)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
var resp APIResponse
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.False(t, resp.Success)
|
|
require.NotNil(t, resp.Error)
|
|
assert.Equal(t, ErrNotFound, resp.Error.Code)
|
|
assert.Equal(t, "resource missing", resp.Error.Message)
|
|
assert.Equal(t, "ID=99", resp.Error.Detail)
|
|
assert.Nil(t, resp.Data)
|
|
}
|
|
|
|
func TestAbortWithError_NoDetail(t *testing.T) {
|
|
c, w := newTestContext()
|
|
appErr := NewAppError(ErrBadRequest, "bad request", http.StatusBadRequest)
|
|
AbortWithError(c, appErr)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
var resp APIResponse
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.False(t, resp.Success)
|
|
require.NotNil(t, resp.Error)
|
|
assert.Equal(t, ErrBadRequest, resp.Error.Code)
|
|
assert.Empty(t, resp.Error.Detail)
|
|
}
|
|
|
|
func TestAbortWithStatusError(t *testing.T) {
|
|
c, w := newTestContext()
|
|
AbortWithStatusError(c, http.StatusUnauthorized, ErrUnauthorized, "token expired")
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
var resp APIResponse
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.False(t, resp.Success)
|
|
require.NotNil(t, resp.Error)
|
|
assert.Equal(t, ErrUnauthorized, resp.Error.Code)
|
|
assert.Equal(t, "token expired", resp.Error.Message)
|
|
assert.Empty(t, resp.Error.Detail) // AbortWithStatusError does not set Detail
|
|
}
|
|
|
|
func TestAbortWithStatusError_LocalizesRawValidationMessage(t *testing.T) {
|
|
c, w := newTestContext()
|
|
raw := "Key: 'LoginRequest.Password' Error:Field validation for 'Password' failed on the 'min' tag"
|
|
AbortWithStatusError(c, http.StatusBadRequest, ErrValidation, raw)
|
|
|
|
var resp APIResponse
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
require.NotNil(t, resp.Error)
|
|
assert.Equal(t, "请求参数不符合要求", resp.Error.Message)
|
|
}
|
|
|
|
func TestAbortWithStatusError_ForbidsFurtherHandlers(t *testing.T) {
|
|
// Verify that abort truly prevents subsequent handlers from running
|
|
router := gin.New()
|
|
router.GET("/test", func(c *gin.Context) {
|
|
AbortWithStatusError(c, http.StatusForbidden, ErrForbidden, "denied")
|
|
}, func(c *gin.Context) {
|
|
t.Fatal("second handler should not be called after abort")
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
func TestAPIResponse_JSONSerialization(t *testing.T) {
|
|
resp := APIResponse{
|
|
Success: true,
|
|
Data: "test data",
|
|
}
|
|
data, err := json.Marshal(resp)
|
|
require.NoError(t, err)
|
|
|
|
// Should contain "success" and "data" but not "error" or "meta" (omitempty)
|
|
s := string(data)
|
|
assert.Contains(t, s, `"success":true`)
|
|
assert.Contains(t, s, `"data":"test data"`)
|
|
assert.NotContains(t, s, `"error"`)
|
|
assert.NotContains(t, s, `"meta"`)
|
|
}
|
|
|
|
func TestAPIResponse_ErrorJSONSerialization(t *testing.T) {
|
|
resp := APIResponse{
|
|
Success: false,
|
|
Error: &ErrorBody{
|
|
Code: ErrNotFound,
|
|
Message: "not here",
|
|
},
|
|
}
|
|
data, err := json.Marshal(resp)
|
|
require.NoError(t, err)
|
|
|
|
s := string(data)
|
|
assert.Contains(t, s, `"success":false`)
|
|
assert.Contains(t, s, `"error"`)
|
|
assert.Contains(t, s, `"code":"NOT_FOUND"`)
|
|
assert.Contains(t, s, `"message":"not here"`)
|
|
assert.NotContains(t, s, `"data"`)
|
|
assert.NotContains(t, s, `"meta"`)
|
|
}
|