4 Commits

Author SHA1 Message Date
charles 789e32a57f 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.
2026-04-01 19:03:22 +00:00
charles 9720517a8c Merge pull request 'Add Actor model with unit tests and integration tests' (#1) from add-actor-model into main
Reviewed-on: https://git.tipsy.codes/charles/webstory/pulls/1
2026-04-01 18:29:37 +00:00
charles a9a3556b18 Add Actor model with unit tests and integration tests
Add Actor model implementation in pkg/database/actor/actor.go with
associated unit tests in actor_test.go. Implement tests following the
pattern used for Story model, including Postgres integration tests.
2026-04-01 07:54:50 +00:00
charles 6d3db74bf4 add: styleguide and updated agents 2026-04-01 00:24:22 -07:00
5 changed files with 1021 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "styleguide"]
path = styleguide
url = https://github.com/google/styleguide.git
+3
View File
@@ -13,6 +13,9 @@ Before committing changes, run `make test`. The tests must pass.
## Backend/Golang ## Backend/Golang
Read the style guide, it can be found at styleguide/go/.
Make sure the git submodules are loaded.
Do not alter go.mod directly, instead, use the CLI tools `go mod tidy`, `go mod Do not alter go.mod directly, instead, use the CLI tools `go mod tidy`, `go mod
init`, and so forth. init`, and so forth.
+424
View File
@@ -0,0 +1,424 @@
// Package actor provides database access for the Actor resource.
// It defines the DBActor struct and ActorTranslator to convert between
// the API format (api.pb.go) and the database format (schema.sql).
package actor
import (
"database/sql"
"errors"
"fmt"
"regexp"
"strings"
"time"
v1 "git.tipsy.codes/charles/webstory/pkg/api/webstory/v1"
"github.com/google/uuid"
"github.com/lib/pq"
"google.golang.org/protobuf/types/known/timestamppb"
)
var (
// ErrActorIDInvalid is returned when the actor_id format is invalid.
ErrActorIDInvalid = errors.New("invalid actor_id format: must be 4-63 characters, matching pattern /[a-z][0-9-]/")
// actorIDPattern validates the actor_id format.
actorIDPattern = regexp.MustCompile(`^[a-z][0-9a-z-]{2,61}[0-9a-z]$`)
)
// DBActor represents an actor in the PostgreSQL database.
type DBActor struct {
ID int64
StoryID int64
ActorID string
NameValue string
Role string
Notes string
CreatedAt time.Time
UpdatedAt time.Time
Etag string
}
// ActorTranslator provides conversion methods between API and database formats.
type ActorTranslator struct{}
func NewActorTranslator() *ActorTranslator {
return &ActorTranslator{}
}
func (t *ActorTranslator) FromAPI(apiActor *v1.Actor) (*DBActor, error) {
if apiActor == nil {
return nil, errors.New("api actor cannot be nil")
}
if apiActor.ActorId != "" {
if err := validateActorID(apiActor.ActorId); err != nil {
return nil, err
}
}
if apiActor.NameValue == "" {
return nil, errors.New("name_value is required")
}
var createdAt, updatedAt time.Time
if apiActor.GetCreateTime() != nil {
createdAt = apiActor.CreateTime.AsTime()
}
if apiActor.GetUpdateTime() != nil {
updatedAt = apiActor.UpdateTime.AsTime()
}
return &DBActor{
ID: 0,
StoryID: 0,
ActorID: apiActor.ActorId,
NameValue: apiActor.NameValue,
Role: apiActor.Role,
Notes: apiActor.Notes,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
Etag: apiActor.Etag,
}, nil
}
func (t *ActorTranslator) ToAPI(dbActor *DBActor) *v1.Actor {
if dbActor == nil {
return nil
}
name := ""
if dbActor.StoryID > 0 && dbActor.ActorID != "" {
name = fmt.Sprintf("stories/%d/actors/%s", dbActor.StoryID, dbActor.ActorID)
}
return &v1.Actor{
Name: name,
ActorId: dbActor.ActorID,
NameValue: dbActor.NameValue,
Role: dbActor.Role,
Notes: dbActor.Notes,
CreateTime: timestamppb.New(dbActor.CreatedAt),
UpdateTime: timestamppb.New(dbActor.UpdatedAt),
Etag: dbActor.Etag,
}
}
func (t *ActorTranslator) ToAPIDetailed(dbActor *DBActor, storyID int64) *v1.Actor {
apiActor := t.ToAPI(dbActor)
if dbActor.StoryID > 0 {
apiActor.Name = fmt.Sprintf("stories/%d/actors/%s", dbActor.StoryID, dbActor.ActorID)
}
return apiActor
}
func (a *DBActor) Validate() error {
if a.ActorID == "" {
return errors.New("actor_id is required")
}
if a.NameValue == "" {
return errors.New("name_value is required")
}
if a.StoryID <= 0 {
return errors.New("story_id is required")
}
if err := validateActorID(a.ActorID); err != nil {
return err
}
return nil
}
func validateActorID(actorID string) error {
if actorID == "" {
return ErrActorIDInvalid
}
if len(actorID) < 4 || len(actorID) > 63 {
return ErrActorIDInvalid
}
if !actorIDPattern.MatchString(actorID) {
return ErrActorIDInvalid
}
return nil
}
func (a *DBActor) Save(db *sql.DB) error {
if err := a.Validate(); err != nil {
return err
}
if a.ID > 0 {
return a.update(db)
}
return a.insert(db)
}
func (a *DBActor) insert(db *sql.DB) error {
query := `
INSERT INTO actors (story_id, actor_id, name_value, role, notes, created_at, updated_at, etag)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, updated_at, etag
`
now := time.Now()
if a.CreatedAt.IsZero() {
a.CreatedAt = now
}
if a.UpdatedAt.IsZero() {
a.UpdatedAt = now
}
etag := a.Etag
if etag == "" {
etag = generateEtag()
}
var createdID int64
var createdAt, updatedAt time.Time
var returnedEtag string
err := db.QueryRow(
query,
a.StoryID,
a.ActorID,
a.NameValue,
pq.Array([]string{a.Role}),
pq.Array([]string{a.Notes}),
a.CreatedAt,
a.UpdatedAt,
etag,
).Scan(&createdID, &createdAt, &updatedAt, &returnedEtag)
if err != nil {
return fmt.Errorf("failed to insert actor: %w", err)
}
a.ID = createdID
a.CreatedAt = createdAt
a.UpdatedAt = updatedAt
a.Etag = returnedEtag
return nil
}
func (a *DBActor) update(db *sql.DB) error {
query := `
UPDATE actors
SET name_value = $1,
role = $2,
notes = $3,
updated_at = $4,
etag = $5
WHERE id = $6
RETURNING created_at, updated_at, etag
`
now := time.Now()
if a.UpdatedAt.IsZero() {
a.UpdatedAt = now
}
etag := a.Etag
if etag == "" {
etag = generateEtag()
}
var createdAt, updatedAt time.Time
var returnedEtag string
err := db.QueryRow(
query,
a.NameValue,
a.Role,
a.Notes,
a.UpdatedAt,
etag,
a.ID,
).Scan(&createdAt, &updatedAt, &returnedEtag)
if err != nil {
return fmt.Errorf("failed to update actor: %w", err)
}
a.CreatedAt = createdAt
a.UpdatedAt = updatedAt
a.Etag = returnedEtag
return nil
}
func generateEtag() string {
return uuid.New().String()
}
func GetByID(db *sql.DB, id int64) (*DBActor, error) {
query := `
SELECT id, story_id, actor_id, name_value, role, notes, created_at, updated_at, etag
FROM actors
WHERE id = $1
`
actor := &DBActor{}
err := db.QueryRow(query, id).Scan(
&actor.ID,
&actor.StoryID,
&actor.ActorID,
&actor.NameValue,
&actor.Role,
&actor.Notes,
&actor.CreatedAt,
&actor.UpdatedAt,
&actor.Etag,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("actor with id %d not found", id)
}
return nil, fmt.Errorf("failed to get actor: %w", err)
}
return actor, nil
}
func GetByActorID(db *sql.DB, storyID int64, actorID string) (*DBActor, error) {
if err := validateActorID(actorID); err != nil {
return nil, err
}
query := `
SELECT id, story_id, actor_id, name_value, role, notes, created_at, updated_at, etag
FROM actors
WHERE story_id = $1 AND actor_id = $2
`
actor := &DBActor{}
err := db.QueryRow(query, storyID, actorID).Scan(
&actor.ID,
&actor.StoryID,
&actor.ActorID,
&actor.NameValue,
&actor.Role,
&actor.Notes,
&actor.CreatedAt,
&actor.UpdatedAt,
&actor.Etag,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("actor with story_id %d and actor_id '%s' not found", storyID, actorID)
}
return nil, fmt.Errorf("failed to get actor: %w", err)
}
return actor, nil
}
func ListByStoryID(db *sql.DB, storyID int64) ([]*DBActor, error) {
if storyID <= 0 {
return nil, errors.New("story_id must be positive")
}
query := `
SELECT id, story_id, actor_id, name_value, role, notes, created_at, updated_at, etag
FROM actors
WHERE story_id = $1
ORDER BY name_value ASC
`
rows, err := db.Query(query, storyID)
if err != nil {
return nil, fmt.Errorf("failed to list actors: %w", err)
}
defer rows.Close()
actors := make([]*DBActor, 0)
for rows.Next() {
var id int64
var storyID int64
var actorID, nameValue, role, notes string
var createdAt, updatedAt time.Time
var etag string
err := rows.Scan(&id, &storyID, &actorID, &nameValue, &role, &notes, &createdAt, &updatedAt, &etag)
if err != nil {
return nil, fmt.Errorf("failed to scan actor: %w", err)
}
actors = append(actors, &DBActor{
ID: id,
StoryID: storyID,
ActorID: actorID,
NameValue: nameValue,
Role: role,
Notes: notes,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
Etag: etag,
})
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating actors: %w", err)
}
return actors, nil
}
func Delete(db *sql.DB, id int64) error {
query := `
DELETE FROM actors
WHERE id = $1
`
result, err := db.Exec(query, id)
if err != nil {
return fmt.Errorf("failed to delete actor: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("actor with id %d not found", id)
}
return nil
}
func GetActorIDForResourceName(resourceName string) (int64, string, error) {
prefix := "stories/"
if !strings.HasPrefix(resourceName, prefix) {
return 0, "", fmt.Errorf("invalid resource name: must start with '%s', got '%s'", prefix, resourceName)
}
remaining := strings.TrimPrefix(resourceName, prefix)
actorPrefix := "actors/"
if !strings.HasPrefix(remaining, actorPrefix) {
return 0, "", fmt.Errorf("invalid actor resource name: must contain '%s', got '%s'", actorPrefix, remaining)
}
actorID := strings.TrimPrefix(remaining, actorPrefix)
if actorID == "" {
return 0, "", errors.New("actor_id is empty in resource name")
}
if err := validateActorID(actorID); err != nil {
return 0, "", fmt.Errorf("invalid actor_id in resource name: %w", err)
}
storyPart := strings.TrimSuffix(remaining, actorPrefix+actorID)
if storyPart == "" {
return 0, "", errors.New("story_id is missing from resource name")
}
var storyID int64
_, err := fmt.Sscanf(storyPart, "%d", &storyID)
if err != nil || storyID <= 0 {
return 0, "", errors.New("invalid story_id in resource name")
}
return storyID, actorID, nil
}
+590
View File
@@ -0,0 +1,590 @@
// 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)
}
})
}
}
Submodule
+1
Submodule styleguide added at c098353acb