mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-27 02:37:21 +01:00
feat(database): add user_library table and library access methods
Signed-off-by: Deluan <deluan@navidrome.org> # Conflicts: # tests/mock_library_repo.go
This commit is contained in:
parent
3c1e5603d0
commit
40bbae16c4
9 changed files with 533 additions and 6 deletions
|
|
@ -145,3 +145,68 @@ func getEncKey() []byte {
|
|||
sum := sha256.Sum256([]byte(key))
|
||||
return sum[:]
|
||||
}
|
||||
|
||||
// Library-based permission helpers
|
||||
|
||||
// HasLibraryAccess checks if a user has access to a specific library
|
||||
func HasLibraryAccess(ctx context.Context, ds model.DataStore, userID string, libraryID int) bool {
|
||||
user, ok := request.UserFrom(ctx)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Admin users have access to all libraries
|
||||
if user.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if user has explicit access to this library
|
||||
libraries, err := ds.User(ctx).GetUserLibraries(userID)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error checking library access", "userID", userID, "libraryID", libraryID, err)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, lib := range libraries {
|
||||
if lib.ID == libraryID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetUserLibraryIDs returns a list of library IDs the user has access to
|
||||
func GetUserLibraryIDs(ctx context.Context, ds model.DataStore, userID string) []int {
|
||||
user, ok := request.UserFrom(ctx)
|
||||
if !ok {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Admin users have access to all libraries
|
||||
if user.IsAdmin {
|
||||
libraries, err := ds.Library(ctx).GetAll()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error getting all libraries for admin", "userID", userID, err)
|
||||
return []int{}
|
||||
}
|
||||
var ids []int
|
||||
for _, lib := range libraries {
|
||||
ids = append(ids, lib.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// Get user's accessible libraries
|
||||
libraries, err := ds.User(ctx).GetUserLibraries(userID)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error getting user libraries", "userID", userID, err)
|
||||
return []int{}
|
||||
}
|
||||
|
||||
var ids []int
|
||||
for _, lib := range libraries {
|
||||
ids = append(ids, lib.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
|
|
|||
56
db/migrations/20250106113337_add_user_library_table.go
Normal file
56
db/migrations/20250106113337_add_user_library_table.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upAddUserLibraryTable, downAddUserLibraryTable)
|
||||
}
|
||||
|
||||
func upAddUserLibraryTable(ctx context.Context, tx *sql.Tx) error {
|
||||
// Create user_library association table
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
CREATE TABLE user_library (
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
library_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, library_id),
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (library_id) REFERENCES library(id) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create indexes for performance
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
CREATE INDEX idx_user_library_user_id ON user_library(user_id);
|
||||
CREATE INDEX idx_user_library_library_id ON user_library(library_id);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Populate with existing users having access to library ID 1 (existing setup)
|
||||
// Admin users get access to all libraries, regular users get access to library 1
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO user_library (user_id, library_id)
|
||||
SELECT u.id, 1
|
||||
FROM user u;
|
||||
`)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func downAddUserLibraryTable(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
DROP INDEX IF EXISTS idx_user_library_library_id;
|
||||
DROP INDEX IF EXISTS idx_user_library_user_id;
|
||||
DROP TABLE IF EXISTS user_library;
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
|
@ -36,6 +36,9 @@ type LibraryRepository interface {
|
|||
StoreMusicFolder() error
|
||||
AddArtist(id int, artistID string) error
|
||||
|
||||
// User-library association methods
|
||||
GetUsersWithLibraryAccess(libraryID int) (Users, error)
|
||||
|
||||
// TODO These methods should be moved to a core service
|
||||
ScanBegin(id int, fullScan bool) error
|
||||
ScanEnd(id int) error
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ type User struct {
|
|||
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
|
||||
|
||||
// Library associations (many-to-many relationship)
|
||||
Libraries []Library `structs:"-" json:"libraries,omitempty"`
|
||||
|
||||
// This is only available on the backend, and it is never sent over the wire
|
||||
Password string `structs:"-" json:"-"`
|
||||
// This is used to set or change a password when calling Put. If it is empty, the password is not changed.
|
||||
|
|
@ -35,4 +38,10 @@ type UserRepository interface {
|
|||
FindByUsername(username string) (*User, error)
|
||||
// FindByUsernameWithPassword is the same as above, but also returns the decrypted password
|
||||
FindByUsernameWithPassword(username string) (*User, error)
|
||||
|
||||
// Library association methods
|
||||
GetUserLibraries(userID string) (Libraries, error)
|
||||
SetUserLibraries(userID string, libraryIDs []int) error
|
||||
AddUserLibrary(userID string, libraryID int) error
|
||||
RemoveUserLibrary(userID string, libraryID int) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package persistence
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -87,6 +88,17 @@ func (r *libraryRepository) Put(l *model.Library) error {
|
|||
defer libLock.Unlock()
|
||||
libCache[l.ID] = l.Path
|
||||
}
|
||||
|
||||
// Auto-assign library to all admin users
|
||||
if err == nil {
|
||||
sql := Expr(
|
||||
"INSERT INTO user_library (user_id, library_id) SELECT id, ? FROM user WHERE is_admin = true ON CONFLICT (user_id, library_id) DO NOTHING",
|
||||
l.ID,
|
||||
)
|
||||
if _, addErr := r.executeSQL(sql); addErr != nil {
|
||||
return fmt.Errorf("failed to assign library to admin users: %w", addErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -206,4 +218,18 @@ func (r *libraryRepository) GetAll(ops ...model.QueryOptions) (model.Libraries,
|
|||
return res, err
|
||||
}
|
||||
|
||||
// User-library association methods
|
||||
|
||||
func (r *libraryRepository) GetUsersWithLibraryAccess(libraryID int) (model.Users, error) {
|
||||
sel := Select("u.*").
|
||||
From("user u").
|
||||
Join("user_library ul ON u.id = ul.user_id").
|
||||
Where(Eq{"ul.library_id": libraryID}).
|
||||
OrderBy("u.name")
|
||||
|
||||
var res model.Users
|
||||
err := r.queryAll(sel, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
var _ model.LibraryRepository = (*libraryRepository)(nil)
|
||||
|
|
|
|||
|
|
@ -79,18 +79,34 @@ func (r *userRepository) Put(u *model.User) error {
|
|||
return fmt.Errorf("error converting user to SQL args: %w", err)
|
||||
}
|
||||
delete(values, "current_password")
|
||||
|
||||
// Save/update the user
|
||||
update := Update(r.tableName).Where(Eq{"id": u.ID}).SetMap(values)
|
||||
count, err := r.executeSQL(update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
if count == 0 {
|
||||
values["created_at"] = time.Now()
|
||||
insert := Insert(r.tableName).SetMap(values)
|
||||
_, err = r.executeSQL(insert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
values["created_at"] = time.Now()
|
||||
insert := Insert(r.tableName).SetMap(values)
|
||||
_, err = r.executeSQL(insert)
|
||||
return err
|
||||
|
||||
// Auto-assign all libraries to admin users in a single SQL operation
|
||||
if u.IsAdmin {
|
||||
sql := Expr(
|
||||
"INSERT OR IGNORE INTO user_library (user_id, library_id) SELECT ?, id FROM library",
|
||||
u.ID,
|
||||
)
|
||||
if _, err := r.executeSQL(sql); err != nil {
|
||||
return fmt.Errorf("failed to assign all libraries to admin user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) FindFirstAdmin() (*model.User, error) {
|
||||
|
|
@ -365,6 +381,57 @@ func (r *userRepository) decryptAllPasswords(users model.Users) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Library association methods
|
||||
|
||||
func (r *userRepository) GetUserLibraries(userID string) (model.Libraries, error) {
|
||||
sel := Select("l.*").
|
||||
From("library l").
|
||||
Join("user_library ul ON l.id = ul.library_id").
|
||||
Where(Eq{"ul.user_id": userID}).
|
||||
OrderBy("l.name")
|
||||
|
||||
var res model.Libraries
|
||||
err := r.queryAll(sel, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r *userRepository) SetUserLibraries(userID string, libraryIDs []int) error {
|
||||
// Remove existing associations
|
||||
delSql := Delete("user_library").Where(Eq{"user_id": userID})
|
||||
if _, err := r.executeSQL(delSql); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add new associations
|
||||
if len(libraryIDs) > 0 {
|
||||
insert := Insert("user_library").Columns("user_id", "library_id")
|
||||
for _, libID := range libraryIDs {
|
||||
insert = insert.Values(userID, libID)
|
||||
}
|
||||
_, err := r.executeSQL(insert)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) AddUserLibrary(userID string, libraryID int) error {
|
||||
insert := Insert("user_library").
|
||||
Columns("user_id", "library_id").
|
||||
Values(userID, libraryID).
|
||||
Suffix("ON CONFLICT (user_id, library_id) DO NOTHING")
|
||||
_, err := r.executeSQL(insert)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *userRepository) RemoveUserLibrary(userID string, libraryID int) error {
|
||||
del := Delete("user_library").Where(And{
|
||||
Eq{"user_id": userID},
|
||||
Eq{"library_id": libraryID},
|
||||
})
|
||||
_, err := r.executeSQL(del)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ model.UserRepository = (*userRepository)(nil)
|
||||
var _ rest.Repository = (*userRepository)(nil)
|
||||
var _ rest.Persistable = (*userRepository)(nil)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
|
|
@ -235,4 +236,260 @@ var _ = Describe("UserRepository", func() {
|
|||
Expect(err).To(MatchError("fake error"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Library Association Methods", func() {
|
||||
var userID string
|
||||
var library1, library2 model.Library
|
||||
|
||||
BeforeEach(func() {
|
||||
// Create a test user first to satisfy foreign key constraints
|
||||
testUser := model.User{
|
||||
ID: "test-user-id",
|
||||
UserName: "testuser",
|
||||
Name: "Test User",
|
||||
Email: "test@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: false,
|
||||
}
|
||||
Expect(repo.Put(&testUser)).To(BeNil())
|
||||
userID = testUser.ID
|
||||
|
||||
library1 = model.Library{ID: 500, Name: "Library 500", Path: "/path/500"}
|
||||
library2 = model.Library{ID: 501, Name: "Library 501", Path: "/path/501"}
|
||||
|
||||
// Create test libraries
|
||||
libRepo := NewLibraryRepository(log.NewContext(context.TODO()), GetDBXBuilder())
|
||||
Expect(libRepo.Put(&library1)).To(BeNil())
|
||||
Expect(libRepo.Put(&library2)).To(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up user-library associations to ensure test isolation
|
||||
_ = repo.SetUserLibraries(userID, []int{})
|
||||
|
||||
// Clean up test libraries to ensure isolation between test groups
|
||||
libRepo := NewLibraryRepository(log.NewContext(context.TODO()), GetDBXBuilder())
|
||||
_ = libRepo.(*libraryRepository).delete(squirrel.Eq{"id": []int{500, 501}})
|
||||
})
|
||||
|
||||
Describe("AddUserLibrary", func() {
|
||||
It("adds a library association for a user", func() {
|
||||
err := repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
Expect(libraries[0].ID).To(Equal(library1.ID))
|
||||
})
|
||||
|
||||
It("does not create duplicate associations", func() {
|
||||
err := repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Add the same association again
|
||||
err = repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetUserLibraries", func() {
|
||||
It("returns empty list when user has no library associations", func() {
|
||||
libraries, err := repo.GetUserLibraries("non-existent-user")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
|
||||
It("returns user's associated libraries", func() {
|
||||
err := repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
err = repo.AddUserLibrary(userID, library2.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(2))
|
||||
|
||||
libIDs := []int{libraries[0].ID, libraries[1].ID}
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("SetUserLibraries", func() {
|
||||
It("sets user's library associations", func() {
|
||||
libraryIDs := []int{library1.ID, library2.ID}
|
||||
err := repo.SetUserLibraries(userID, libraryIDs)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("replaces existing associations", func() {
|
||||
// Set initial associations
|
||||
err := repo.SetUserLibraries(userID, []int{library1.ID, library2.ID})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Replace with just one library
|
||||
err = repo.SetUserLibraries(userID, []int{library1.ID})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
Expect(libraries[0].ID).To(Equal(library1.ID))
|
||||
})
|
||||
|
||||
It("removes all associations when passed empty slice", func() {
|
||||
// Set initial associations
|
||||
err := repo.SetUserLibraries(userID, []int{library1.ID, library2.ID})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Remove all
|
||||
err = repo.SetUserLibraries(userID, []int{})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("RemoveUserLibrary", func() {
|
||||
It("removes a library association", func() {
|
||||
err := repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
err = repo.AddUserLibrary(userID, library2.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
err = repo.RemoveUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
Expect(libraries[0].ID).To(Equal(library2.ID))
|
||||
})
|
||||
|
||||
It("does nothing when association doesn't exist", func() {
|
||||
err := repo.RemoveUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Admin User Auto-Assignment", func() {
|
||||
var (
|
||||
libRepo model.LibraryRepository
|
||||
library1 model.Library
|
||||
library2 model.Library
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
libRepo = NewLibraryRepository(log.NewContext(context.TODO()), GetDBXBuilder())
|
||||
library1 = model.Library{ID: 201, Name: "Admin Test Library 1", Path: "/admin/test/path1"}
|
||||
library2 = model.Library{ID: 202, Name: "Admin Test Library 2", Path: "/admin/test/path2"}
|
||||
|
||||
// Create test libraries
|
||||
Expect(libRepo.Put(&library1)).To(BeNil())
|
||||
Expect(libRepo.Put(&library2)).To(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up test libraries and their associations
|
||||
_ = libRepo.(*libraryRepository).delete(squirrel.Eq{"id": []int{201, 202}})
|
||||
|
||||
// Clean up user-library associations for these test libraries
|
||||
_, _ = repo.(*userRepository).executeSQL(squirrel.Delete("user_library").Where(squirrel.Eq{"library_id": []int{201, 202}}))
|
||||
})
|
||||
|
||||
It("automatically assigns all libraries to admin users when created", func() {
|
||||
adminUser := model.User{
|
||||
ID: "admin-user-id-1",
|
||||
UserName: "adminuser1",
|
||||
Name: "Admin User",
|
||||
Email: "admin1@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
err := repo.Put(&adminUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Admin should automatically have access to all libraries (including existing ones)
|
||||
libraries, err := repo.GetUserLibraries(adminUser.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(3)) // Our 2 test libraries + existing library ID 1
|
||||
|
||||
libIDs := make([]int, len(libraries))
|
||||
for i, lib := range libraries {
|
||||
libIDs[i] = lib.ID
|
||||
}
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID, 1))
|
||||
})
|
||||
|
||||
It("automatically assigns all libraries to admin users when updated", func() {
|
||||
// Create regular user first
|
||||
regularUser := model.User{
|
||||
ID: "regular-user-id-1",
|
||||
UserName: "regularuser1",
|
||||
Name: "Regular User",
|
||||
Email: "regular1@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
err := repo.Put(®ularUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Give them access to just one library
|
||||
err = repo.SetUserLibraries(regularUser.ID, []int{library1.ID})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Promote to admin
|
||||
regularUser.IsAdmin = true
|
||||
err = repo.Put(®ularUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Should now have access to all libraries (including existing ones)
|
||||
libraries, err := repo.GetUserLibraries(regularUser.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(3)) // Our 2 test libraries + existing library ID 1
|
||||
|
||||
libIDs := make([]int, len(libraries))
|
||||
for i, lib := range libraries {
|
||||
libIDs[i] = lib.ID
|
||||
}
|
||||
// Should include our test libraries plus all existing ones
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID, 1))
|
||||
})
|
||||
|
||||
It("does not affect regular users", func() {
|
||||
regularUser := model.User{
|
||||
ID: "regular-user-id-2",
|
||||
UserName: "regularuser2",
|
||||
Name: "Regular User",
|
||||
Email: "regular2@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
err := repo.Put(®ularUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Regular user should have no library associations
|
||||
libraries, err := repo.GetUserLibraries(regularUser.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -39,4 +39,14 @@ func (m *MockLibraryRepo) RefreshStats(id int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// User-library association methods - mock implementations
|
||||
|
||||
func (m *MockLibraryRepo) GetUsersWithLibraryAccess(libraryID int) (model.Users, error) {
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
// Mock: return empty users for now
|
||||
return model.Users{}, nil
|
||||
}
|
||||
|
||||
var _ model.LibraryRepository = &MockLibraryRepo{}
|
||||
|
|
|
|||
|
|
@ -74,3 +74,37 @@ func (u *MockedUserRepo) UpdateLastAccessAt(id string) error {
|
|||
}
|
||||
return u.Error
|
||||
}
|
||||
|
||||
// Library association methods - mock implementations
|
||||
|
||||
func (u *MockedUserRepo) GetUserLibraries(userID string) (model.Libraries, error) {
|
||||
if u.Error != nil {
|
||||
return nil, u.Error
|
||||
}
|
||||
// Mock: return empty libraries for now
|
||||
return model.Libraries{}, nil
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) SetUserLibraries(userID string, libraryIDs []int) error {
|
||||
if u.Error != nil {
|
||||
return u.Error
|
||||
}
|
||||
// Mock: do nothing for now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) AddUserLibrary(userID string, libraryID int) error {
|
||||
if u.Error != nil {
|
||||
return u.Error
|
||||
}
|
||||
// Mock: do nothing for now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MockedUserRepo) RemoveUserLibrary(userID string, libraryID int) error {
|
||||
if u.Error != nil {
|
||||
return u.Error
|
||||
}
|
||||
// Mock: do nothing for now
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue