// seed_test.go — seedDemoData 的 in-memory(dbBacked=false)unit test。 // // Owner: testing agent(補測任務 — cmd/api-server 可測部分) // // 對象:seedDemoData。in-memory 分支不需 DB,純 unit。DB-backed(dbBacked=true)分支 // 需真 Postgres → 拆到 seed_db_test.go(//go:build dbtest,130 docker 跑)。 // // 行為對齊 seed.go docstring: // - dbBacked=false 用傳入 userID(StaticUserID 概念)當 owner,model/device id 帶隨機後綴。 // - 不 upsert demo user(in-memory 不需 FK)。 // - seedDemoData 本身「每次呼叫都產新資料」(id 帶 uuid 後綴 / pairing 每次新建)—— // 「重啟不重複 seed」是 main.go 只呼叫一次保證的,不是本函式的冪等性;故本檔不誤測冪等。 package main import ( "context" "io" "log/slog" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "visiona-backend/internal/auth" "visiona-backend/internal/device" "visiona-backend/internal/model" "visiona-backend/internal/user" ) // discardLog 丟棄 seed 的 log 噪音。 func discardLog() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } // seedInMemoryDeps 建立一組全 in-memory 的 seed 依賴。 type seedDeps struct { devRepo *device.InMemoryRepository mdlRepo *model.InMemoryRepository pairings *auth.InMemoryPairingStore userStore *user.InMemoryStore } func newSeedDeps() seedDeps { return seedDeps{ devRepo: device.NewInMemoryRepository(), mdlRepo: model.NewInMemoryRepository(), pairings: auth.NewInMemoryPairingStore(), userStore: user.NewInMemoryStore(), } } // happy:in-memory seed 應建出 1 device + 1 model + 1 pairing token,全掛在傳入的 userID 下。 func TestSeedDemoData_InMemory_SeedsAllEntities(t *testing.T) { t.Parallel() d := newSeedDeps() const uid = "demo-user" // 雛形 StaticUserID 概念(in-memory 不需是合法 UUID) require.NoError(t, seedDemoData( d.devRepo, d.mdlRepo, d.pairings, d.userStore, uid, false /* dbBacked */, discardLog(), )) ctx := context.Background() // device:owner = uid devs, err := d.devRepo.List(ctx, uid) require.NoError(t, err) require.Len(t, devs, 1, "in-memory seed 應建 1 個 demo device") assert.Equal(t, uid, devs[0].OwnerUserID) assert.Equal(t, "kl520", devs[0].DeviceType) assert.Equal(t, device.RemoteStatusOffline, devs[0].RemoteStatus) // model:owner = uid,StorageKey 用 owner 拼路徑 mdls, err := d.mdlRepo.List(ctx, model.ListFilter{OwnerUserID: uid}) require.NoError(t, err) require.Len(t, mdls, 1, "in-memory seed 應建 1 個 demo model") assert.Equal(t, uid, mdls[0].OwnerUserID) assert.Equal(t, "kl520", mdls[0].TargetChip) assert.Equal(t, model.SourceUploaded, mdls[0].Source) assert.Equal(t, "models/"+uid+"/demo.nef", mdls[0].StorageKey) require.NotNil(t, mdls[0].UploadedAt, "seed model 應設 UploadedAt(ready)") // pairing token:owner = uid tokens, err := d.pairings.List(ctx, uid) require.NoError(t, err) require.Len(t, tokens, 1, "in-memory seed 應建 1 個 demo pairing token") assert.Equal(t, uid, tokens[0].UserID) } // in-memory 不 upsert demo user(不需 FK);userStore 應維持空(demoSeedUserID 不存在)。 func TestSeedDemoData_InMemory_DoesNotUpsertDemoUser(t *testing.T) { t.Parallel() d := newSeedDeps() require.NoError(t, seedDemoData( d.devRepo, d.mdlRepo, d.pairings, d.userStore, "demo-user", false, discardLog(), )) // dbBacked=false 不應走 userStore.Upsert(demoSeedUserID) _, err := d.userStore.Get(context.Background(), demoSeedUserID) assert.Error(t, err, "in-memory seed 不應 upsert demoSeedUserID(無 FK 需求)") } // boundary / concurrency-ish:seedDemoData 每次呼叫產生獨立資料(id 帶隨機後綴)—— // 連呼叫兩次同一 userID 應累積 2 device / 2 model / 2 token(對齊 docstring:本函式非冪等, // 「不重複 seed」由 main 只呼叫一次保證)。此測試把該行為釘住,避免有人誤改成「靜默去重」。 func TestSeedDemoData_InMemory_CalledTwice_Accumulates(t *testing.T) { t.Parallel() d := newSeedDeps() const uid = "demo-user" log := discardLog() require.NoError(t, seedDemoData(d.devRepo, d.mdlRepo, d.pairings, d.userStore, uid, false, log)) require.NoError(t, seedDemoData(d.devRepo, d.mdlRepo, d.pairings, d.userStore, uid, false, log)) ctx := context.Background() devs, err := d.devRepo.List(ctx, uid) require.NoError(t, err) assert.Len(t, devs, 2, "兩次呼叫應產生 2 個 device(id 帶隨機後綴、不去重)") mdls, err := d.mdlRepo.List(ctx, model.ListFilter{OwnerUserID: uid}) require.NoError(t, err) assert.Len(t, mdls, 2, "兩次呼叫應產生 2 個 model") tokens, err := d.pairings.List(ctx, uid) require.NoError(t, err) assert.Len(t, tokens, 2, "兩次呼叫應產生 2 個 pairing token") } // error path:底層 repo Save 失敗時 seedDemoData 只 log warning、不回 error、不中斷(fail-soft)。 // 用一個 Save 永遠失敗的 device repo 驗「seed 失敗不阻擋啟動」契約。 func TestSeedDemoData_InMemory_DeviceSaveFails_StillReturnsNil(t *testing.T) { t.Parallel() d := newSeedDeps() err := seedDemoData( failingDeviceRepo{}, // device.Save 永遠失敗 d.mdlRepo, d.pairings, d.userStore, "demo-user", false, discardLog(), ) require.NoError(t, err, "device save 失敗時 seedDemoData 仍應回 nil(fail-soft,不阻擋啟動)") // model / pairing 仍應照常建立(device 失敗不影響後續步驟) mdls, err := d.mdlRepo.List(context.Background(), model.ListFilter{OwnerUserID: "demo-user"}) require.NoError(t, err) assert.Len(t, mdls, 1, "device save 失敗不應中斷 model 建立") } // failingDeviceRepo 是 device.Save 永遠回錯的 device.Repository(其餘方法委派給空行為)。 type failingDeviceRepo struct{} func (failingDeviceRepo) Get(ctx context.Context, id string) (*device.Device, error) { return nil, device.ErrNotFound } func (failingDeviceRepo) GetBySerial(ctx context.Context, ownerUserID, serial string) (*device.Device, error) { return nil, device.ErrNotFound } func (failingDeviceRepo) List(ctx context.Context, ownerUserID string) ([]*device.Device, error) { return nil, nil } func (failingDeviceRepo) Save(ctx context.Context, dev *device.Device) error { return assert.AnError } func (failingDeviceRepo) Delete(ctx context.Context, id string) error { return nil }