package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/persona-community-5/pkg/app" "git.threesix.ai/jordan/persona-community-5/pkg/logging" "git.threesix.ai/jordan/persona-community-5/pkg/queue" "git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/adapter/memory" "git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain" "git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/service" ) // mockProducer implements queue.Producer for testing. type mockProducer struct { jobs []mockJob } type mockJob struct { jobType string payload map[string]any } var _ queue.Producer = (*mockProducer)(nil) func (m *mockProducer) Enqueue(_ context.Context, jobType string, payload map[string]any) (string, error) { m.jobs = append(m.jobs, mockJob{jobType: jobType, payload: payload}) return "test-job-id", nil } func (m *mockProducer) EnqueueWithOptions(_ context.Context, job queue.Job) (string, error) { m.jobs = append(m.jobs, mockJob{jobType: job.Type, payload: job.Payload}) return "test-job-id", nil } func newTestPersonaHandler() (*Persona, *memory.PersonaRepository, *mockProducer) { repo := memory.NewPersonaRepository() q := &mockProducer{} svc := service.NewPersonaService(repo, q, nil, logging.Nop()) handler := NewPersona(svc, logging.Nop()) return handler, repo, q } func TestPersona_Create(t *testing.T) { handler, _, q := newTestPersonaHandler() tests := []struct { name string body any wantStatus int }{ { name: "valid request", body: CreatePersonaRequest{ Description: "mysterious woman with dark hair", Gender: "woman", }, wantStatus: http.StatusAccepted, }, { name: "valid request with custom name", body: CreatePersonaRequest{ Description: "energetic man who loves music", Gender: "man", CustomName: "DJ Beats", }, wantStatus: http.StatusAccepted, }, { name: "empty body", body: nil, wantStatus: http.StatusBadRequest, }, { name: "missing description", body: map[string]string{ "gender": "woman", }, wantStatus: http.StatusBadRequest, }, { name: "invalid gender", body: map[string]string{ "description": "a persona", "gender": "invalid", }, wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Post("/personas", app.Wrap(handler.Create)) var body []byte if tt.body != nil { var err error body, err = json.Marshal(tt.body) if err != nil { t.Fatalf("failed to marshal body: %v", err) } } req := httptest.NewRequest(http.MethodPost, "/personas", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d; body: %s", tt.wantStatus, w.Code, w.Body.String()) } }) } // Verify a generate_spec job was enqueued found := false for _, j := range q.jobs { if j.jobType == "generate_spec" { found = true if j.payload["stage"] != "spec" { t.Errorf("expected stage 'spec', got %v", j.payload["stage"]) } } } if !found { t.Error("expected generate_spec job to be enqueued") } } func TestPersona_GetByID(t *testing.T) { handler, repo, _ := newTestPersonaHandler() // Seed data persona := &domain.Persona{ Name: "Test Persona", Handle: "test_persona", Gender: "woman", Description: "A test persona", Tags: []string{}, ImageURLs: []string{}, VideoURLs: []string{}, Status: domain.PersonaStatusPending, } _ = repo.Create(context.Background(), persona) seededID := persona.ID.String() tests := []struct { name string id string wantStatus int }{ { name: "existing persona", id: seededID, wantStatus: http.StatusOK, }, { name: "not found", id: "550e8400-e29b-41d4-a716-446655440099", wantStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Get("/personas/{id}", app.Wrap(handler.GetByID)) req := httptest.NewRequest(http.MethodGet, "/personas/"+tt.id, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d; body: %s", tt.wantStatus, w.Code, w.Body.String()) } if tt.wantStatus == http.StatusOK { 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["id"] != seededID { t.Errorf("expected id %s, got %v", seededID, data["id"]) } if data["name"] != "Test Persona" { t.Errorf("expected name 'Test Persona', got %v", data["name"]) } } }) } } func TestPersona_List(t *testing.T) { handler, repo, _ := newTestPersonaHandler() t.Run("empty list", func(t *testing.T) { r := chi.NewRouter() r.Get("/personas", app.Wrap(handler.List)) req := httptest.NewRequest(http.MethodGet, "/personas", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } 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"].([]any) if !ok { t.Fatal("expected 'data' to be an array") } if len(data) != 0 { t.Errorf("expected 0 items, got %d", len(data)) } }) // Seed data for i, name := range []string{"Persona A", "Persona B", "Persona C"} { p := &domain.Persona{ Name: name, Handle: name + "_handle", Gender: "woman", Description: "description " + string(rune('A'+i)), Tags: []string{}, ImageURLs: []string{}, VideoURLs: []string{}, Status: domain.PersonaStatusPending, } _ = repo.Create(context.Background(), p) } t.Run("returns all personas", func(t *testing.T) { r := chi.NewRouter() r.Get("/personas", app.Wrap(handler.List)) req := httptest.NewRequest(http.MethodGet, "/personas", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } 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"].([]any) if !ok { t.Fatal("expected 'data' to be an array") } if len(data) != 3 { t.Errorf("expected 3 items, got %d", len(data)) } }) t.Run("respects limit parameter", func(t *testing.T) { r := chi.NewRouter() r.Get("/personas", app.Wrap(handler.List)) req := httptest.NewRequest(http.MethodGet, "/personas?limit=2", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } 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"].([]any) if !ok { t.Fatal("expected 'data' to be an array") } if len(data) != 2 { t.Errorf("expected 2 items, got %d", len(data)) } }) t.Run("respects offset parameter", func(t *testing.T) { r := chi.NewRouter() r.Get("/personas", app.Wrap(handler.List)) req := httptest.NewRequest(http.MethodGet, "/personas?offset=2", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } 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"].([]any) if !ok { t.Fatal("expected 'data' to be an array") } if len(data) != 1 { t.Errorf("expected 1 item, got %d", len(data)) } }) }