persona-community-5/services/persona-api/internal/service/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

261 lines
7.2 KiB
Go

package service
import (
"context"
"testing"
"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"
)
// mockProducer implements queue.Producer for testing.
type mockProducer struct {
jobs []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, struct {
jobType string
payload map[string]any
}{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, struct {
jobType string
payload map[string]any
}{jobType: job.Type, payload: job.Payload})
return "test-job-id", nil
}
func TestPersonaService_Create(t *testing.T) {
repo := memory.NewPersonaRepository()
q := &mockProducer{}
svc := NewPersonaService(repo, q, nil, logging.Nop())
t.Run("creates persona with custom name", func(t *testing.T) {
persona, err := svc.Create(context.Background(), "mysterious woman with dark hair", "woman", "Luna Shadow")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if persona.Name != "Luna Shadow" {
t.Errorf("expected name 'Luna Shadow', got '%s'", persona.Name)
}
if persona.ID.IsZero() {
t.Error("expected non-empty ID")
}
if persona.Gender != "woman" {
t.Errorf("expected gender 'woman', got '%s'", persona.Gender)
}
if persona.Status != domain.PersonaStatusPending {
t.Errorf("expected status 'pending', got '%s'", persona.Status)
}
if persona.Handle == "" {
t.Error("expected non-empty handle")
}
})
t.Run("creates persona with generated name", func(t *testing.T) {
persona, err := svc.Create(context.Background(), "energetic man who loves music", "man", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if persona.Name == "" {
t.Error("expected auto-generated name")
}
if persona.ID.IsZero() {
t.Error("expected non-empty ID")
}
})
t.Run("enqueues generate_spec job", func(t *testing.T) {
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 _, ok := j.payload["persona_id"]; !ok {
t.Error("expected persona_id in job payload")
}
}
}
if !found {
t.Error("expected generate_spec job to be enqueued")
}
})
t.Run("initializes empty slices", func(t *testing.T) {
persona, err := svc.Create(context.Background(), "test persona for slices", "non_binary", "SliceTest")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if persona.Tags == nil {
t.Error("expected Tags to be initialized (not nil)")
}
if persona.ImageURLs == nil {
t.Error("expected ImageURLs to be initialized (not nil)")
}
if persona.VideoURLs == nil {
t.Error("expected VideoURLs to be initialized (not nil)")
}
})
}
func TestPersonaService_GetByID(t *testing.T) {
repo := memory.NewPersonaRepository()
q := &mockProducer{}
svc := NewPersonaService(repo, q, nil, logging.Nop())
created, _ := svc.Create(context.Background(), "test persona", "woman", "GetTest")
t.Run("returns existing persona", func(t *testing.T) {
persona, err := svc.GetByID(context.Background(), created.ID)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if persona.Name != "GetTest" {
t.Errorf("expected name 'GetTest', got '%s'", persona.Name)
}
})
t.Run("returns not found for missing persona", func(t *testing.T) {
_, err := svc.GetByID(context.Background(), "nonexistent-id")
if err != domain.ErrPersonaNotFound {
t.Errorf("expected ErrPersonaNotFound, got %v", err)
}
})
}
func TestPersonaService_List(t *testing.T) {
repo := memory.NewPersonaRepository()
q := &mockProducer{}
svc := NewPersonaService(repo, q, nil, logging.Nop())
t.Run("returns empty list initially", func(t *testing.T) {
personas, err := svc.List(context.Background(), 20, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(personas) != 0 {
t.Errorf("expected 0 personas, got %d", len(personas))
}
})
// Create some personas
_, _ = svc.Create(context.Background(), "persona one", "woman", "ListOne")
_, _ = svc.Create(context.Background(), "persona two", "man", "ListTwo")
_, _ = svc.Create(context.Background(), "persona three", "non_binary", "ListThree")
t.Run("returns all personas", func(t *testing.T) {
personas, err := svc.List(context.Background(), 20, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(personas) != 3 {
t.Errorf("expected 3 personas, got %d", len(personas))
}
})
t.Run("respects limit", func(t *testing.T) {
personas, err := svc.List(context.Background(), 2, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(personas) != 2 {
t.Errorf("expected 2 personas, got %d", len(personas))
}
})
t.Run("clamps negative limit to default", func(t *testing.T) {
personas, err := svc.List(context.Background(), -1, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(personas) != 3 {
t.Errorf("expected 3 personas (default limit), got %d", len(personas))
}
})
t.Run("clamps limit above 100", func(t *testing.T) {
personas, err := svc.List(context.Background(), 200, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(personas) != 3 {
t.Errorf("expected 3 personas (all within max limit), got %d", len(personas))
}
})
t.Run("respects offset", func(t *testing.T) {
personas, err := svc.List(context.Background(), 20, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(personas) != 1 {
t.Errorf("expected 1 persona, got %d", len(personas))
}
})
}
func TestGenerateHandle(t *testing.T) {
tests := []struct {
name string
input string
}{
{name: "simple name", input: "Luna Shadow"},
{name: "special chars", input: "DJ Beats!@#"},
{name: "unicode", input: "Café Noir"},
{name: "long name", input: "This Is A Very Long Name That Exceeds The Maximum Handle Length Allowed"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handle := generateHandle(tt.input)
if handle == "" {
t.Error("expected non-empty handle")
}
if len(handle) > 50 {
t.Errorf("handle too long: %d chars", len(handle))
}
})
}
}
func TestGeneratePersonaName(t *testing.T) {
tests := []struct {
name string
desc string
check func(string) bool
}{
{
name: "short description",
desc: "mysterious woman",
check: func(s string) bool { return s != "" },
},
{
name: "long description uses first 3 words",
desc: "mysterious woman with dark hair who loves poetry",
check: func(s string) bool { return s != "" && len(s) <= 50 },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name := generatePersonaName(tt.desc)
if !tt.check(name) {
t.Errorf("unexpected name for desc %q: %q", tt.desc, name)
}
})
}
}