643 lines
15 KiB
Go
643 lines
15 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"
|
|
|
|
story "git.tipsy.codes/charles/webstory/pkg/database/story"
|
|
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)
|
|
}
|
|
|
|
// Create a story first
|
|
story := &story.DBStory{
|
|
StoryID: "save-test-story-1",
|
|
Title: "Save Test Story",
|
|
}
|
|
err = story.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("failed to save story: %v", err)
|
|
}
|
|
|
|
actor := &DBActor{
|
|
StoryID: story.ID,
|
|
ActorID: "save-test-1",
|
|
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)
|
|
}
|
|
|
|
// Create a story first
|
|
story := &story.DBStory{
|
|
StoryID: "get-by-id-story-1",
|
|
Title: "Get By ID Story",
|
|
}
|
|
err = story.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("failed to save story: %v", err)
|
|
}
|
|
|
|
actor := &DBActor{
|
|
StoryID: story.ID,
|
|
ActorID: "get-by-id-test-1",
|
|
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)
|
|
}
|
|
|
|
// Create a story first
|
|
story := &story.DBStory{
|
|
StoryID: "get-by-actor-id-story-1",
|
|
Title: "Get By Actor ID Story",
|
|
}
|
|
err = story.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("failed to save story: %v", err)
|
|
}
|
|
|
|
actor := &DBActor{
|
|
StoryID: story.ID,
|
|
ActorID: "get-by-actor-id-test-1",
|
|
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)
|
|
}
|
|
|
|
// Create a story first
|
|
story := &story.DBStory{
|
|
StoryID: "list-test-story-1",
|
|
Title: "List Test Story",
|
|
}
|
|
err = story.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("failed to save story: %v", err)
|
|
}
|
|
|
|
actor1 := &DBActor{
|
|
StoryID: story.ID,
|
|
ActorID: "list-test-1",
|
|
NameValue: "Actor One",
|
|
Role: "Role One",
|
|
}
|
|
actor2 := &DBActor{
|
|
StoryID: story.ID,
|
|
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)
|
|
}
|
|
|
|
// Create a story first
|
|
story := &story.DBStory{
|
|
StoryID: "delete-test-story-1",
|
|
Title: "Delete Test Story",
|
|
}
|
|
err = story.Save(db)
|
|
if err != nil {
|
|
t.Fatalf("failed to save story: %v", err)
|
|
}
|
|
|
|
actor := &DBActor{
|
|
StoryID: story.ID,
|
|
ActorID: "delete-test-actor-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)
|
|
}
|
|
})
|
|
}
|
|
}
|