Files
webstory/pkg/database/actor/actor_test.go
charles 4d4be92df7 feat: implement Actor model with database access and unit tests
Add database access layer for Actor resource with:
- ActorTranslator for API/database conversions
- DBActor struct with validation
- Database operations (insert, update, query, delete)
- Unit tests covering all public APIs

Fix actor_id validation to always validate, consistent with Story model.
2026-04-01 07:20:21 +00:00

167 lines
4.1 KiB
Go

package actor
import (
"fmt"
"strings"
"testing"
"time"
v1 "git.tipsy.codes/charles/webstory/pkg/api/webstory/v1"
"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
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: "actor1",
NameValue: "Player",
},
expectErr: false,
checkFunc: func(a *DBActor) error {
if a.ActorID != "actor1" {
return fmt.Errorf("expected ActorID actor1, got %s", a.ActorID)
}
if a.NameValue != "Player" {
return fmt.Errorf("expected NameValue Player, got %s", a.NameValue)
}
return nil
},
},
{
name: "actor with all fields converts correctly",
apiActor: &v1.Actor{
Name: "stories/my-actor-1",
ActorId: "my-actor-1",
NameValue: "The Hero",
Role: "protagonist",
Notes: "Main character",
CreateTime: timestamppb.New(now),
UpdateTime: timestamppb.New(now),
Etag: "etag-123",
},
expectErr: false,
checkFunc: func(a *DBActor) error {
if a.ActorID != "my-actor-1" {
return fmt.Errorf("expected ActorID my-actor-1, got %s", a.ActorID)
}
if a.NameValue != "The Hero" {
return fmt.Errorf("expected NameValue The Hero, got %s", a.NameValue)
}
if a.Role != "protagonist" {
return fmt.Errorf("expected Role protagonist, got %s", a.Role)
}
if a.Notes != "Main character" {
return fmt.Errorf("expected Notes Main character, got %s", a.Notes)
}
if a.Etag != "etag-123" {
return fmt.Errorf("expected Etag etag-123, got %s", a.Etag)
}
return nil
},
},
{
name: "empty notes converted to empty string",
apiActor: &v1.Actor{
ActorId: "actor2",
NameValue: "NPC",
Notes: "",
},
expectErr: false,
checkFunc: func(a *DBActor) error {
if a.Notes != "" {
return fmt.Errorf("expected empty Notes, got %s", a.Notes)
}
return nil
},
},
{
name: "missing name_value returns error",
apiActor: &v1.Actor{
ActorId: "actor4",
},
expectErr: true,
errMsg: "name_value is required",
},
{
name: "invalid actor_id_short returns error",
apiActor: &v1.Actor{
NameValue: "Test",
},
expectErr: true,
errMsg: "invalid actor_id format: must be 4-63 characters, matching pattern /[a-z][0-9-][0-9]$/",
},
{
name: "invalid actor_id_uppercase returns error",
apiActor: &v1.Actor{
ActorId: "Actor2",
},
expectErr: true,
errMsg: "invalid actor_id format: must be 4-63 characters, matching pattern /[a-z][0-9-][0-9]$/",
},
{
name: "invalid actor_id_starts_with_number returns error",
apiActor: &v1.Actor{
ActorId: "1actor",
},
expectErr: true,
errMsg: "invalid actor_id format: must be 4-63 characters, matching pattern /[a-z][0-9-][0-9]$/",
},
{
name: "valid actor_id_with_hyphens_and_numbers",
apiActor: &v1.Actor{
NameValue: "Test Actor",
ActorId: "my-awesome-actor-123",
},
expectErr: false,
checkFunc: func(a *DBActor) error {
if a.ActorID != "my-awesome-actor-123" {
return fmt.Errorf("expected ActorID my-awesome-actor-123, got %s", a.ActorID)
}
return nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
translator := NewActorTranslator()
got, err := translator.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 message to contain %q, got %q", tt.errMsg, err.Error())
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tt.checkFunc != nil {
if err := tt.checkFunc(got); err != nil {
t.Errorf("checkFunc failed: %v", err)
}
}
})
}
}