package postgres import ( "context" "database/sql" "encoding/json" "errors" "reflect" "testing" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets" ) func TestAssetCatalogUsesExplicitOwnerScopedQueries(t *testing.T) { now := time.Date(2026, 8, 13, 8, 0, 0, 0, time.UTC) row := []any{"asset-1", "owner-1", "image", "poster.png", "https://cdn.test/a.png", "uploads/a.png", "upload", []string{"upload"}, []byte(`{"size":4}`), now, now} tests := []struct { name string call func(*Database) (assets.Asset, bool, error) wantSQL string wantArgs []any }{ {name: "get owner", call: func(db *Database) (assets.Asset, bool, error) { return db.GetOwner(context.Background(), "owner-1", "asset-1") }, wantSQL: GetOwnerAssetSQL, wantArgs: []any{"owner-1", "asset-1"}}, {name: "get storage path within owner", call: func(db *Database) (assets.Asset, bool, error) { return db.GetOwnerByStoragePath(context.Background(), "owner-1", "uploads/a.png") }, wantSQL: GetOwnerAssetByStoragePathSQL, wantArgs: []any{"owner-1", "uploads/a.png"}}, {name: "get public", call: func(db *Database) (assets.Asset, bool, error) { return db.GetPublic(context.Background(), "api:agent-a", "agent-a", "asset-1", 200) }, wantSQL: GetPublicAssetSQL, wantArgs: []any{"api:agent-a", "asset-1", "api-client:agent-a", "agent-a", 200}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { q := &assetQuerier{rows: &assetRows{rows: [][]any{row}}} db := NewDatabase(Config{Backend: BackendPostgres}, q) got, found, err := tt.call(db) if err != nil || !found { t.Fatalf("result = %#v,%v,%v", got, found, err) } if q.sql != tt.wantSQL || !reflect.DeepEqual(q.args, tt.wantArgs) { t.Fatalf("query=%q args=%#v", q.sql, q.args) } if got.Metadata["size"] != float64(4) || got.StoragePath != "uploads/a.png" { t.Fatalf("asset=%#v", got) } }) } } func TestAssetCatalogCreateDefaultsCollectionsAndUsesAllFields(t *testing.T) { now := time.Date(2026, 8, 13, 8, 0, 0, 0, time.UTC) q := &assetQuerier{rows: &assetRows{rows: [][]any{{"a", "o", "image", "n", "u", nil, "external", []string{}, []byte(`{}`), now, now}}}} db := NewDatabase(Config{Backend: BackendPostgres}, q) got, err := db.Create(context.Background(), assets.Asset{ID: "a", OwnerID: "o", Kind: assets.KindImage, Name: "n", URL: "u", Source: assets.SourceExternal, CreatedAt: now, UpdatedAt: now}) if err != nil { t.Fatal(err) } if q.sql != CreateAssetSQL || got.Tags == nil || got.Metadata == nil { t.Fatalf("query=%q asset=%#v", q.sql, got) } if !reflect.DeepEqual(q.args[7], []string{}) || string(q.args[8].([]byte)) != "{}" { t.Fatalf("args=%#v", q.args) } } func TestAssetCatalogDeleteIsOwnerScopedAndNotFound(t *testing.T) { q := &assetQuerier{rows: &assetRows{}} db := NewDatabase(Config{Backend: BackendPostgres}, q) _, found, err := db.DeleteOwner(context.Background(), "owner", "asset") if err != nil || found { t.Fatalf("found=%v err=%v", found, err) } if q.sql != DeleteOwnerAssetSQL || !reflect.DeepEqual(q.args, []any{"owner", "asset"}) { t.Fatalf("query=%q args=%#v", q.sql, q.args) } } func TestAssetCatalogFailsClosedWithoutPostgres(t *testing.T) { db := NewDatabase(Config{Backend: BackendLocal}, nil) if _, err := db.ListOwner(context.Background(), "owner"); err == nil { t.Fatal("expected error") } } var _ assets.Catalog = (*Database)(nil) type assetQuerier struct { rows *assetRows err error sql string args []any } func (q *assetQuerier) Query(_ context.Context, sql string, args ...any) (Rows, error) { q.sql = sql q.args = args return q.rows, q.err } type assetRows struct { rows [][]any idx int err error } func (r *assetRows) Close() {} func (r *assetRows) Err() error { return r.err } func (r *assetRows) Next() bool { return r.idx < len(r.rows) } func (r *assetRows) Scan(dest ...any) error { if r.idx >= len(r.rows) { return errors.New("past end") } row := r.rows[r.idx] r.idx++ if len(row) != len(dest) { return errors.New("arity") } for i, v := range row { switch d := dest[i].(type) { case *string: if v == nil { *d = "" } else { *d = v.(string) } case *sql.NullString: if v == nil { *d = sql.NullString{} } else { *d = sql.NullString{String: v.(string), Valid: true} } case *[]string: *d = append([]string(nil), v.([]string)...) case *[]byte: *d = append([]byte(nil), v.([]byte)...) case *time.Time: *d = v.(time.Time) default: return errors.New("unsupported") } } return nil } func TestAssetWireContractFixtureDecodes(t *testing.T) { raw, err := readContractFixture("../../../contracts/assets/assets-v1.json") if err != nil { t.Fatal(err) } var fixture struct { Asset assets.Asset `json:"asset"` PublicVisibility struct { JobLookupLimit int `json:"jobLookupLimit"` } `json:"publicVisibility"` } if err = json.Unmarshal(raw, &fixture); err != nil { t.Fatal(err) } if fixture.Asset.ID != "asset-contract-1" || fixture.PublicVisibility.JobLookupLimit != assets.PublicJobLookupLimit { t.Fatalf("fixture=%#v", fixture) } wire, err := json.Marshal(fixture.Asset) if err != nil { t.Fatal(err) } var encoded map[string]any if err := json.Unmarshal(wire, &encoded); err != nil { t.Fatal(err) } if encoded["createdAt"] != "2026-08-13T08:00:00.000Z" || encoded["updatedAt"] != "2026-08-13T08:00:00.000Z" { t.Fatalf("wire timestamps = %q, %q", encoded["createdAt"], encoded["updatedAt"]) } }