🏡 index : quine-global/quintodrome.git

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())
				})
			})
		})
	})
})