From ab2c77f7b2c528c5600d36e2e198f3a48452e659 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 7 Sep 2026 19:05:14 -0400 Subject: [PATCH] feat(persistence): add UserRepository.UpdateImage Adds the interface method, real repository implementation, and mock together so the build stays green. UpdateImage sets updated_at explicitly in the same UPDATE statement, since AvatarTag's ETag derives from uploaded_image + updated_at. --- model/user.go | 2 ++ persistence/user_repository.go | 15 +++++++++++ persistence/user_repository_test.go | 40 +++++++++++++++++++++++++++++ tests/mock_user_repo.go | 14 ++++++++++ 4 files changed, 71 insertions(+) diff --git a/model/user.go b/model/user.go index 3ea3985e6..452b60b0a 100644 --- a/model/user.go +++ b/model/user.go @@ -73,6 +73,8 @@ type UserRepository interface { Get(id string) (*User, error) GetAll(options ...QueryOptions) (Users, error) Put(*User) error + // UpdateImage is the only writer of uploaded_image. See the field comment on User. + UpdateImage(id string, filename string) error UpdateLastLoginAt(id string) error UpdateLastAccessAt(id string) error FindFirstAdmin() (*User, error) diff --git a/persistence/user_repository.go b/persistence/user_repository.go index 9de37876b..df53a34f3 100644 --- a/persistence/user_repository.go +++ b/persistence/user_repository.go @@ -189,6 +189,21 @@ func (r *userRepository) Put(u *model.User) error { return nil } +func (r *userRepository) UpdateImage(id string, filename string) error { + upd := Update(r.tableName). + Set("uploaded_image", filename). + Set("updated_at", time.Now()). + Where(Eq{"id": id}) + count, err := r.executeSQL(upd) + if err != nil { + return err + } + if count == 0 { + return model.ErrNotFound + } + return nil +} + func (r *userRepository) FindFirstAdmin() (*model.User, error) { sel := r.selectUserWithLibraries(model.QueryOptions{Sort: "updated_at", Max: 1}).Where(Eq{"user.is_admin": true}) var usr dbUser diff --git a/persistence/user_repository_test.go b/persistence/user_repository_test.go index dc519d0a1..69f136a15 100644 --- a/persistence/user_repository_test.go +++ b/persistence/user_repository_test.go @@ -5,6 +5,7 @@ import ( "errors" "slices" "sync" + "time" "github.com/Masterminds/squirrel" "github.com/deluan/rest" @@ -94,6 +95,45 @@ var _ = Describe("UserRepository", func() { }) }) + Describe("UpdateImage", func() { + It("stores the filename and bumps updated_at", func() { + before := time.Now().Add(-time.Hour) + Expect(repo.UpdateImage(adminUser.ID, "u1_admin.png")).To(Succeed()) + + usr, err := repo.Get(adminUser.ID) + Expect(err).ToNot(HaveOccurred()) + Expect(usr.UploadedImage).To(Equal("u1_admin.png")) + Expect(usr.UpdatedAt).To(BeTemporally(">", before)) + }) + + It("is not erased by a later full-row Put", func() { + Expect(repo.UpdateImage(adminUser.ID, "u1_admin.png")).To(Succeed()) + + usr, err := repo.Get(adminUser.ID) + Expect(err).ToNot(HaveOccurred()) + usr.Name = "Renamed" + Expect(repo.Put(usr)).To(Succeed()) + + usr, err = repo.Get(adminUser.ID) + Expect(err).ToNot(HaveOccurred()) + Expect(usr.UploadedImage).To(Equal("u1_admin.png")) + }) + + It("clears the filename when given an empty string", func() { + Expect(repo.UpdateImage(adminUser.ID, "u1_admin.png")).To(Succeed()) + Expect(repo.UpdateImage(adminUser.ID, "")).To(Succeed()) + + usr, err := repo.Get(adminUser.ID) + Expect(err).ToNot(HaveOccurred()) + Expect(usr.UploadedImage).To(BeEmpty()) + }) + + It("returns ErrNotFound for an unknown id", func() { + err := repo.UpdateImage("no-such-user", "x.png") + Expect(err).To(MatchError(model.ErrNotFound)) + }) + }) + Describe("validatePasswordChange", func() { var loggedUser *model.User diff --git a/tests/mock_user_repo.go b/tests/mock_user_repo.go index 2d6ff3c02..7bd32d229 100644 --- a/tests/mock_user_repo.go +++ b/tests/mock_user_repo.go @@ -92,6 +92,20 @@ func (u *MockedUserRepo) GetAll(options ...model.QueryOptions) (model.Users, err return users, nil } +func (u *MockedUserRepo) UpdateImage(id string, filename string) error { + if u.Error != nil { + return u.Error + } + for _, usr := range u.Data { + if usr.ID == id { + usr.UploadedImage = filename + usr.UpdatedAt = time.Now() + return nil + } + } + return model.ErrNotFound +} + func (u *MockedUserRepo) UpdateLastLoginAt(id string) error { for _, usr := range u.Data { if usr.ID == id {