This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/channels_test.go
2026-02-21 19:03:19 +00:00

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)
}
}