package httpapi import ( "context" "net/http" "strings" "testing" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/layercompositions" ) type layerCompositionRepositoryStub struct { value layercompositions.Composition found bool } func (repository *layerCompositionRepositoryStub) FindLayerComposition(_ context.Context, ownerID, jobID string) (layercompositions.Composition, bool, error) { if !repository.found || repository.value.OwnerID != ownerID || repository.value.JobID != jobID { return layercompositions.Composition{}, false, nil } return repository.value, true, nil } func (repository *layerCompositionRepositoryStub) SaveLayerComposition(_ context.Context, value layercompositions.Composition, version int64) (layercompositions.Composition, error) { if repository.found && repository.value.Version != version { return layercompositions.Composition{}, layercompositions.ErrConflict } if !repository.found && version != 0 { return layercompositions.Composition{}, layercompositions.ErrConflict } value.Version = version + 1 value.CreatedAt = time.Date(2026, 9, 2, 8, 0, 0, 0, time.UTC) value.UpdatedAt = value.CreatedAt repository.value = value repository.found = true return value, nil } type layerCompositionAssetCatalog map[string]assets.Asset func (catalog layerCompositionAssetCatalog) GetOwner(_ context.Context, ownerID, assetID string) (assets.Asset, bool, error) { asset, found := catalog[assetID] return asset, found && asset.OwnerID == ownerID, nil } func TestLayerCompositionsHTTPPersistsOnlyOwnedJobOutputsAndAssets(t *testing.T) { jobStore := &httpJobStore{values: map[string]jobs.Job{ "job-1": {ID: "job-1", OwnerID: "owner", Capability: "image.generate", OutputAssetIDs: []string{"base"}}, "job-2": {ID: "job-2", OwnerID: "another-owner", Capability: "image.generate", OutputAssetIDs: []string{"foreign-base"}}, }} jobService := jobs.NewService(jobStore, nil) repository := &layerCompositionRepositoryStub{} compositionService := layercompositions.NewService(repository, layerCompositionAssetCatalog{ "base": {ID: "base", OwnerID: "owner", Kind: assets.KindImage}, "layer": {ID: "layer", OwnerID: "owner", Kind: assets.KindImage}, }) authorizer, err := NewPlatformAuthorizer(AuthState{Required: true, Configured: true}, &fixedSessionResolver{session: identity.Session{User: identity.User{ID: "owner", ClientID: "platform", Role: "user"}}}) if err != nil { t.Fatal(err) } handler, err := NewLayerCompositionsHandler(authorizer, jobService, compositionService, LayerCompositionsConfig{}) if err != nil { t.Fatal(err) } missing := serveJSON(t, handler, http.MethodGet, "/api/layer-compositions/job-1", nil) if missing.Code != http.StatusOK || strings.TrimSpace(missing.Body.String()) != `{"composition":null}` { t.Fatalf("missing=%d %s", missing.Code, missing.Body.String()) } saved := serveJSON(t, handler, http.MethodPut, "/api/layer-compositions/job-1", map[string]any{ "version": 0, "baseAssetId": "base", "baseVisible": true, "layers": []any{map[string]any{"id": "layer-1", "assetId": "layer", "name": "主体", "visible": true, "box": []any{100, 100, 900, 900}, "rotation": 15, "source": "provider"}}, }) if saved.Code != http.StatusOK || repository.value.Version != 1 || repository.value.Layers[0].Rotation != 15 { t.Fatalf("saved=%d %s value=%#v", saved.Code, saved.Body.String(), repository.value) } foreignJob := serveJSON(t, handler, http.MethodGet, "/api/layer-compositions/job-2", nil) if foreignJob.Code != http.StatusNotFound { t.Fatalf("foreign job=%d %s", foreignJob.Code, foreignJob.Body.String()) } invalidBase := serveJSON(t, handler, http.MethodPut, "/api/layer-compositions/job-1", map[string]any{ "version": 1, "baseAssetId": "layer", "baseVisible": true, "layers": []any{}, }) if invalidBase.Code != http.StatusBadRequest { t.Fatalf("invalid base=%d %s", invalidBase.Code, invalidBase.Body.String()) } }