package model import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // readyModel 建一個 ready(UploadedAt 已設)的 model helper。 func readyModel(id, owner, visibility string) *Model { now := time.Now().UTC() return &Model{ ID: id, OwnerUserID: owner, Name: "m-" + id, StorageKey: "k/" + id, FileSize: 1024, Source: SourceUploaded, Visibility: visibility, UploadedAt: &now, } } // TestInMemory_SaveDefaultsVisibility 驗證 Save 未設 visibility 時預設 private。 func TestInMemory_SaveDefaultsVisibility(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() require.NoError(t, r.Save(ctx, &Model{ID: "m", OwnerUserID: "u", Name: "n", StorageKey: "k", Source: SourceUploaded})) got, err := r.Get(ctx, "m") require.NoError(t, err) assert.Equal(t, VisibilityPrivate, got.Visibility, "未設 visibility 應預設 private") } // TestInMemory_ShareCRUD 驗證 share 的 Upsert / Get / List / Delete。 func TestInMemory_ShareCRUD(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() require.NoError(t, r.UpsertShare(ctx, &ModelShare{ModelID: "m", GranteeUserID: "bob", Role: "viewer", GrantedBy: "owner"})) got, err := r.GetShare(ctx, "m", "bob") require.NoError(t, err) assert.Equal(t, "viewer", got.Role) // upsert 同 grantee → 更新 role。 require.NoError(t, r.UpsertShare(ctx, &ModelShare{ModelID: "m", GranteeUserID: "bob", Role: "editor", GrantedBy: "owner"})) got, err = r.GetShare(ctx, "m", "bob") require.NoError(t, err) assert.Equal(t, "editor", got.Role, "重複 upsert 應更新 role") // list require.NoError(t, r.UpsertShare(ctx, &ModelShare{ModelID: "m", GranteeUserID: "alice", Role: "viewer", GrantedBy: "owner"})) shares, err := r.ListShares(ctx, "m") require.NoError(t, err) assert.Len(t, shares, 2) // delete require.NoError(t, r.DeleteShare(ctx, "m", "bob")) _, err = r.GetShare(ctx, "m", "bob") assert.ErrorIs(t, err, ErrNotFound) // delete 不存在 → ErrNotFound assert.ErrorIs(t, r.DeleteShare(ctx, "m", "ghost"), ErrNotFound) } // TestInMemory_LibraryVisibility 驗證 Library predicate:我的 ∪ public ∪ shared,排除別人 private。 func TestInMemory_LibraryVisibility(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() require.NoError(t, r.Save(ctx, readyModel("mine", "me", VisibilityPrivate))) require.NoError(t, r.Save(ctx, readyModel("otherPriv", "other", VisibilityPrivate))) require.NoError(t, r.Save(ctx, readyModel("otherPub", "other", VisibilityPublic))) require.NoError(t, r.Save(ctx, readyModel("otherShared", "other", VisibilityPrivate))) require.NoError(t, r.UpsertShare(ctx, &ModelShare{ModelID: "otherShared", GranteeUserID: "me", Role: "viewer", GrantedBy: "other"})) items, _, err := r.Library(ctx, LibraryQuery{UserID: "me", Limit: 100}) require.NoError(t, err) ids := map[string]*LibraryItem{} for _, it := range items { ids[it.Model.ID] = it } assert.Contains(t, ids, "mine") assert.Contains(t, ids, "otherPub") assert.Contains(t, ids, "otherShared") assert.NotContains(t, ids, "otherPriv", "別人 private 不應可見") assert.True(t, ids["otherShared"].SharedWithMe) assert.Equal(t, AccessOwner, ids["mine"].MyAccess) } // TestInMemory_LibraryExcludesNotReady 驗證未 ready 的 model 不進 Library。 func TestInMemory_LibraryExcludesNotReady(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() require.NoError(t, r.Save(ctx, &Model{ID: "pending", OwnerUserID: "me", Name: "p", StorageKey: "k", Source: SourceUploaded, Visibility: VisibilityPublic})) items, _, err := r.Library(ctx, LibraryQuery{UserID: "me", Limit: 100}) require.NoError(t, err) assert.Empty(t, items) } // TestInMemory_LibraryTenantStub 驗證 tenant 可見性:有 org 對應才命中(in-memory 用 SetUserOrg 模擬)。 func TestInMemory_LibraryTenantStub(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() r.SetUserOrg("owner", "org-1") r.SetUserOrg("teammate", "org-1") r.SetUserOrg("outsider", "org-2") require.NoError(t, r.Save(ctx, readyModel("tenantModel", "owner", VisibilityTenant))) // 同 org → 可見 items, _, err := r.Library(ctx, LibraryQuery{UserID: "teammate", UserOrgID: "org-1", Limit: 100}) require.NoError(t, err) require.Len(t, items, 1) assert.Equal(t, "tenantModel", items[0].Model.ID) // 異 org → 不可見 items, _, err = r.Library(ctx, LibraryQuery{UserID: "outsider", UserOrgID: "org-2", Limit: 100}) require.NoError(t, err) assert.Empty(t, items, "異 org 不應看到 tenant model") // 無 org(OIDC 現況)→ 不可見(安全預設) items, _, err = r.Library(ctx, LibraryQuery{UserID: "teammate", UserOrgID: "", Limit: 100}) require.NoError(t, err) assert.Empty(t, items, "無 org 不應命中 tenant") } // TestInMemory_LibraryPaginationStable 驗證 cursor 分頁不重不漏。 func TestInMemory_LibraryPaginationStable(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() for i := 0; i < 5; i++ { require.NoError(t, r.Save(ctx, readyModel(string(rune('a'+i)), "me", VisibilityPrivate))) } seen := map[string]bool{} var cursor *Cursor for page := 0; page < 10; page++ { items, hasMore, err := r.Library(ctx, LibraryQuery{ UserID: "me", Limit: 2, Sort: "name", Order: "asc", Cursor: cursor, }) require.NoError(t, err) for _, it := range items { assert.False(t, seen[it.Model.ID], "分頁重複 %s", it.Model.ID) seen[it.Model.ID] = true } if !hasMore { break } last := items[len(items)-1].Model cursor = &Cursor{ID: last.ID, SortValue: last.Name} } assert.Len(t, seen, 5, "所有 model 應被分頁走過一次") }