258 lines
11 KiB
Go
258 lines
11 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
|
|
"infinite-canvas/backend/internal/model"
|
|
"infinite-canvas/backend/internal/repository"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func newProjectDeleteTestService(t *testing.T) (*Service, *gorm.DB) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open("file:"+newID()+"?mode=memory&cache=shared"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(
|
|
&model.Project{},
|
|
&model.ProjectUnit{},
|
|
&model.CanvasProject{},
|
|
&model.CanvasShare{},
|
|
&model.CanvasUnitLink{},
|
|
&model.ProjectAssetLink{},
|
|
&model.ProjectAssetFolder{},
|
|
&model.ProjectAssetCandidate{},
|
|
&model.Asset{},
|
|
&model.Task{},
|
|
&model.Shot{},
|
|
&model.ShotRevision{},
|
|
&model.ShotArtifact{},
|
|
&model.ShotAssetReference{},
|
|
&model.WorkflowInstance{},
|
|
&model.WorkflowStepInstance{},
|
|
&model.WorkflowStepTask{},
|
|
&model.ProductionTaskLink{},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &Service{repo: repository.New(db)}, db
|
|
}
|
|
|
|
func TestDeleteProjectUnlinksCanvasAndKeepsIndependentRecords(t *testing.T) {
|
|
service, db := newProjectDeleteTestService(t)
|
|
project := model.Project{ID: "project-1", UserID: "user-1", Name: "短剧", Status: model.ProjectStatusActive}
|
|
canvas := model.CanvasProject{ID: "canvas-1", UserID: "user-1", ProjectID: project.ID, Title: "分镜", PayloadJSON: `{"projectId":"project-1","nodes":[]}`}
|
|
asset := model.Asset{ID: "asset-1", UserID: "user-1", Title: "角色", Status: model.AssetVersionStatusConfirmed}
|
|
task := model.Task{ID: "task-1", UserID: "user-1", ProjectID: project.ID, Status: model.TaskStatusSucceeded, Prompt: "已完成"}
|
|
canvasTask := model.Task{ID: "task-2", UserID: "user-1", ProjectID: canvas.ID, Status: model.TaskStatusSucceeded, Prompt: "画布任务"}
|
|
seed := []any{
|
|
&project,
|
|
&canvas,
|
|
&model.CanvasShare{ID: "share-1", UserID: "user-1", ProjectID: project.ID},
|
|
&model.ProjectUnit{ID: "unit-1", ProjectID: project.ID, Title: "第一集"},
|
|
&model.CanvasUnitLink{ID: "canvas-link-1", ProjectID: project.ID, CanvasID: canvas.ID, UnitID: "unit-1"},
|
|
&asset,
|
|
&model.ProjectAssetLink{ID: "asset-link-1", ProjectID: project.ID, AssetID: asset.ID},
|
|
&model.ProjectAssetFolder{ID: "folder-1", ProjectID: project.ID, Name: "角色", NameKey: "角色"},
|
|
&model.ProjectAssetCandidate{ID: "candidate-1", ProjectID: project.ID, UnitID: "unit-1", Name: "角色", Status: "pending_confirmation"},
|
|
&model.Shot{ID: "shot-1", ProjectID: project.ID, UnitID: "unit-1", CurrentRevisionID: "revision-1", Title: "镜头一"},
|
|
&model.ShotRevision{ID: "revision-1", ShotID: "shot-1", Version: 1, PlotDescription: "角色入场"},
|
|
&model.ShotArtifact{ID: "artifact-1", ProjectID: project.ID, UnitID: "unit-1", ShotID: "shot-1", RevisionID: "revision-1", Type: "storyboard", Version: 1, Status: "ready"},
|
|
&task,
|
|
&canvasTask,
|
|
&model.ProductionTaskLink{ID: "production-link-1", TaskID: task.ID, ProjectID: project.ID, UnitID: "unit-1", ShotID: "shot-1"},
|
|
}
|
|
for _, item := range seed {
|
|
if err := db.Create(item).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := service.DeleteProject("user-1", project.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var storedProject model.Project
|
|
if err := db.First(&storedProject, "id = ?", project.ID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
t.Fatalf("project error = %v, want record not found", err)
|
|
}
|
|
var storedCanvas model.CanvasProject
|
|
if err := db.First(&storedCanvas, "id = ?", canvas.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if storedCanvas.ProjectID != "" {
|
|
t.Fatalf("canvas project id = %q, want empty", storedCanvas.ProjectID)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal([]byte(storedCanvas.PayloadJSON), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := payload["projectId"]; exists {
|
|
t.Fatalf("canvas payload still contains deleted project id: %s", storedCanvas.PayloadJSON)
|
|
}
|
|
|
|
for _, check := range []struct {
|
|
name string
|
|
model any
|
|
where string
|
|
}{
|
|
{name: "canvas share", model: &model.CanvasShare{}, where: "project_id = ?"},
|
|
{name: "canvas unit link", model: &model.CanvasUnitLink{}, where: "project_id = ?"},
|
|
{name: "project unit", model: &model.ProjectUnit{}, where: "project_id = ?"},
|
|
{name: "asset link", model: &model.ProjectAssetLink{}, where: "project_id = ?"},
|
|
{name: "asset folder", model: &model.ProjectAssetFolder{}, where: "project_id = ?"},
|
|
{name: "asset candidate", model: &model.ProjectAssetCandidate{}, where: "project_id = ?"},
|
|
{name: "shot", model: &model.Shot{}, where: "project_id = ?"},
|
|
{name: "shot artifact", model: &model.ShotArtifact{}, where: "project_id = ?"},
|
|
{name: "production task link", model: &model.ProductionTaskLink{}, where: "project_id = ?"},
|
|
} {
|
|
var count int64
|
|
if err := db.Model(check.model).Where(check.where, project.ID).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("%s count = %d, want 0", check.name, count)
|
|
}
|
|
}
|
|
var storedAsset model.Asset
|
|
if err := db.First(&storedAsset, "id = ?", asset.ID).Error; err != nil {
|
|
t.Fatalf("asset should remain in the account library: %v", err)
|
|
}
|
|
|
|
var storedTask model.Task
|
|
if err := db.First(&storedTask, "id = ?", task.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if storedTask.ProjectID != "" {
|
|
t.Fatalf("direct project task id = %q, want empty", storedTask.ProjectID)
|
|
}
|
|
var storedCanvasTask model.Task
|
|
if err := db.First(&storedCanvasTask, "id = ?", canvasTask.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if storedCanvasTask.ProjectID != canvas.ID {
|
|
t.Fatalf("independent canvas task project id = %q, want %q", storedCanvasTask.ProjectID, canvas.ID)
|
|
}
|
|
}
|
|
|
|
func TestDeleteUserCanvasProjectDetachesTaskScope(t *testing.T) {
|
|
service, db := newProjectDeleteTestService(t)
|
|
canvas := model.CanvasProject{ID: "canvas-delete-1", UserID: "user-1", Title: "待删除画布", PayloadJSON: `{"id":"canvas-delete-1"}`}
|
|
task := model.Task{ID: "task-canvas-delete-1", UserID: "user-1", ProjectID: canvas.ID, Status: model.TaskStatusSucceeded, Prompt: "已完成"}
|
|
canvasLink := model.CanvasUnitLink{ID: "canvas-link-delete-1", ProjectID: "project-1", CanvasID: canvas.ID, UnitID: "unit-1", Role: "primary"}
|
|
for _, item := range []any{&canvas, &model.CanvasShare{ID: "share-canvas-delete-1", UserID: "user-1", ProjectID: canvas.ID}, &canvasLink, &task} {
|
|
if err := db.Create(item).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := service.DeleteUserCanvasProject("user-1", canvas.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var storedCanvas model.CanvasProject
|
|
if err := db.First(&storedCanvas, "id = ?", canvas.ID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
t.Fatalf("canvas error = %v, want record not found", err)
|
|
}
|
|
var storedTask model.Task
|
|
if err := db.First(&storedTask, "id = ?", task.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if storedTask.ProjectID != "" {
|
|
t.Fatalf("task project id = %q, want empty", storedTask.ProjectID)
|
|
}
|
|
var shareCount int64
|
|
if err := db.Model(&model.CanvasShare{}).Where("project_id = ?", canvas.ID).Count(&shareCount).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if shareCount != 0 {
|
|
t.Fatalf("canvas share count = %d, want 0", shareCount)
|
|
}
|
|
var linkCount int64
|
|
if err := db.Model(&model.CanvasUnitLink{}).Where("canvas_id = ?", canvas.ID).Count(&linkCount).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if linkCount != 0 {
|
|
t.Fatalf("canvas link count = %d, want 0", linkCount)
|
|
}
|
|
}
|
|
|
|
func TestDeleteProjectRejectsActiveProjectOrCanvasTasks(t *testing.T) {
|
|
service, db := newProjectDeleteTestService(t)
|
|
project := model.Project{ID: "project-1", UserID: "user-1", Name: "短剧", Status: model.ProjectStatusActive}
|
|
canvas := model.CanvasProject{ID: "canvas-1", UserID: "user-1", ProjectID: project.ID, Title: "分镜", PayloadJSON: `{"projectId":"project-1"}`}
|
|
activeTask := model.Task{ID: "task-1", UserID: "user-1", ProjectID: canvas.ID, Status: model.TaskStatusRunning, Prompt: "生成中"}
|
|
for _, item := range []any{&project, &canvas, &activeTask} {
|
|
if err := db.Create(item).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
err := service.DeleteProject("user-1", project.ID)
|
|
if err == nil || err.Error() != "项目仍有进行中的生成任务,请等待任务完成或取消后再删除" {
|
|
t.Fatalf("DeleteProject() error = %v", err)
|
|
}
|
|
var storedProject model.Project
|
|
if err := db.First(&storedProject, "id = ?", project.ID).Error; err != nil {
|
|
t.Fatalf("project should remain after rejected deletion: %v", err)
|
|
}
|
|
var storedTask model.Task
|
|
if err := db.First(&storedTask, "id = ?", activeTask.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if storedTask.ProjectID != canvas.ID {
|
|
t.Fatalf("active task project id = %q, want %q", storedTask.ProjectID, canvas.ID)
|
|
}
|
|
}
|
|
|
|
func TestRepositoryDeleteProjectRechecksActiveTasksInsideTransaction(t *testing.T) {
|
|
service, db := newProjectDeleteTestService(t)
|
|
project := model.Project{ID: "project-1", UserID: "user-1", Name: "短剧", Status: model.ProjectStatusActive}
|
|
canvas := model.CanvasProject{ID: "canvas-1", UserID: "user-1", ProjectID: project.ID, Title: "分镜", PayloadJSON: `{"projectId":"project-1"}`}
|
|
activeTask := model.Task{ID: "task-1", UserID: "user-1", ProjectID: canvas.ID, Status: model.TaskStatusQueued, Prompt: "排队中"}
|
|
for _, item := range []any{&project, &canvas, &activeTask} {
|
|
if err := db.Create(item).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
err := service.repo.DeleteProject("user-1", project.ID, []model.CanvasProject{canvas})
|
|
if !errors.Is(err, repository.ErrProjectHasActiveTasks) {
|
|
t.Fatalf("DeleteProject() error = %v, want active-task conflict", err)
|
|
}
|
|
var storedProject model.Project
|
|
if err := db.First(&storedProject, "id = ?", project.ID).Error; err != nil {
|
|
t.Fatalf("project should remain after repository conflict: %v", err)
|
|
}
|
|
var storedCanvas model.CanvasProject
|
|
if err := db.First(&storedCanvas, "id = ?", canvas.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if storedCanvas.ProjectID != project.ID {
|
|
t.Fatalf("canvas project id = %q, want %q", storedCanvas.ProjectID, project.ID)
|
|
}
|
|
}
|
|
|
|
func TestDeleteProjectRejectsCanvasPayloadThatCannotBeUpdated(t *testing.T) {
|
|
service, db := newProjectDeleteTestService(t)
|
|
project := model.Project{ID: "project-1", UserID: "user-1", Name: "短剧", Status: model.ProjectStatusActive}
|
|
canvas := model.CanvasProject{ID: "canvas-1", UserID: "user-1", ProjectID: project.ID, Title: "分镜", PayloadJSON: "{"}
|
|
for _, item := range []any{&project, &canvas} {
|
|
if err := db.Create(item).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := service.DeleteProject("user-1", project.ID); err == nil {
|
|
t.Fatal("DeleteProject() error = nil for malformed canvas payload")
|
|
}
|
|
var storedProject model.Project
|
|
if err := db.First(&storedProject, "id = ?", project.ID).Error; err != nil {
|
|
t.Fatalf("project should remain after payload validation failure: %v", err)
|
|
}
|
|
}
|