Files
gochat/backend/internal/handler/api/v1/conversation_handler_edge_test.go
T
Rogeeandrogee 2b182f9956 H-300: wire Captain Skills into Web runtime (#48)
* H-300: wire Captain Skills into Web runtime

* H-300: enforce effective model and conservative skill budget

* H-300: fix CI gosec step

* ci: extend golangci-lint timeout

* fix lint findings across backend

* fix(push): resolve delivery protocol blockers

* test(repository): close SQLite test databases

* test(repository): reuse SQLite schema per package

* H-307: restore backend Go cache in CI

* H-307: prefetch modules before cold lint

* H-307: resolve govulncheck security gate

* H-307: build lint with patched Go toolchain

* H-307: clear remaining security scan findings

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-19 07:08:14 +08:00

309 lines
10 KiB
Go

package v1
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/gochat/gochat/internal/service"
)
// setupConversationEdgeRouter creates a test router for conversation handler edge-case tests.
func setupConversationEdgeRouter(handler *ConversationHandler) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(gin.Recovery(), mockAuthMiddleware())
router.POST("/api/v1/accounts/:account_id/conversations", handler.Create)
router.POST("/api/v1/accounts/:account_id/conversations/filter", handler.Filter)
router.PATCH("/api/v1/accounts/:account_id/conversations/:id", handler.Update)
router.GET("/api/v1/accounts/:account_id/conversations", handler.List)
router.GET("/api/v1/accounts/:account_id/conversations/:id", handler.Get)
router.DELETE("/api/v1/accounts/:account_id/conversations/:id", handler.Delete)
return router
}
func newConversationEdgeHandler() *ConversationHandler {
return NewConversationHandler(&service.ConversationService{}, &service.MessageService{})
}
// ===========================
// Create Conversation edge cases
// ===========================
func TestConversationCreate_EmptyBody(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations", nil)
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// ShouldBindJSON on nil body returns EOF error → 400
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
panic(err)
}
assert.Contains(t, resp, "error")
}
func TestConversationCreate_InvalidJSON(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations", bytes.NewBufferString(`{invalid json`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
panic(err)
}
assert.Contains(t, resp, "error")
}
func TestConversationCreate_MissingRequiredFields(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
// inbox_id and contact_id are required (validate:"required")
w := httptest.NewRecorder()
body := `{"status":"open"}`
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// ShouldBindJSON succeeds (no syntax error), but the zero-service will fail when calling Create
// with validation error. Since service is nil, it panics → Recovery returns 500.
// But the handler should at least not crash. We just check it doesn't return 200.
assert.NotEqual(t, http.StatusOK, w.Code)
}
func TestConversationCreate_InvalidAccountID(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/conversations", bytes.NewBufferString(`{"inbox_id":1,"contact_id":1}`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationCreate_OutOfRangeStatus(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
// status must be oneof=open resolved pending snoozed; "invalid_status" is out of range
w := httptest.NewRecorder()
body := `{"inbox_id":1,"contact_id":1,"status":"invalid_status"}`
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// ShouldBindJSON succeeds (JSON syntax is valid), but service validation should reject
// With nil service, handler calls service.Create which panics → 500 from Recovery
assert.NotEqual(t, http.StatusOK, w.Code)
}
// ===========================
// Filter Conversation edge cases
// ===========================
func TestConversationFilter_EmptyBody(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/filter", nil)
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationFilter_InvalidJSON(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/filter", bytes.NewBufferString(`{broken`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationFilter_InvalidAccountID(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/notanumber/conversations/filter", bytes.NewBufferString(`{"status":"open"}`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationFilter_OutOfRangeParams(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
// status must be oneof=open resolved pending snoozed; "closed" is invalid
w := httptest.NewRecorder()
body := `{"status":"closed"}`
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/filter", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// ShouldBindJSON succeeds, but service.Filter will fail with nil db → 500 from Recovery
assert.NotEqual(t, http.StatusOK, w.Code)
}
// ===========================
// Update Conversation edge cases
// ===========================
func TestConversationUpdate_EmptyBody(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/conversations/1", nil)
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationUpdate_InvalidJSON(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/conversations/1", bytes.NewBufferString(`{"status":`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationUpdate_InvalidAccountID(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/accounts/abc/conversations/1", bytes.NewBufferString(`{"status":"open"}`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationUpdate_InvalidConversationID(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/conversations/xyz", bytes.NewBufferString(`{"status":"open"}`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationUpdate_OutOfRangePriority(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
// priority must be oneof=urgent high medium low; "extreme" is invalid
w := httptest.NewRecorder()
body := `{"priority":"extreme"}`
req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/conversations/1", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
// ShouldBindJSON succeeds, but service rejects via validation → 500 from nil service
assert.NotEqual(t, http.StatusOK, w.Code)
}
// ===========================
// List Conversation edge cases
// ===========================
func TestConversationList_InvalidAccountID(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/invalid/conversations", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationList_WrongMethod(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
// List is GET-only; sending DELETE should return 404 (no route match)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/conversations", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
// ===========================
// Get Conversation edge cases
// ===========================
func TestConversationGet_InvalidAccountID(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/conversations/1", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestConversationGet_InvalidConversationID(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations/notanumber", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ===========================
// Delete Conversation edge cases
// ===========================
func TestConversationDelete_WrongMethod(t *testing.T) {
handler := newConversationEdgeHandler()
router := setupConversationEdgeRouter(handler)
// PUT on the DELETE-only route returns 404 (no route match)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/conversations/1", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}