All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Add auth-svc /validate endpoint for token checking Add chat-svc with auth client and Redis task queue Add worker-svc chat handler for task processing Co-Authored-By: Claude Code <claude@anthropic.com>
171 lines
4.8 KiB
Go
171 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.threesix.ai/jordan/sp4-debug-1770477266/pkg/auth"
|
|
"git.threesix.ai/jordan/sp4-debug-1770477266/pkg/logging"
|
|
"git.threesix.ai/jordan/sp4-debug-1770477266/pkg/queue"
|
|
"git.threesix.ai/jordan/sp4-debug-1770477266/services/chat-svc/internal/taskqueue"
|
|
)
|
|
|
|
// mockQueueProducer implements queue.Producer for testing.
|
|
type mockQueueProducer struct {
|
|
mu sync.Mutex
|
|
jobs []queue.Job
|
|
}
|
|
|
|
func (m *mockQueueProducer) Enqueue(ctx context.Context, jobType string, payload map[string]any) (string, error) {
|
|
return m.EnqueueWithOptions(ctx, queue.Job{
|
|
Type: jobType,
|
|
Payload: payload,
|
|
})
|
|
}
|
|
|
|
func (m *mockQueueProducer) EnqueueWithOptions(ctx context.Context, job queue.Job) (string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
job.ID = "mock-job-id"
|
|
m.jobs = append(m.jobs, job)
|
|
return job.ID, nil
|
|
}
|
|
|
|
func newTestChatHandler() (*Chat, *mockQueueProducer) {
|
|
mockQueue := &mockQueueProducer{}
|
|
producer := taskqueue.NewProducer(mockQueue, logging.Nop())
|
|
handler := NewChat(producer, logging.Nop())
|
|
return handler, mockQueue
|
|
}
|
|
|
|
func TestChat_Send_Success(t *testing.T) {
|
|
handler, mockQueue := newTestChatHandler()
|
|
|
|
r := chi.NewRouter()
|
|
r.Post("/api/chat-svc/send", func(w http.ResponseWriter, r *http.Request) {
|
|
// Inject authenticated user into context
|
|
ctx := auth.SetUser(r.Context(), &auth.User{ID: "user-123", Email: "test@example.com"})
|
|
r = r.WithContext(ctx)
|
|
|
|
if err := handler.Send(w, r); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
|
|
body, _ := json.Marshal(SendRequest{Message: "Hello world"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/chat-svc/send", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusAccepted {
|
|
t.Errorf("expected status 202, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("expected 'data' field in response")
|
|
}
|
|
if data["job_id"] != "mock-job-id" {
|
|
t.Errorf("expected job_id 'mock-job-id', got %v", data["job_id"])
|
|
}
|
|
if data["status"] != "queued" {
|
|
t.Errorf("expected status 'queued', got %v", data["status"])
|
|
}
|
|
|
|
// Verify job was enqueued
|
|
mockQueue.mu.Lock()
|
|
defer mockQueue.mu.Unlock()
|
|
if len(mockQueue.jobs) != 1 {
|
|
t.Fatalf("expected 1 enqueued job, got %d", len(mockQueue.jobs))
|
|
}
|
|
if mockQueue.jobs[0].Payload["user_id"] != "user-123" {
|
|
t.Errorf("expected user_id 'user-123' in payload, got %v", mockQueue.jobs[0].Payload["user_id"])
|
|
}
|
|
}
|
|
|
|
func TestChat_Send_NoAuth(t *testing.T) {
|
|
handler, _ := newTestChatHandler()
|
|
|
|
r := chi.NewRouter()
|
|
r.Post("/api/chat-svc/send", func(w http.ResponseWriter, r *http.Request) {
|
|
if err := handler.Send(w, r); err != nil {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
})
|
|
|
|
body, _ := json.Marshal(SendRequest{Message: "Hello"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/chat-svc/send", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status 401, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestChat_Send_NilProducer(t *testing.T) {
|
|
handler := NewChat(nil, logging.Nop())
|
|
|
|
r := chi.NewRouter()
|
|
r.Post("/api/chat-svc/send", func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := auth.SetUser(r.Context(), &auth.User{ID: "user-123"})
|
|
r = r.WithContext(ctx)
|
|
|
|
if err := handler.Send(w, r); err != nil {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
})
|
|
|
|
body, _ := json.Marshal(SendRequest{Message: "Hello"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/chat-svc/send", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("expected status 503, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestChat_Send_EmptyMessage(t *testing.T) {
|
|
handler, _ := newTestChatHandler()
|
|
|
|
r := chi.NewRouter()
|
|
r.Post("/api/chat-svc/send", func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := auth.SetUser(r.Context(), &auth.User{ID: "user-123"})
|
|
r = r.WithContext(ctx)
|
|
|
|
if err := handler.Send(w, r); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
})
|
|
|
|
body, _ := json.Marshal(map[string]string{"message": ""})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/chat-svc/send", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Empty message should fail validation (required,min=1)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|