343 lines
9.6 KiB
Go
343 lines
9.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/database"
|
|
)
|
|
|
|
// ── Channel Request Validation ─────────────────
|
|
|
|
func TestCreateChannelMissingTitle(t *testing.T) {
|
|
h := NewChannelHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/channels",
|
|
strings.NewReader(`{}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateChannel(c)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateChannelTitleTooLong(t *testing.T) {
|
|
h := NewChannelHandler()
|
|
|
|
longTitle := strings.Repeat("x", 501)
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/channels",
|
|
strings.NewReader(`{"title":"`+longTitle+`"}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateChannel(c)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected 400 for title > 500 chars, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestUpdateChannelEmptyBody(t *testing.T) {
|
|
h := NewChannelHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set("user_id", "test-user-id")
|
|
c.Params = gin.Params{{Key: "id", Value: "test-channel-id"}}
|
|
c.Request = httptest.NewRequest("PUT", "/api/v1/channels/test-channel-id",
|
|
strings.NewReader(`{}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
// Without a DB connection, UpdateChannel will fail at ownership check.
|
|
// Integration tests with a real DB validate the "no fields" path.
|
|
// Here we just confirm it doesn't return 400 for valid JSON.
|
|
h.UpdateChannel(c)
|
|
|
|
if w.Code == http.StatusBadRequest {
|
|
t.Error("Empty JSON body should not be a parse error")
|
|
}
|
|
}
|
|
|
|
// ── Message Request Validation ──────────────
|
|
|
|
func TestCreateMessageMissingRole(t *testing.T) {
|
|
h := NewMessageHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "test-channel"}}
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/channels/test-channel/messages",
|
|
strings.NewReader(`{"content":"hello"}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateMessage(c)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected 400 for missing role, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateMessageInvalidRole(t *testing.T) {
|
|
h := NewMessageHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "test-channel"}}
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/channels/test-channel/messages",
|
|
strings.NewReader(`{"role":"invalid","content":"hello"}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateMessage(c)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected 400 for invalid role, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestCreateMessageMissingContent(t *testing.T) {
|
|
h := NewMessageHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: "test-channel"}}
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/channels/test-channel/messages",
|
|
strings.NewReader(`{"role":"user"}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateMessage(c)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected 400 for missing content, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ── Integration: Message CRUD with Real DB ──────
|
|
|
|
func TestCreateMessageValidRoles(t *testing.T) {
|
|
database.RequireTestDB(t)
|
|
database.TruncateAll(t)
|
|
|
|
userID := database.SeedTestUser(t, "roletester", "role@test.com")
|
|
channelID := database.SeedTestChannel(t, userID, "Role Test")
|
|
|
|
h := NewMessageHandler()
|
|
|
|
for _, role := range []string{"user", "assistant", "system"} {
|
|
t.Run(role, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set("user_id", userID)
|
|
c.Params = gin.Params{{Key: "id", Value: channelID}}
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/channels/"+channelID+"/messages",
|
|
strings.NewReader(`{"role":"`+role+`","content":"hello from `+role+`"}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.CreateMessage(c)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("role=%s: expected 201, got %d: %s", role, w.Code, w.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChannelCRUDIntegration(t *testing.T) {
|
|
database.RequireTestDB(t)
|
|
database.TruncateAll(t)
|
|
|
|
userID := database.SeedTestUser(t, "cruduser", "crud@test.com")
|
|
|
|
h := NewChannelHandler()
|
|
r := gin.New()
|
|
r.Use(func(c *gin.Context) { c.Set("user_id", userID); c.Next() })
|
|
r.POST("/channels", h.CreateChannel)
|
|
r.GET("/channels", h.ListChannels)
|
|
r.GET("/channels/:id", h.GetChannel)
|
|
r.PUT("/channels/:id", h.UpdateChannel)
|
|
r.DELETE("/channels/:id", h.DeleteChannel)
|
|
|
|
// Create
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/channels",
|
|
strings.NewReader(`{"title":"Integration Test Channel"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("Create: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var created map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &created)
|
|
channelID := created["id"].(string)
|
|
|
|
// Get
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/channels/"+channelID, nil)
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Get: expected 200, got %d", w.Code)
|
|
}
|
|
|
|
// List
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/channels", nil)
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("List: expected 200, got %d", w.Code)
|
|
}
|
|
var listResp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &listResp)
|
|
if listResp["total"].(float64) < 1 {
|
|
t.Error("List: expected at least 1 channel")
|
|
}
|
|
|
|
// Update
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("PUT", "/channels/"+channelID,
|
|
strings.NewReader(`{"title":"Updated Title"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Update: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Delete
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("DELETE", "/channels/"+channelID, nil)
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Delete: expected 200, got %d", w.Code)
|
|
}
|
|
|
|
// Verify gone
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/channels/"+channelID, nil)
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("Get after delete: expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRegeneratePassesOwnershipCheck(t *testing.T) {
|
|
database.RequireTestDB(t)
|
|
database.TruncateAll(t)
|
|
|
|
userID := database.SeedTestUser(t, "regenuser", "regen@test.com")
|
|
channelID := database.SeedTestChannel(t, userID, "Regen Test")
|
|
|
|
// Seed an assistant message to regenerate
|
|
var msgID string
|
|
err := database.DB.QueryRow(`
|
|
INSERT INTO messages (channel_id, role, content, participant_type, participant_id)
|
|
VALUES ($1, 'assistant', 'original response', 'model', 'test-model')
|
|
RETURNING id
|
|
`, channelID).Scan(&msgID)
|
|
if err != nil {
|
|
t.Fatalf("seed message: %v", err)
|
|
}
|
|
|
|
h := NewMessageHandler()
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set("user_id", userID)
|
|
c.Params = gin.Params{
|
|
{Key: "id", Value: channelID},
|
|
{Key: "msgId", Value: msgID},
|
|
}
|
|
c.Request = httptest.NewRequest("POST",
|
|
"/api/v1/channels/"+channelID+"/messages/"+msgID+"/regenerate",
|
|
strings.NewReader(`{}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.Regenerate(c)
|
|
|
|
// Should NOT be 404 — ownership check passed. Will be 400 or 500
|
|
// because no API config is set up, which is expected.
|
|
if w.Code == http.StatusNotFound {
|
|
t.Errorf("Expected to pass ownership check, got 404: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// ── Pagination Helpers ──────────────────────
|
|
|
|
func TestParsePaginationDefaults(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("GET", "/api/v1/channels", nil)
|
|
|
|
page, perPage, offset := parsePagination(c)
|
|
|
|
if page != 1 {
|
|
t.Errorf("Default page should be 1, got %d", page)
|
|
}
|
|
if perPage != 50 {
|
|
t.Errorf("Default per_page should be 50, got %d", perPage)
|
|
}
|
|
if offset != 0 {
|
|
t.Errorf("Default offset should be 0, got %d", offset)
|
|
}
|
|
}
|
|
|
|
func TestParsePaginationCustom(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("GET", "/api/v1/channels?page=3&per_page=10", nil)
|
|
|
|
page, perPage, offset := parsePagination(c)
|
|
|
|
if page != 3 {
|
|
t.Errorf("Page should be 3, got %d", page)
|
|
}
|
|
if perPage != 10 {
|
|
t.Errorf("Per page should be 10, got %d", perPage)
|
|
}
|
|
if offset != 20 {
|
|
t.Errorf("Offset should be 20, got %d", offset)
|
|
}
|
|
}
|
|
|
|
func TestParsePaginationClampMax(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("GET", "/api/v1/channels?per_page=500", nil)
|
|
|
|
_, perPage, _ := parsePagination(c)
|
|
|
|
if perPage != 100 {
|
|
t.Errorf("Per page should be clamped to 100, got %d", perPage)
|
|
}
|
|
}
|
|
|
|
// ── getUserID ───────────────────────────────
|
|
|
|
func TestGetUserID(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set("user_id", "abc-123")
|
|
|
|
uid := getUserID(c)
|
|
if uid != "abc-123" {
|
|
t.Errorf("Expected abc-123, got %s", uid)
|
|
}
|
|
}
|
|
|
|
func TestGetUserIDMissing(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
uid := getUserID(c)
|
|
if uid != "" {
|
|
t.Errorf("Expected empty string, got %s", uid)
|
|
}
|
|
}
|