test(actor): add comprehensive test cases for database functions

Add test cases for Save, GetByActorID, ListByStoryID, Delete, and
GetActorIDForResourceName functions. Use real database instances for
integration tests following Story test patterns.
This commit is contained in:
2026-04-01 19:03:22 +00:00
parent 9720517a8c
commit 789e32a57f
+496
View File
@@ -2,12 +2,16 @@
package actor package actor
import ( import (
"context"
"database/sql"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"time" "time"
v1 "git.tipsy.codes/charles/webstory/pkg/api/webstory/v1" 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" "google.golang.org/protobuf/types/known/timestamppb"
) )
@@ -92,3 +96,495 @@ func TestActorTranslator_FromAPI(t *testing.T) {
}) })
} }
} }
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)
}
})
}
}