176 lines
5.4 KiB
Go
176 lines
5.4 KiB
Go
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"])
|
|
}
|
|
}
|