Files
gochat/backend/pkg/response/response_test.go
T
Rogeeandrogee 60ad320e8d [H-337] Fix Captain provider runtime and knowledge flow (#61)
* fix(H-337): configure Captain provider runtime

* fix(captain): make knowledge rebuild atomic

* fix(captain): scope retrieval provider failures

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-20 14:46:01 +08:00

323 lines
9.2 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_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"`)
}