Changeset 0.7.2 (#40)

This commit is contained in:
2026-02-21 19:03:19 +00:00
parent 494b1aa981
commit 416e5439ea
28 changed files with 3813 additions and 138 deletions

View File

@@ -1,17 +1,16 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func init() {
gin.SetMode(gin.TestMode)
}
"git.gobha.me/xcaliber/chat-switchboard/database"
)
// ── Channel Request Validation ─────────────────
@@ -60,7 +59,7 @@ func TestUpdateChannelEmptyBody(t *testing.T) {
c.Request.Header.Set("Content-Type", "application/json")
// Without a DB connection, UpdateChannel will fail at ownership check.
// Integration tests with a real DB would validate the "no fields" path.
// 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)
@@ -122,19 +121,151 @@ func TestCreateMessageMissingContent(t *testing.T) {
}
}
// ── Integration: Message CRUD with Real DB ──────
func TestCreateMessageValidRoles(t *testing.T) {
// Valid roles should pass validation (not return 400).
// They'll fail at ownership check (no DB), which is expected.
t.Skip("requires database connection for ownership check")
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())
}
})
}
}
// ── Regenerate ───────────────────────────────
func TestChannelCRUDIntegration(t *testing.T) {
database.RequireTestDB(t)
database.TruncateAll(t)
func TestRegenerateRequiresDB(t *testing.T) {
// Regenerate is now a full handler (creates sibling assistant message
// via tree-aware streaming). It calls userOwnsChannel → database.DB,
// so it cannot run without a live database connection.
t.Skip("requires database connection for ownership check")
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 ──────────────────────