250 lines
6.6 KiB
Go
250 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/config"
|
|
"git.gobha.me/xcaliber/chat-switchboard/handlers"
|
|
"git.gobha.me/xcaliber/chat-switchboard/middleware"
|
|
"git.gobha.me/xcaliber/chat-switchboard/providers"
|
|
)
|
|
|
|
func init() {
|
|
gin.SetMode(gin.TestMode)
|
|
providers.Init()
|
|
}
|
|
|
|
func TestHealthEndpoint(t *testing.T) {
|
|
r := gin.New()
|
|
r.GET("/health", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok", "version": "test", "database": false})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/health", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
}
|
|
|
|
func TestCORSHeaders(t *testing.T) {
|
|
r := gin.New()
|
|
r.Use(middleware.CORS())
|
|
r.GET("/test", func(c *gin.Context) { c.Status(200) })
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("OPTIONS", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("Expected %d for OPTIONS, got %d", http.StatusNoContent, w.Code)
|
|
}
|
|
if w.Header().Get("Access-Control-Allow-Origin") != "*" {
|
|
t.Error("Missing CORS header")
|
|
}
|
|
}
|
|
|
|
func TestAllRoutesRegistered(t *testing.T) {
|
|
cfg := &config.Config{JWTSecret: "test"}
|
|
auth := handlers.NewAuthHandler(cfg)
|
|
chats := handlers.NewChatHandler()
|
|
msgs := handlers.NewMessageHandler()
|
|
comp := handlers.NewCompletionHandler()
|
|
apiCfg := handlers.NewAPIConfigHandler()
|
|
settings := handlers.NewSettingsHandler()
|
|
adm := handlers.NewAdminHandler()
|
|
|
|
r := gin.New()
|
|
api := r.Group("/api/v1")
|
|
|
|
authGroup := api.Group("/auth")
|
|
{
|
|
authGroup.POST("/register", auth.Register)
|
|
authGroup.POST("/login", auth.Login)
|
|
authGroup.POST("/refresh", auth.Refresh)
|
|
authGroup.POST("/logout", auth.Logout)
|
|
}
|
|
|
|
protected := api.Group("")
|
|
{
|
|
// Chats
|
|
protected.GET("/chats", chats.ListChats)
|
|
protected.POST("/chats", chats.CreateChat)
|
|
protected.GET("/chats/:id", chats.GetChat)
|
|
protected.PUT("/chats/:id", chats.UpdateChat)
|
|
protected.DELETE("/chats/:id", chats.DeleteChat)
|
|
|
|
// Messages
|
|
protected.GET("/chats/:id/messages", msgs.ListMessages)
|
|
protected.POST("/chats/:id/messages", msgs.CreateMessage)
|
|
protected.POST("/chats/:id/regenerate", msgs.Regenerate)
|
|
|
|
// Chat Engine
|
|
protected.POST("/chat/completions", comp.Complete)
|
|
|
|
// API Configs
|
|
protected.GET("/api-configs", apiCfg.ListConfigs)
|
|
protected.POST("/api-configs", apiCfg.CreateConfig)
|
|
protected.GET("/api-configs/:id", apiCfg.GetConfig)
|
|
protected.PUT("/api-configs/:id", apiCfg.UpdateConfig)
|
|
protected.DELETE("/api-configs/:id", apiCfg.DeleteConfig)
|
|
protected.GET("/api-configs/:id/models", apiCfg.ListModels)
|
|
protected.GET("/models", apiCfg.ListAllModels)
|
|
|
|
// User Settings & Profile
|
|
protected.GET("/profile", settings.GetProfile)
|
|
protected.PUT("/profile", settings.UpdateProfile)
|
|
protected.POST("/profile/password", settings.ChangePassword)
|
|
protected.GET("/settings", settings.GetSettings)
|
|
protected.PUT("/settings", settings.UpdateSettings)
|
|
}
|
|
|
|
// Admin routes
|
|
admin := api.Group("/admin")
|
|
{
|
|
admin.GET("/users", adm.ListUsers)
|
|
admin.POST("/users", adm.CreateUser)
|
|
admin.PUT("/users/:id/role", adm.UpdateUserRole)
|
|
admin.PUT("/users/:id/active", adm.ToggleUserActive)
|
|
admin.POST("/users/:id/reset-password", adm.ResetPassword)
|
|
admin.DELETE("/users/:id", adm.DeleteUser)
|
|
admin.GET("/settings", adm.ListGlobalSettings)
|
|
admin.GET("/settings/:key", adm.GetGlobalSetting)
|
|
admin.PUT("/settings/:key", adm.UpdateGlobalSetting)
|
|
admin.GET("/stats", adm.GetStats)
|
|
}
|
|
|
|
routes := r.Routes()
|
|
routePaths := make(map[string]bool)
|
|
for _, route := range routes {
|
|
routePaths[route.Method+" "+route.Path] = true
|
|
}
|
|
|
|
expected := []string{
|
|
// Auth
|
|
"POST /api/v1/auth/register",
|
|
"POST /api/v1/auth/login",
|
|
"POST /api/v1/auth/refresh",
|
|
"POST /api/v1/auth/logout",
|
|
// Chats
|
|
"GET /api/v1/chats",
|
|
"POST /api/v1/chats",
|
|
"GET /api/v1/chats/:id",
|
|
"PUT /api/v1/chats/:id",
|
|
"DELETE /api/v1/chats/:id",
|
|
// Messages
|
|
"GET /api/v1/chats/:id/messages",
|
|
"POST /api/v1/chats/:id/messages",
|
|
"POST /api/v1/chats/:id/regenerate",
|
|
// Chat Engine
|
|
"POST /api/v1/chat/completions",
|
|
// API Configs
|
|
"GET /api/v1/api-configs",
|
|
"POST /api/v1/api-configs",
|
|
"GET /api/v1/api-configs/:id",
|
|
"PUT /api/v1/api-configs/:id",
|
|
"DELETE /api/v1/api-configs/:id",
|
|
"GET /api/v1/api-configs/:id/models",
|
|
// Models
|
|
"GET /api/v1/models",
|
|
// Profile & Settings
|
|
"GET /api/v1/profile",
|
|
"PUT /api/v1/profile",
|
|
"POST /api/v1/profile/password",
|
|
"GET /api/v1/settings",
|
|
"PUT /api/v1/settings",
|
|
// Admin
|
|
"GET /api/v1/admin/users",
|
|
"POST /api/v1/admin/users",
|
|
"PUT /api/v1/admin/users/:id/role",
|
|
"PUT /api/v1/admin/users/:id/active",
|
|
"POST /api/v1/admin/users/:id/reset-password",
|
|
"DELETE /api/v1/admin/users/:id",
|
|
"GET /api/v1/admin/settings",
|
|
"GET /api/v1/admin/settings/:key",
|
|
"PUT /api/v1/admin/settings/:key",
|
|
"GET /api/v1/admin/stats",
|
|
}
|
|
|
|
for _, e := range expected {
|
|
if !routePaths[e] {
|
|
t.Errorf("Missing route: %s", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProviderRegistry(t *testing.T) {
|
|
ids := providers.List()
|
|
if len(ids) < 2 {
|
|
t.Errorf("Expected at least 2 providers, got %d", len(ids))
|
|
}
|
|
|
|
// OpenAI should be registered
|
|
p, err := providers.Get("openai")
|
|
if err != nil {
|
|
t.Errorf("OpenAI provider not found: %v", err)
|
|
}
|
|
if p.ID() != "openai" {
|
|
t.Errorf("Expected ID 'openai', got '%s'", p.ID())
|
|
}
|
|
|
|
// Anthropic should be registered
|
|
p, err = providers.Get("anthropic")
|
|
if err != nil {
|
|
t.Errorf("Anthropic provider not found: %v", err)
|
|
}
|
|
if p.ID() != "anthropic" {
|
|
t.Errorf("Expected ID 'anthropic', got '%s'", p.ID())
|
|
}
|
|
|
|
// Unknown should fail
|
|
_, err = providers.Get("nonexistent")
|
|
if err == nil {
|
|
t.Error("Expected error for unknown provider")
|
|
}
|
|
}
|
|
|
|
func TestAnthropicStaticModels(t *testing.T) {
|
|
p := &providers.AnthropicProvider{}
|
|
models, err := p.ListModels(nil, providers.ProviderConfig{})
|
|
if err != nil {
|
|
t.Errorf("ListModels should not fail: %v", err)
|
|
}
|
|
if len(models) == 0 {
|
|
t.Error("Expected some static models")
|
|
}
|
|
|
|
// Check that known models are present
|
|
found := false
|
|
for _, m := range models {
|
|
if m.ID == "claude-sonnet-4-20250514" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Expected claude-sonnet-4 in static model list")
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddlewareNoDatabase(t *testing.T) {
|
|
cfg := &config.Config{JWTSecret: "test"}
|
|
r := gin.New()
|
|
r.Use(middleware.Auth(cfg))
|
|
r.GET("/protected", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/protected", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected pass-through with no DB, got %d", w.Code)
|
|
}
|
|
}
|