package persistence import ( "context" "time" "github.com/Masterminds/squirrel" "github.com/deluan/rest" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("ShareRepository", func() { var repo model.ShareRepository var ctx context.Context var adminUser = model.User{ID: "admin", UserName: "admin", IsAdmin: true} BeforeEach(func() { DeferCleanup(configtest.SetupConfig()) ctx = request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) repo = NewShareRepository(GetDBXBuilder()) // Insert the admin user into the database (required for foreign key constraint) ur := NewUserRepository(GetDBXBuilder()) err := ur.Put(ctx, &adminUser) Expect(err).ToNot(HaveOccurred()) // Clean up shares db := GetDBXBuilder() _, err = db.NewQuery("DELETE FROM share").Execute() Expect(err).ToNot(HaveOccurred()) }) Describe("Headless Access", func() { Context("Repository creation and basic operations", func() { var headlessCtx context.Context BeforeEach(func() { headlessCtx = GinkgoT().Context() }) It("should create repository successfully with no user context", func() { // Create repository with no user context (headless) headlessRepo := NewShareRepository(GetDBXBuilder()) Expect(headlessRepo).ToNot(BeNil()) }) It("should handle GetAll for headless processes", func() { // Create a simple share directly in database shareID := "headless-test-share" _, err := GetDBXBuilder().NewQuery(` INSERT INTO share (id, user_id, description, resource_type, resource_ids, created_at, updated_at) VALUES ({:id}, {:user}, {:desc}, {:type}, {:ids}, {:created}, {:updated}) `).Bind(map[string]any{ "id": shareID, "user": adminUser.ID, "desc": "Headless Test Share", "type": "song", "ids": "song-1", "created": time.Now(), "updated": time.Now(), }).Execute() Expect(err).ToNot(HaveOccurred()) // Headless process should see all shares headlessRepo := NewShareRepository(GetDBXBuilder()) shares, err := headlessRepo.GetAll(headlessCtx) Expect(err).ToNot(HaveOccurred()) found := false for _, s := range shares { if s.ID == shareID { found = true break } } Expect(found).To(BeTrue(), "Headless process should see all shares") }) It("should handle individual share retrieval for headless processes", func() { // Create a simple share shareID := "headless-get-share" _, err := GetDBXBuilder().NewQuery(` INSERT INTO share (id, user_id, description, resource_type, resource_ids, created_at, updated_at) VALUES ({:id}, {:user}, {:desc}, {:type}, {:ids}, {:created}, {:updated}) `).Bind(map[string]any{ "id": shareID, "user": adminUser.ID, "desc": "Headless Get Share", "type": "song", "ids": "song-2", "created": time.Now(), "updated": time.Now(), }).Execute() Expect(err).ToNot(HaveOccurred()) // Headless process should be able to get the share headlessRepo := NewShareRepository(GetDBXBuilder()) share, err := headlessRepo.Get(headlessCtx, shareID) Expect(err).ToNot(HaveOccurred()) Expect(share.ID).To(Equal(shareID)) Expect(share.Description).To(Equal("Headless Get Share")) }) }) }) Describe("SQL ambiguity fix verification", func() { It("should handle share operations without SQL ambiguity errors", func() { // This test verifies that the loadMedia function doesn't cause SQL ambiguity // The key fix was using "album.id" instead of "id" in the album query filters // Create a share that would trigger the loadMedia function shareID := "sql-test-share" _, err := GetDBXBuilder().NewQuery(` INSERT INTO share (id, user_id, description, resource_type, resource_ids, created_at, updated_at) VALUES ({:id}, {:user}, {:desc}, {:type}, {:ids}, {:created}, {:updated}) `).Bind(map[string]any{ "id": shareID, "user": adminUser.ID, "desc": "SQL Test Share", "type": "album", "ids": "non-existent-album", // Won't find albums, but shouldn't cause SQL errors "created": time.Now(), "updated": time.Now(), }).Execute() Expect(err).ToNot(HaveOccurred()) // The Get operation should work without SQL ambiguity errors // even if no albums are found share, err := repo.Get(ctx, shareID) Expect(err).ToNot(HaveOccurred()) Expect(share.ID).To(Equal(shareID)) // Albums array should be empty since we used non-existent album ID Expect(share.Albums).To(BeEmpty()) }) }) Describe("Playlist share library scoping", func() { var otherLib model.Library var owner model.User var plsID string BeforeEach(func() { adminCtx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) // A second library the owner has no access to, plus a track in it lr := NewLibraryRepository(GetDBXBuilder()) otherLib = model.Library{ID: 0, Name: "Share Other Library", Path: "/share/other/lib"} Expect(lr.Put(adminCtx, &otherLib)).To(Succeed()) mr := NewMediaFileRepository(GetDBXBuilder()) Expect(mr.Put(adminCtx, &model.MediaFile{ID: "share-other", LibraryID: otherLib.ID, Path: "s/other.mp3", Title: "ShareOther"})).To(Succeed()) Expect(mr.Put(adminCtx, &model.MediaFile{ID: "share-ok", LibraryID: 1, Path: "s/ok.mp3", Title: "ShareOK"})).To(Succeed()) // Non-admin owner with access to library 1 only owner = createUserWithLibraries("share-owner", []int{1}) ur := NewUserRepository(GetDBXBuilder()) Expect(ur.Put(adminCtx, &owner)).To(Succeed()) Expect(ur.SetUserLibraries(adminCtx, owner.ID, []int{1})).To(Succeed()) // Owner-owned playlist containing tracks from both libraries plsID = "share-scope-pls" ownerCtx := request.WithUser(log.NewContext(GinkgoT().Context()), owner) pr := NewPlaylistRepository(GetDBXBuilder()) pls := &model.Playlist{ID: plsID, Name: "Scope Test", OwnerID: owner.ID} pls.AddMediaFiles(model.MediaFiles{{ID: "share-ok"}, {ID: "share-other"}}) Expect(pr.Put(ownerCtx, pls)).To(Succeed()) // Share row owned by the non-admin owner _, err := GetDBXBuilder().NewQuery(` INSERT INTO share (id, user_id, description, resource_type, resource_ids, created_at, updated_at) VALUES ({:id}, {:user}, {:desc}, {:type}, {:ids}, {:created}, {:updated}) `).Bind(map[string]any{ "id": "share-scope", "user": owner.ID, "desc": "Scope test share", "type": "playlist", "ids": plsID, "created": time.Now(), "updated": time.Now(), }).Execute() Expect(err).ToNot(HaveOccurred()) }) AfterEach(func() { adminCtx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) b := GetDBXBuilder() _, _ = b.NewQuery(`DELETE FROM share WHERE id = 'share-scope'`).Execute() pr := NewPlaylistRepository(b) _ = pr.Delete(adminCtx, plsID) mr := NewMediaFileRepository(b).(*mediaFileRepository) _, _ = mr.executeSQL(adminCtx, squirrel.Delete("media_file").Where(squirrel.Eq{"id": []string{"share-other", "share-ok"}})) lr := NewLibraryRepository(b).(*libraryRepository) _ = lr.delete(adminCtx, squirrel.Eq{"id": otherLib.ID}) _ = NewUserRepository(b).Delete(adminCtx, owner.ID) }) It("excludes tracks the owner cannot access from the shared playlist", func() { // Read the share as admin (mimics the public-share render path, which uses // the share repository's own context). loadMedia must scope to the owner. adminCtx := request.WithUser(ctx, adminUser) adminRepo := NewShareRepository(GetDBXBuilder()) share, err := adminRepo.Get(adminCtx, "share-scope") Expect(err).ToNot(HaveOccurred()) Expect(share.Tracks).To(ContainElement(HaveField("ID", "share-ok"))) Expect(share.Tracks).ToNot(ContainElement(HaveField("ID", "share-other")), "a track outside the owner's libraries must not appear in the share") }) It("returns no tracks when the playlist is not visible to the owner", func() { // A private playlist owned by someone else: the share owner can no longer // see it, so Tracks() returns nil. The share must render with no tracks // instead of panicking. privatePlsID := "private-pls" adminCtx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) pr := NewPlaylistRepository(GetDBXBuilder()) privatePls := &model.Playlist{ID: privatePlsID, Name: "Private", OwnerID: adminUser.ID, Public: false} privatePls.AddMediaFiles(model.MediaFiles{{ID: "share-ok"}}) Expect(pr.Put(adminCtx, privatePls)).To(Succeed()) DeferCleanup(func() { _ = pr.Delete(adminCtx, privatePlsID) }) _, err := GetDBXBuilder().NewQuery(` INSERT INTO share (id, user_id, description, resource_type, resource_ids, created_at, updated_at) VALUES ({:id}, {:user}, {:desc}, {:type}, {:ids}, {:created}, {:updated}) `).Bind(map[string]any{ "id": "share-private", "user": owner.ID, "desc": "Private share", "type": "playlist", "ids": privatePlsID, "created": time.Now(), "updated": time.Now(), }).Execute() Expect(err).ToNot(HaveOccurred()) DeferCleanup(func() { _, _ = GetDBXBuilder().NewQuery(`DELETE FROM share WHERE id = 'share-private'`).Execute() }) adminRepo := NewShareRepository(GetDBXBuilder()) share, err := adminRepo.Get(adminCtx, "share-private") Expect(err).ToNot(HaveOccurred()) Expect(share.Tracks).To(BeEmpty()) }) }) Describe("Artist, album and media file share library scoping", func() { var otherLib model.Library var owner model.User const primaryID = "share-aa-primary" const secondaryID = "share-aa-secondary" BeforeEach(func() { adminCtx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) b := GetDBXBuilder() // A second library the owner has no access to lr := NewLibraryRepository(b) otherLib = model.Library{ID: 0, Name: "Artist Share Other Library", Path: "/share/artist/other"} Expect(lr.Put(adminCtx, &otherLib)).To(Succeed()) ar := NewArtistRepository(b) Expect(createArtistWithLibrary(adminCtx, ar, &model.Artist{ID: primaryID, Name: "AA Primary", OrderArtistName: "aa primary"}, 1)).To(Succeed()) Expect(createArtistWithLibrary(adminCtx, ar, &model.Artist{ID: secondaryID, Name: "AA Secondary", OrderArtistName: "aa secondary"}, 1)).To(Succeed()) // Secondary is a co-album-artist (not the first): album_artist_id points at // primary, so the legacy-column filter would miss both tracks. aaParticipants := model.Participants{model.RoleAlbumArtist: { {Artist: model.Artist{ID: primaryID, Name: "AA Primary"}}, {Artist: model.Artist{ID: secondaryID, Name: "AA Secondary"}}, }} alr := NewAlbumRepository(b) Expect(alr.Put(ctx, &model.Album{ID: "art-album-ok", LibraryID: 1, Name: "Art Album OK", AlbumArtistID: primaryID, AlbumArtist: "AA Primary", Participants: aaParticipants})).To(Succeed()) Expect(alr.Put(ctx, &model.Album{ID: "art-album-other", LibraryID: otherLib.ID, Name: "Art Album Other", AlbumArtistID: primaryID, AlbumArtist: "AA Primary", Participants: aaParticipants})).To(Succeed()) mr := NewMediaFileRepository(b) Expect(mr.Put(adminCtx, &model.MediaFile{ID: "art-ok", LibraryID: 1, AlbumID: "art-album-ok", Path: "a/ok.mp3", Title: "ArtOK", AlbumArtistID: primaryID, Participants: aaParticipants})).To(Succeed()) Expect(mr.Put(adminCtx, &model.MediaFile{ID: "art-other", LibraryID: otherLib.ID, AlbumID: "art-album-other", Path: "a/other.mp3", Title: "ArtOther", AlbumArtistID: primaryID, Participants: aaParticipants})).To(Succeed()) // Non-admin owner with access to library 1 only owner = createUserWithLibraries("artist-share-owner", []int{1}) ur := NewUserRepository(b) Expect(ur.Put(adminCtx, &owner)).To(Succeed()) Expect(ur.SetUserLibraries(adminCtx, owner.ID, []int{1})).To(Succeed()) for _, s := range []struct{ id, typ, ids string }{ {"art-share", "artist", secondaryID}, {"art-album-share", "album", "art-album-ok,art-album-other"}, {"art-mf-share", "media_file", "art-ok,art-other"}, } { _, err := b.NewQuery(` INSERT INTO share (id, user_id, description, resource_type, resource_ids, created_at, updated_at) VALUES ({:id}, {:user}, {:desc}, {:type}, {:ids}, {:created}, {:updated}) `).Bind(map[string]any{ "id": s.id, "user": owner.ID, "desc": "Scope share", "type": s.typ, "ids": s.ids, "created": time.Now(), "updated": time.Now(), }).Execute() Expect(err).ToNot(HaveOccurred()) } }) AfterEach(func() { adminCtx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) b := GetDBXBuilder() _, _ = b.NewQuery(`DELETE FROM share WHERE id IN ('art-share', 'art-album-share', 'art-mf-share')`).Execute() mr := NewMediaFileRepository(b).(*mediaFileRepository) _, _ = mr.executeSQL(adminCtx, squirrel.Delete("media_file").Where(squirrel.Eq{"id": []string{"art-ok", "art-other"}})) alr := NewAlbumRepository(b).(*albumRepository) _, _ = alr.executeSQL(adminCtx, squirrel.Delete("album").Where(squirrel.Eq{"id": []string{"art-album-ok", "art-album-other"}})) ar := NewArtistRepository(b).(*artistRepository) _, _ = ar.executeSQL(adminCtx, squirrel.Delete("artist").Where(squirrel.Eq{"id": []string{primaryID, secondaryID}})) lr := NewLibraryRepository(b).(*libraryRepository) _ = lr.delete(adminCtx, squirrel.Eq{"id": otherLib.ID}) _ = NewUserRepository(b).Delete(adminCtx, owner.ID) }) It("includes co-album-artist tracks the owner can access and excludes those they cannot", func() { // Read as admin (mimics the public-share render path); loadMedia must still // scope to the owner's libraries. adminCtx := request.WithUser(ctx, adminUser) adminRepo := NewShareRepository(GetDBXBuilder()) share, err := adminRepo.Get(adminCtx, "art-share") Expect(err).ToNot(HaveOccurred()) Expect(share.Tracks).To(ContainElement(HaveField("ID", "art-ok")), "a co-album-artist track (not matched by album_artist_id) must be included") Expect(share.Tracks).ToNot(ContainElement(HaveField("ID", "art-other")), "a track outside the owner's libraries must not appear in the share") Expect(share.Albums).To(ContainElement(HaveField("ID", "art-album-ok")), "a co-album-artist album must be included") Expect(share.Albums).ToNot(ContainElement(HaveField("ID", "art-album-other")), "an album outside the owner's libraries must not appear in the share") }) It("excludes albums and their tracks outside the owner's libraries from an album share", func() { // Public share rendering has no user in the context. share, err := NewShareRepository(GetDBXBuilder()).Get(log.NewContext(GinkgoT().Context()), "art-album-share") Expect(err).ToNot(HaveOccurred()) Expect(share.Albums).To(ContainElement(HaveField("ID", "art-album-ok"))) Expect(share.Albums).ToNot(ContainElement(HaveField("ID", "art-album-other"))) Expect(share.Tracks).To(ContainElement(HaveField("ID", "art-ok"))) Expect(share.Tracks).ToNot(ContainElement(HaveField("ID", "art-other"))) }) It("excludes tracks outside the owner's libraries from a media file share", func() { share, err := NewShareRepository(GetDBXBuilder()).Get(log.NewContext(GinkgoT().Context()), "art-mf-share") Expect(err).ToNot(HaveOccurred()) Expect(share.Tracks).To(ContainElement(HaveField("ID", "art-ok"))) Expect(share.Tracks).ToNot(ContainElement(HaveField("ID", "art-other"))) }) }) Describe("Ownership Checks", func() { var ownerUser = model.User{ID: "2222", UserName: "regular-user"} var otherUser = model.User{ID: "3333", UserName: "third-user"} insertShare := func(shareID, userID string) { _, err := GetDBXBuilder().NewQuery(` INSERT INTO share (id, user_id, description, resource_type, resource_ids, created_at, updated_at) VALUES ({:id}, {:user}, {:desc}, {:type}, {:ids}, {:created}, {:updated}) `).Bind(map[string]any{ "id": shareID, "user": userID, "desc": "Test Share", "type": "media_file", "ids": "1001", "created": time.Now(), "updated": time.Now(), }).Execute() Expect(err).ToNot(HaveOccurred()) } Describe("Delete", func() { It("allows a non-admin user to delete their own share", func() { insertShare("own-share-del", ownerUser.ID) ctx := request.WithUser(log.NewContext(GinkgoT().Context()), ownerUser) repo := NewShareRepository(GetDBXBuilder()) err := repo.Delete(ctx, "own-share-del") Expect(err).ToNot(HaveOccurred()) }) It("denies a non-admin user from deleting another user's share", func() { insertShare("other-share-del", ownerUser.ID) ctx := request.WithUser(log.NewContext(GinkgoT().Context()), otherUser) repo := NewShareRepository(GetDBXBuilder()) err := repo.Delete(ctx, "other-share-del") Expect(err).To(Equal(rest.ErrPermissionDenied)) // The share was not deleted: the owner can still read it. ownerCtx := request.WithUser(log.NewContext(GinkgoT().Context()), ownerUser) ownerRepo := NewShareRepository(GetDBXBuilder()) _, err = ownerRepo.Read(ownerCtx, "other-share-del") Expect(err).ToNot(HaveOccurred()) }) It("allows an admin to delete any user's share", func() { insertShare("admin-del-share", ownerUser.ID) ctx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) repo := NewShareRepository(GetDBXBuilder()) err := repo.Delete(ctx, "admin-del-share") Expect(err).ToNot(HaveOccurred()) }) It("allows headless context (no user) to delete a share", func() { insertShare("headless-del-share", ownerUser.ID) repo := NewShareRepository(GetDBXBuilder()) err := repo.Delete(GinkgoT().Context(), "headless-del-share") Expect(err).ToNot(HaveOccurred()) }) }) Describe("Save", func() { It("assigns the logged-in user as owner, ignoring a client-supplied UserID", func() { ur := NewUserRepository(GetDBXBuilder()) Expect(ur.Put(ctx, &ownerUser)).To(Succeed()) Expect(ur.Put(ctx, &otherUser)).To(Succeed()) attackerCtx := request.WithUser(log.NewContext(GinkgoT().Context()), ownerUser) attackerRepo := NewShareRepository(GetDBXBuilder()) id, err := attackerRepo.Save(attackerCtx, &model.Share{ ID: "spoof-save-share", UserID: otherUser.ID, ResourceType: "media_file", ResourceIDs: "1001", }) Expect(err).ToNot(HaveOccurred()) adminCtx := request.WithUser(ctx, adminUser) adminRepo := NewShareRepository(GetDBXBuilder()) got, err := adminRepo.Get(adminCtx, id) Expect(err).ToNot(HaveOccurred()) Expect(got.UserID).To(Equal(ownerUser.ID)) }) }) Describe("Update", func() { It("allows a non-admin user to update their own share", func() { insertShare("own-share-upd", ownerUser.ID) ctx := request.WithUser(log.NewContext(GinkgoT().Context()), ownerUser) repo := NewShareRepository(GetDBXBuilder()) err := repo.Update(ctx, "own-share-upd", model.Share{Description: "Updated"}, "description") Expect(err).ToNot(HaveOccurred()) }) It("denies a non-admin user from updating another user's share", func() { insertShare("other-share-upd", ownerUser.ID) ctx := request.WithUser(log.NewContext(GinkgoT().Context()), otherUser) repo := NewShareRepository(GetDBXBuilder()) err := repo.Update(ctx, "other-share-upd", model.Share{Description: "Hacked"}, "description") Expect(err).To(Equal(rest.ErrPermissionDenied)) }) It("allows an admin to update any user's share", func() { insertShare("admin-upd-share", ownerUser.ID) ctx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) repo := NewShareRepository(GetDBXBuilder()) err := repo.Update(ctx, "admin-upd-share", model.Share{Description: "Admin Updated"}, "description") Expect(err).ToNot(HaveOccurred()) }) It("allows headless context (no user) to update a share", func() { insertShare("headless-upd-share", ownerUser.ID) repo := NewShareRepository(GetDBXBuilder()) err := repo.Update(GinkgoT().Context(), "headless-upd-share", model.Share{Description: "Headless"}, "description") Expect(err).ToNot(HaveOccurred()) }) It("returns not found when updating a nonexistent share", func() { ctx := request.WithUser(log.NewContext(context.TODO()), ownerUser) repo := NewShareRepository(GetDBXBuilder()) err := repo.Update(ctx, "does-not-exist", model.Share{Description: "Ghost"}, "description") Expect(err).To(Equal(rest.ErrNotFound)) }) It("updates all columns when no specific columns are given", func() { insertShare("all-cols-share", ownerUser.ID) ctx := request.WithUser(log.NewContext(context.TODO()), ownerUser) repo := NewShareRepository(GetDBXBuilder()) // No cols: the update must write every column, not just updated_at. err := repo.Update(ctx, "all-cols-share", model.Share{Description: "All Updated", MaxBitRate: 192, ResourceType: "album", ResourceIDs: "2002"}) Expect(err).ToNot(HaveOccurred()) share, err := repo.Read(ctx, "all-cols-share") Expect(err).ToNot(HaveOccurred()) Expect(share.Description).To(Equal("All Updated")) Expect(share.MaxBitRate).To(Equal(192)) Expect(share.ResourceType).To(Equal("album")) }) It("does not let an owner reassign their share to another user", func() { insertShare("reassign-share", ownerUser.ID) ctx := request.WithUser(log.NewContext(context.TODO()), ownerUser) repo := NewShareRepository(GetDBXBuilder()) err := repo.Update(ctx, "reassign-share", model.Share{UserID: otherUser.ID, Description: "Given away"}, "user_id", "description") Expect(err).ToNot(HaveOccurred()) // Ownership must not have moved, even though user_id was passed in the body and cols. got, err := repo.Read(ctx, "reassign-share") Expect(err).ToNot(HaveOccurred()) Expect(got.UserID).To(Equal(ownerUser.ID)) }) }) Describe("Read scoping", func() { BeforeEach(func() { // Persist owner/other users so the JOIN in selectShare resolves. ur := NewUserRepository(GetDBXBuilder()) Expect(ur.Put(ctx, &ownerUser)).To(Succeed()) Expect(ur.Put(ctx, &otherUser)).To(Succeed()) insertShare("share-owner-1", ownerUser.ID) insertShare("share-owner-2", ownerUser.ID) insertShare("share-other-1", otherUser.ID) }) Context("non-admin user", func() { var nonAdminRepo model.ShareRepository var nonAdminCtx context.Context BeforeEach(func() { nonAdminCtx = request.WithUser(ctx, ownerUser) nonAdminRepo = NewShareRepository(GetDBXBuilder()) }) It("GetAll returns only own shares", func() { shares, err := nonAdminRepo.GetAll(nonAdminCtx) Expect(err).ToNot(HaveOccurred()) ids := make([]string, len(shares)) for i, s := range shares { ids[i] = s.ID } Expect(ids).To(ConsistOf("share-owner-1", "share-owner-2")) }) It("ReadAll returns only own shares", func() { shares, err := nonAdminRepo.ReadAll(nonAdminCtx) Expect(err).ToNot(HaveOccurred()) ids := make([]string, len(shares)) for i, s := range shares { ids[i] = s.ID } Expect(ids).To(ConsistOf("share-owner-1", "share-owner-2")) }) It("Get returns own share", func() { s, err := nonAdminRepo.Get(nonAdminCtx, "share-owner-1") Expect(err).ToNot(HaveOccurred()) Expect(s.ID).To(Equal("share-owner-1")) }) It("Get returns ErrNotFound for another user's share", func() { _, err := nonAdminRepo.Get(nonAdminCtx, "share-other-1") Expect(err).To(MatchError(model.ErrNotFound)) }) It("Read returns ErrNotFound for another user's share", func() { _, err := nonAdminRepo.Read(nonAdminCtx, "share-other-1") Expect(err).To(MatchError(model.ErrNotFound)) }) It("Exists returns true for own share", func() { exists, err := nonAdminRepo.Exists(nonAdminCtx, "share-owner-1") Expect(err).ToNot(HaveOccurred()) Expect(exists).To(BeTrue()) }) It("Exists returns false for another user's share", func() { exists, err := nonAdminRepo.Exists(nonAdminCtx, "share-other-1") Expect(err).ToNot(HaveOccurred()) Expect(exists).To(BeFalse()) }) It("CountAll counts only own shares", func() { count, err := nonAdminRepo.CountAll(nonAdminCtx) Expect(err).ToNot(HaveOccurred()) Expect(count).To(BeNumerically("==", 2)) }) It("Count (rest) counts only own shares", func() { count, err := nonAdminRepo.Count(nonAdminCtx) Expect(err).ToNot(HaveOccurred()) Expect(count).To(BeNumerically("==", 2)) }) }) Context("admin user", func() { It("GetAll returns all shares", func() { adminCtx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) adminRepo := NewShareRepository(GetDBXBuilder()) shares, err := adminRepo.GetAll(adminCtx) Expect(err).ToNot(HaveOccurred()) ids := make([]string, len(shares)) for i, s := range shares { ids[i] = s.ID } Expect(ids).To(ConsistOf("share-owner-1", "share-owner-2", "share-other-1")) }) It("CountAll counts all shares", func() { adminCtx := request.WithUser(log.NewContext(GinkgoT().Context()), adminUser) adminRepo := NewShareRepository(GetDBXBuilder()) count, err := adminRepo.CountAll(adminCtx) Expect(err).ToNot(HaveOccurred()) Expect(count).To(BeNumerically("==", 3)) }) }) Context("headless context (public share route)", func() { var headlessCtx context.Context BeforeEach(func() { headlessCtx = GinkgoT().Context() }) It("GetAll returns all shares", func() { headlessRepo := NewShareRepository(GetDBXBuilder()) shares, err := headlessRepo.GetAll(headlessCtx) Expect(err).ToNot(HaveOccurred()) Expect(shares).To(HaveLen(3)) }) It("Get returns another user's share", func() { headlessRepo := NewShareRepository(GetDBXBuilder()) s, err := headlessRepo.Get(headlessCtx, "share-other-1") Expect(err).ToNot(HaveOccurred()) Expect(s.ID).To(Equal("share-other-1")) }) It("Exists returns true for any share", func() { headlessRepo := NewShareRepository(GetDBXBuilder()) exists, err := headlessRepo.Exists(headlessCtx, "share-other-1") Expect(err).ToNot(HaveOccurred()) Expect(exists).To(BeTrue()) }) }) }) }) })