package layercompositions import ( "context" "errors" "testing" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets" ) type compositionRepositoryStub struct { value Composition found bool } func (repository *compositionRepositoryStub) FindLayerComposition(_ context.Context, ownerID, jobID string) (Composition, bool, error) { if !repository.found || repository.value.OwnerID != ownerID || repository.value.JobID != jobID { return Composition{}, false, nil } return repository.value, true, nil } func (repository *compositionRepositoryStub) SaveLayerComposition(_ context.Context, value Composition, expectedVersion int64) (Composition, error) { if repository.found && repository.value.Version != expectedVersion { return Composition{}, ErrConflict } if !repository.found && expectedVersion != 0 { return Composition{}, ErrConflict } now := time.Date(2026, 9, 2, 8, 0, 0, 0, time.UTC) value.Version = expectedVersion + 1 value.CreatedAt = now value.UpdatedAt = now repository.value = value repository.found = true return value, nil } type assetCatalogStub map[string]assets.Asset func (catalog assetCatalogStub) GetOwner(_ context.Context, ownerID, assetID string) (assets.Asset, bool, error) { asset, found := catalog[assetID] return asset, found && asset.OwnerID == ownerID, nil } func TestServiceSavesOwnedCompositionAndNormalizesRotation(t *testing.T) { repository := &compositionRepositoryStub{} catalog := assetCatalogStub{ "base": {ID: "base", OwnerID: "owner", Kind: assets.KindImage}, "layer": {ID: "layer", OwnerID: "owner", Kind: assets.KindImage}, } service := NewService(repository, catalog) saved, err := service.Save(context.Background(), "owner", "job-1", 0, Composition{ BaseAssetID: "base", BaseVisible: true, Layers: []Layer{{ID: "layer-1", AssetID: "layer", Name: "主体", Visible: true, Box: Box{100, 100, 900, 900}, Rotation: 450, Source: "upload"}}, }) if err != nil { t.Fatal(err) } if saved.Version != 1 || saved.OwnerID != "owner" || saved.JobID != "job-1" || saved.Layers[0].Rotation != 90 { t.Fatalf("saved=%#v", saved) } } func TestServiceRejectsForeignOrMalformedLayerAssets(t *testing.T) { service := NewService(&compositionRepositoryStub{}, assetCatalogStub{ "base": {ID: "base", OwnerID: "owner", Kind: assets.KindImage}, "foreign": {ID: "foreign", OwnerID: "another-owner", Kind: assets.KindImage}, }) _, err := service.Save(context.Background(), "owner", "job-1", 0, Composition{ BaseAssetID: "base", BaseVisible: true, Layers: []Layer{{ID: "layer-1", AssetID: "foreign", Name: "主体", Visible: true, Box: Box{100, 100, 900, 900}, Source: "library"}}, }) if !errors.Is(err, ErrAssetNotFound) { t.Fatalf("foreign asset err=%v", err) } _, err = service.Save(context.Background(), "owner", "job-1", 0, Composition{ BaseAssetID: "base", BaseVisible: true, Layers: []Layer{{ID: "layer-1", AssetID: "base", Name: "主体", Visible: true, Box: Box{900, 100, 100, 900}, Source: "provider"}}, }) if !errors.Is(err, ErrInvalid) { t.Fatalf("invalid box err=%v", err) } } func TestServiceUsesOptimisticVersion(t *testing.T) { repository := &compositionRepositoryStub{found: true, value: Composition{JobID: "job-1", OwnerID: "owner", Version: 3}} service := NewService(repository, assetCatalogStub{"base": {ID: "base", OwnerID: "owner", Kind: assets.KindImage}}) _, err := service.Save(context.Background(), "owner", "job-1", 2, Composition{BaseAssetID: "base", BaseVisible: true}) if !errors.Is(err, ErrConflict) { t.Fatalf("err=%v", err) } }