- store: new chat_sessions/chat_messages tables with CRUD, ILIKE message search, and auto-titling from the first user message - context: optional Persister on the sliding-window store — messages are saved asynchronously on Add, sessions not in memory are restored from the database on Get - orchestrator: ProcessInput/FinishRound now carry userID so persisted messages have an owner - api: real session handlers (list/create/delete), message history and clear endpoints, working message search and three-format export (markdown/txt/json), fixing the short-ID panic in export - main: migration, store wiring, ctxStore persister adapter Tests: chat store handler lifecycle (fake store), context persister save/restore, stub auth login flow updated.
137 lines
3.3 KiB
Go
137 lines
3.3 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestHealth(t *testing.T) {
|
|
r := New(Dependencies{AuthMgr: NewStubAuthManager()})
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/api/v1/health")
|
|
if err != nil {
|
|
t.Fatalf("GET health: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestLoginAndProtectedEndpoint(t *testing.T) {
|
|
auth := NewStubAuthManager()
|
|
r := New(Dependencies{AuthMgr: auth, ChatStore: newFakeChatStore()})
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
// Login
|
|
resp, err := http.Post(srv.URL+"/api/v1/auth/login", "application/json",
|
|
strings.NewReader(`{"username":"admin","password":"admin"}`))
|
|
if err != nil {
|
|
t.Fatalf("POST login: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("login: expected 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
// Use token for protected endpoint
|
|
req, _ := http.NewRequest("GET", srv.URL+"/api/v1/sessions", nil)
|
|
req.Header.Set("Authorization", "Bearer stub-token-admin")
|
|
resp2, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("GET sessions: %v", err)
|
|
}
|
|
resp2.Body.Close()
|
|
|
|
if resp2.StatusCode != http.StatusOK {
|
|
t.Errorf("sessions: expected 200, got %d", resp2.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestLoginWrongPassword(t *testing.T) {
|
|
auth := NewStubAuthManager()
|
|
r := New(Dependencies{AuthMgr: auth})
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Post(srv.URL+"/api/v1/auth/login", "application/json",
|
|
strings.NewReader(`{"username":"admin","password":"wrong"}`))
|
|
if err != nil {
|
|
t.Fatalf("POST login: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestProtectedWithoutToken(t *testing.T) {
|
|
auth := NewStubAuthManager()
|
|
r := New(Dependencies{AuthMgr: auth})
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/api/v1/sessions")
|
|
if err != nil {
|
|
t.Fatalf("GET sessions: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestStubAuthManager(t *testing.T) {
|
|
m := NewStubAuthManager()
|
|
|
|
// Login as admin
|
|
token, err := m.Login("admin", "admin")
|
|
if err != nil {
|
|
t.Fatalf("Login: %v", err)
|
|
}
|
|
|
|
uid, uname, role, err := m.ValidateToken(token)
|
|
if err != nil {
|
|
t.Fatalf("ValidateToken: %v", err)
|
|
}
|
|
if uid != "admin" || uname != "admin" || role != "admin" {
|
|
t.Errorf("wrong claims: %s/%s/%s", uid, uname, role)
|
|
}
|
|
|
|
// Invalid token
|
|
_, _, _, err = m.ValidateToken("bogus")
|
|
if err == nil {
|
|
t.Error("expected error for bogus token")
|
|
}
|
|
}
|
|
|
|
func TestCORSHeaders(t *testing.T) {
|
|
r := New(Dependencies{AuthMgr: NewStubAuthManager()})
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
req, _ := http.NewRequest("OPTIONS", srv.URL+"/api/v1/health", nil)
|
|
req.Header.Set("Origin", "http://localhost:5173")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("OPTIONS: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
t.Errorf("expected 204 for OPTIONS, got %d", resp.StatusCode)
|
|
}
|
|
if resp.Header.Get("Access-Control-Allow-Origin") == "" {
|
|
t.Error("expected CORS header")
|
|
}
|
|
}
|