789e32a57f
Add test cases for Save, GetByActorID, ListByStoryID, Delete, and GetActorIDForResourceName functions. Use real database instances for integration tests following Story test patterns.
591 lines
14 KiB
Go
591 lines
14 KiB
Go
// Package actor provides database access for the Actor resource.
|
|
package actor
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
v1 "git.tipsy.codes/charles/webstory/pkg/api/webstory/v1"
|
|
"git.tipsy.codes/charles/webstory/pkg/database/schema"
|
|
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
func TestActorTranslator_FromAPI(t *testing.T) {
|
|
now := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
apiActor *v1.Actor
|
|
expectErr bool
|
|
errMsg string
|
|
expectID int64
|
|
checkFunc func(*DBActor) error
|
|
}{
|
|
{
|
|
name: "nil actor returns error",
|
|
apiActor: nil,
|
|
expectErr: true,
|
|
errMsg: "api actor cannot be nil",
|
|
},
|
|
{
|
|
name: "minimal valid actor creates translator correctly",
|
|
apiActor: &v1.Actor{
|
|
ActorId: "actor-1",
|
|
NameValue: "John Doe",
|
|
Role: "Hero",
|
|
Notes: "Main character",
|
|
CreateTime: timestamppb.New(now),
|
|
UpdateTime: timestamppb.New(now),
|
|
},
|
|
expectErr: false,
|
|
expectID: 0,
|
|
checkFunc: func(s *DBActor) error {
|
|
if s.ActorID != "actor-1" {
|
|
return fmt.Errorf("expected ActorID actor-1, got %s", s.ActorID)
|
|
}
|
|
if s.NameValue != "John Doe" {
|
|
return fmt.Errorf("expected NameValue John Doe, got %s", s.NameValue)
|
|
}
|
|
if s.Role != "Hero" {
|
|
return fmt.Errorf("expected Role Hero, got %s", s.Role)
|
|
}
|
|
if s.Notes != "Main character" {
|
|
return fmt.Errorf("expected Notes Main character, got %s", s.Notes)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
trans := NewActorTranslator()
|
|
result, err := trans.FromAPI(tt.apiActor)
|
|
|
|
if tt.expectErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error but got nil")
|
|
}
|
|
if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
|
t.Errorf("expected error containing '%s' but got '%s'", tt.errMsg, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result == nil {
|
|
t.Fatalf("expected non-nil result")
|
|
}
|
|
|
|
if result.ID != tt.expectID {
|
|
t.Errorf("expected ID %d but got %d", tt.expectID, result.ID)
|
|
}
|
|
|
|
if tt.checkFunc != nil {
|
|
if err := tt.checkFunc(result); err != nil {
|
|
t.Errorf("check failed: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActorTranslator_ToAPI(t *testing.T) {
|
|
now := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
dbActor *DBActor
|
|
checkFunc func(*v1.Actor) error
|
|
}{
|
|
{
|
|
name: "nil dbActor returns nil",
|
|
dbActor: nil,
|
|
checkFunc: func(a *v1.Actor) error {
|
|
if a != nil {
|
|
return fmt.Errorf("expected nil, got %v", a)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "minimal dbActor converts correctly",
|
|
dbActor: &DBActor{
|
|
ID: 1,
|
|
StoryID: 100,
|
|
ActorID: "actor-1",
|
|
NameValue: "John Doe",
|
|
Role: "Hero",
|
|
Notes: "Main character",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Etag: "etag-123",
|
|
},
|
|
checkFunc: func(a *v1.Actor) error {
|
|
if a.ActorId != "actor-1" {
|
|
return fmt.Errorf("expected ActorId actor-1, got %s", a.ActorId)
|
|
}
|
|
if a.NameValue != "John Doe" {
|
|
return fmt.Errorf("expected NameValue John Doe, got %s", a.NameValue)
|
|
}
|
|
if a.Role != "Hero" {
|
|
return fmt.Errorf("expected Role Hero, got %s", a.Role)
|
|
}
|
|
if a.Notes != "Main character" {
|
|
return fmt.Errorf("expected Notes Main character, got %s", a.Notes)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "dbActor with story_id and actor_id creates correct name",
|
|
dbActor: &DBActor{
|
|
ID: 2,
|
|
StoryID: 200,
|
|
ActorID: "actor-2",
|
|
NameValue: "Jane Smith",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
checkFunc: func(a *v1.Actor) error {
|
|
if a.Name != "stories/200/actors/actor-2" {
|
|
return fmt.Errorf("expected name stories/200/actors/actor-2, got %s", a.Name)
|
|
}
|
|
if a.ActorId != "actor-2" {
|
|
return fmt.Errorf("expected ActorId actor-2, got %s", a.ActorId)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "dbActor with zero story_id does not create name",
|
|
dbActor: &DBActor{
|
|
ID: 3,
|
|
StoryID: 0,
|
|
ActorID: "actor-3",
|
|
NameValue: "Test Actor",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
checkFunc: func(a *v1.Actor) error {
|
|
if a.Name != "" {
|
|
return fmt.Errorf("expected empty name, got %s", a.Name)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
trans := NewActorTranslator()
|
|
result := trans.ToAPI(tt.dbActor)
|
|
|
|
if tt.checkFunc != nil {
|
|
if err := tt.checkFunc(result); err != nil {
|
|
t.Errorf("check failed: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDBActor_Save(t *testing.T) {
|
|
server := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().Port(5432))
|
|
err := server.Start()
|
|
if err != nil {
|
|
t.Fatalf("failed to start embedded postgres: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := server.Stop(); err != nil {
|
|
t.Errorf("failed to stop embedded postgres: %v", err)
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
db, err := sql.Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable")
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = db.ExecContext(ctx, string(schema.Bytes))
|
|
if err != nil {
|
|
t.Fatalf("failed to run schema: %v", err)
|
|
}
|
|
|
|
actor := &DBActor{
|
|
StoryID: 1,
|
|
ActorID: "save-test1",
|
|
NameValue: "Save Test",
|
|
Role: "Test Role",
|
|
Notes: "Test notes",
|
|
}
|
|
|
|
err = actor.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
if actor.ID <= 0 {
|
|
t.Errorf("expected positive ID, got %d", actor.ID)
|
|
}
|
|
|
|
var dbActor DBActor
|
|
err = db.QueryRow(
|
|
`SELECT id, story_id, actor_id, name_value, role, notes, created_at, updated_at, etag FROM actors WHERE id = $1`,
|
|
actor.ID,
|
|
).Scan(
|
|
&dbActor.ID, &dbActor.StoryID, &dbActor.ActorID, &dbActor.NameValue,
|
|
&dbActor.Role, &dbActor.Notes, &dbActor.CreatedAt, &dbActor.UpdatedAt, &dbActor.Etag,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("failed to query actor: %v", err)
|
|
}
|
|
|
|
if dbActor.ActorID != actor.ActorID {
|
|
t.Errorf("actor_id mismatch: expected %s, got %s", actor.ActorID, dbActor.ActorID)
|
|
}
|
|
if dbActor.NameValue != actor.NameValue {
|
|
t.Errorf("name_value mismatch: expected %s, got %s", actor.NameValue, dbActor.NameValue)
|
|
}
|
|
if dbActor.Role != actor.Role {
|
|
t.Errorf("role mismatch: expected %s, got %s", actor.Role, dbActor.Role)
|
|
}
|
|
if dbActor.Notes != actor.Notes {
|
|
t.Errorf("notes mismatch: expected %s, got %s", actor.Notes, dbActor.Notes)
|
|
}
|
|
}
|
|
|
|
func TestDBActor_GetByID(t *testing.T) {
|
|
server := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().Port(5432))
|
|
err := server.Start()
|
|
if err != nil {
|
|
t.Fatalf("failed to start embedded postgres: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := server.Stop(); err != nil {
|
|
t.Errorf("failed to stop embedded postgres: %v", err)
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
db, err := sql.Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable")
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = db.ExecContext(ctx, string(schema.Bytes))
|
|
if err != nil {
|
|
t.Fatalf("failed to run schema: %v", err)
|
|
}
|
|
|
|
actor := &DBActor{
|
|
StoryID: 1,
|
|
ActorID: "get-by-id-test",
|
|
NameValue: "Get Test",
|
|
}
|
|
err = actor.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
result, err := GetByID(db, actor.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetByID failed: %v", err)
|
|
}
|
|
|
|
if result.ID != actor.ID {
|
|
t.Errorf("ID mismatch: expected %d, got %d", actor.ID, result.ID)
|
|
}
|
|
if result.NameValue != actor.NameValue {
|
|
t.Errorf("name_value mismatch: expected %s, got %s", actor.NameValue, result.NameValue)
|
|
}
|
|
}
|
|
|
|
func TestDBActor_GetByActorID(t *testing.T) {
|
|
server := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().Port(5432))
|
|
err := server.Start()
|
|
if err != nil {
|
|
t.Fatalf("failed to start embedded postgres: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := server.Stop(); err != nil {
|
|
t.Errorf("failed to stop embedded postgres: %v", err)
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
db, err := sql.Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable")
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = db.ExecContext(ctx, string(schema.Bytes))
|
|
if err != nil {
|
|
t.Fatalf("failed to run schema: %v", err)
|
|
}
|
|
|
|
actor := &DBActor{
|
|
StoryID: 1,
|
|
ActorID: "get-by-actor-id-test",
|
|
NameValue: "Get By Actor ID Test",
|
|
}
|
|
err = actor.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
result, err := GetByActorID(db, actor.StoryID, actor.ActorID)
|
|
if err != nil {
|
|
t.Fatalf("GetByActorID failed: %v", err)
|
|
}
|
|
|
|
if result.NameValue != actor.NameValue {
|
|
t.Errorf("name_value mismatch: expected %s, got %s", actor.NameValue, result.NameValue)
|
|
}
|
|
}
|
|
|
|
func TestDBActor_ListByStoryID(t *testing.T) {
|
|
server := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().Port(5432))
|
|
err := server.Start()
|
|
if err != nil {
|
|
t.Fatalf("failed to start embedded postgres: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := server.Stop(); err != nil {
|
|
t.Errorf("failed to stop embedded postgres: %v", err)
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
db, err := sql.Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable")
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = db.ExecContext(ctx, string(schema.Bytes))
|
|
if err != nil {
|
|
t.Fatalf("failed to run schema: %v", err)
|
|
}
|
|
|
|
actor1 := &DBActor{
|
|
StoryID: 1,
|
|
ActorID: "list-test-1",
|
|
NameValue: "Actor One",
|
|
Role: "Role One",
|
|
}
|
|
actor2 := &DBActor{
|
|
StoryID: 1,
|
|
ActorID: "list-test-2",
|
|
NameValue: "Actor Two",
|
|
Role: "Role Two",
|
|
}
|
|
err = actor1.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
err = actor2.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
actors, err := ListByStoryID(db, 1)
|
|
if err != nil {
|
|
t.Fatalf("ListByStoryID failed: %v", err)
|
|
}
|
|
|
|
if len(actors) != 2 {
|
|
t.Errorf("expected 2 actors, got %d", len(actors))
|
|
}
|
|
|
|
// Verify they're sorted alphabetically
|
|
if actors[0].NameValue != "Actor One" || actors[1].NameValue != "Actor Two" {
|
|
t.Errorf("actors are not in alphabetical order")
|
|
}
|
|
}
|
|
|
|
func TestDBActor_Delete(t *testing.T) {
|
|
server := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().Port(5432))
|
|
err := server.Start()
|
|
if err != nil {
|
|
t.Fatalf("failed to start embedded postgres: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := server.Stop(); err != nil {
|
|
t.Errorf("failed to stop embedded postgres: %v", err)
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
db, err := sql.Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable")
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = db.ExecContext(ctx, string(schema.Bytes))
|
|
if err != nil {
|
|
t.Fatalf("failed to run schema: %v", err)
|
|
}
|
|
|
|
actor := &DBActor{
|
|
StoryID: 1,
|
|
ActorID: "delete-test-1",
|
|
NameValue: "Delete Test",
|
|
}
|
|
err = actor.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
// Verify actor exists
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM actors WHERE id = $1", actor.ID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to count actors: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 actor, got %d", count)
|
|
}
|
|
|
|
err = Delete(db, actor.ID)
|
|
if err != nil {
|
|
t.Fatalf("Delete failed: %v", err)
|
|
}
|
|
|
|
// Verify actor is deleted
|
|
err = db.QueryRow("SELECT COUNT(*) FROM actors WHERE id = $1", actor.ID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to count actors after deletion: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Errorf("expected 0 actors after deletion, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestDBActor_GetActorIDForResourceName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
storyID int64
|
|
actorID string
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "valid story/actor resource name",
|
|
input: "stories/1/actors/my-actor",
|
|
storyID: 1,
|
|
actorID: "my-actor",
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "valid story/actor resource name with numbers",
|
|
input: "stories/100/actors/actor-123",
|
|
storyID: 100,
|
|
actorID: "actor-123",
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "missing actors/ prefix",
|
|
input: "stories/1/actor/my-actor",
|
|
storyID: 1,
|
|
actorID: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "actor_id with underscore not allowed",
|
|
input: "stories/1/actors/my_actor",
|
|
storyID: 1,
|
|
actorID: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "actor_id with spaces not allowed",
|
|
input: "stories/1/actors/my actor",
|
|
storyID: 1,
|
|
actorID: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "actor_id with uppercase not allowed",
|
|
input: "stories/1/actors/MyActor",
|
|
storyID: 1,
|
|
actorID: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "actor_id with dots not allowed",
|
|
input: "stories/1/actors/my.actor",
|
|
storyID: 1,
|
|
actorID: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "missing stories/ prefix",
|
|
input: "actors/my-actor",
|
|
storyID: 1,
|
|
actorID: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "empty actor_id",
|
|
input: "stories/1/actors/",
|
|
storyID: 1,
|
|
actorID: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "wrong stories/ prefix",
|
|
input: "story/1/actors/my-actor",
|
|
storyID: 1,
|
|
actorID: "",
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
storyID, actorID, err := GetActorIDForResourceName(tt.input)
|
|
|
|
if tt.expectErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error but got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if storyID != tt.storyID {
|
|
t.Errorf("expected story_id %d, got %d", tt.storyID, storyID)
|
|
}
|
|
|
|
if actorID != tt.actorID {
|
|
t.Errorf("expected actor_id %s, got %s", tt.actorID, actorID)
|
|
}
|
|
})
|
|
}
|
|
}
|