persona-community-5/services/persona-api/internal/api/handlers/persona_test.go
rdev-worker 9c009926d1
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /implement-feature persona-model --requirements 'DB migration in pers...
2026-02-24 07:58:27 +00:00

323 lines
8.0 KiB
Go

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))
}
})
}