From 9e190e6a0eeaecce14762e88fda9434ee7679280 Mon Sep 17 00:00:00 2001 From: yifen9 Date: Sat, 16 May 2026 00:49:49 +0200 Subject: [PATCH 1/3] user not found Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- internal/storage/postgres/user_repository.go | 6 ++---- internal/user/repository.go | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/storage/postgres/user_repository.go b/internal/storage/postgres/user_repository.go index 524f998..e840b61 100644 --- a/internal/storage/postgres/user_repository.go +++ b/internal/storage/postgres/user_repository.go @@ -8,8 +8,6 @@ import ( "github.com/gamidoc/backend/internal/user" ) -var ErrUserNotFound = errors.New("user not found") - type UserRepository struct { db *DB } @@ -55,7 +53,7 @@ func (r *UserRepository) FindByEmail(ctx context.Context, email string) (user.Us err := row.Scan(&found.ID, &found.Email, &found.PasswordHash, &found.CreatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return user.User{}, ErrUserNotFound + return user.User{}, user.ErrUserNotFound } return user.User{}, err } @@ -78,7 +76,7 @@ func (r *UserRepository) FindByID(ctx context.Context, id string) (user.User, er err := row.Scan(&found.ID, &found.Email, &found.PasswordHash, &found.CreatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return user.User{}, ErrUserNotFound + return user.User{}, user.ErrUserNotFound } return user.User{}, err } diff --git a/internal/user/repository.go b/internal/user/repository.go index 8737566..02466ce 100644 --- a/internal/user/repository.go +++ b/internal/user/repository.go @@ -1,6 +1,11 @@ package user -import "context" +import ( + "context" + "errors" +) + +var ErrUserNotFound = errors.New("user not found") type Repository interface { Create(ctx context.Context, user User) (User, error) From 257b7cf9934960d6d5a418c42bca964967dc9db8 Mon Sep 17 00:00:00 2001 From: yifen9 Date: Sat, 16 May 2026 00:50:02 +0200 Subject: [PATCH 2/3] auth errors Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- internal/auth/handler.go | 6 ++-- internal/auth/service.go | 6 ++++ internal/auth/service_test.go | 35 +++++++++++++++++++---- internal/http/router_test_helpers_test.go | 5 ++-- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/internal/auth/handler.go b/internal/auth/handler.go index a999c8e..85f2109 100644 --- a/internal/auth/handler.go +++ b/internal/auth/handler.go @@ -7,8 +7,8 @@ import ( appmiddleware "github.com/gamidoc/backend/internal/http/middleware" "github.com/gamidoc/backend/internal/http/response" - "github.com/gamidoc/backend/internal/storage/postgres" "github.com/gamidoc/backend/internal/token" + "github.com/gamidoc/backend/internal/user" "github.com/go-chi/chi/v5" ) @@ -69,7 +69,7 @@ func (h *Handler) login(w http.ResponseWriter, r *http.Request) { result, err := h.service.Login(r.Context(), input) if err != nil { switch { - case errors.Is(err, ErrInvalidCredentials), errors.Is(err, postgres.ErrUserNotFound): + case errors.Is(err, ErrInvalidCredentials), errors.Is(err, user.ErrUserNotFound): response.WriteError(w, http.StatusUnauthorized, "INVALID_CREDENTIALS", "Invalid credentials", nil) default: response.WriteError(w, http.StatusInternalServerError, "INTERNAL_SERVER_ERROR", "Internal server error", nil) @@ -89,7 +89,7 @@ func (h *Handler) me(w http.ResponseWriter, r *http.Request) { currentUser, err := h.service.Me(r.Context(), userID) if err != nil { - if errors.Is(err, postgres.ErrUserNotFound) { + if errors.Is(err, user.ErrUserNotFound) { response.WriteError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unauthorized", nil) return } diff --git a/internal/auth/service.go b/internal/auth/service.go index 17c4935..c3643b1 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -60,6 +60,9 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (AuthResult if err == nil { return AuthResult{}, ErrEmailAlreadyExists } + if !errors.Is(err, user.ErrUserNotFound) { + return AuthResult{}, err + } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { @@ -92,6 +95,9 @@ func (s *Service) Login(ctx context.Context, input LoginInput) (AuthResult, erro foundUser, err := s.users.FindByEmail(ctx, email) if err != nil { + if !errors.Is(err, user.ErrUserNotFound) { + return AuthResult{}, err + } return AuthResult{}, ErrInvalidCredentials } diff --git a/internal/auth/service_test.go b/internal/auth/service_test.go index 044f6b0..efb74d8 100644 --- a/internal/auth/service_test.go +++ b/internal/auth/service_test.go @@ -11,12 +11,15 @@ import ( ) type fakeUserRepository struct { - usersByEmail map[string]user.User - usersByID map[string]user.User - createErr error + usersByEmail map[string]user.User + usersByID map[string]user.User + createErr error + findByEmailErr error + createCalls int } func (r *fakeUserRepository) Create(ctx context.Context, input user.User) (user.User, error) { + r.createCalls++ if r.createErr != nil { return user.User{}, r.createErr } @@ -33,9 +36,12 @@ func (r *fakeUserRepository) Create(ctx context.Context, input user.User) (user. } func (r *fakeUserRepository) FindByEmail(ctx context.Context, email string) (user.User, error) { + if r.findByEmailErr != nil { + return user.User{}, r.findByEmailErr + } u, ok := r.usersByEmail[email] if !ok { - return user.User{}, errors.New("not found") + return user.User{}, user.ErrUserNotFound } return u, nil } @@ -43,7 +49,7 @@ func (r *fakeUserRepository) FindByEmail(ctx context.Context, email string) (use func (r *fakeUserRepository) FindByID(ctx context.Context, id string) (user.User, error) { u, ok := r.usersByID[id] if !ok { - return user.User{}, errors.New("not found") + return user.User{}, user.ErrUserNotFound } return u, nil } @@ -73,6 +79,25 @@ func TestRegister(t *testing.T) { } } +func TestRegisterReturnsFindByEmailError(t *testing.T) { + lookupErr := errors.New("lookup failed") + repo := &fakeUserRepository{findByEmailErr: lookupErr} + tokens := token.NewManager("secret", time.Hour) + service := NewService(repo, tokens) + + _, err := service.Register(context.Background(), RegisterInput{ + Email: "test@example.com", + Password: "password123", + }) + if !errors.Is(err, lookupErr) { + t.Fatalf("expected lookup error, got %v", err) + } + + if repo.createCalls != 0 { + t.Fatalf("expected Create not to be called, got %d calls", repo.createCalls) + } +} + func TestLogin(t *testing.T) { repo := &fakeUserRepository{ usersByEmail: map[string]user.User{}, diff --git a/internal/http/router_test_helpers_test.go b/internal/http/router_test_helpers_test.go index dd2d6b7..8ec802b 100644 --- a/internal/http/router_test_helpers_test.go +++ b/internal/http/router_test_helpers_test.go @@ -2,7 +2,6 @@ package http import ( "context" - "errors" "log/slog" "net/http" "os" @@ -57,7 +56,7 @@ func (r *fakeUserRepository) Create(ctx context.Context, input user.User) (user. func (r *fakeUserRepository) FindByEmail(ctx context.Context, email string) (user.User, error) { u, ok := r.usersByEmail[email] if !ok { - return user.User{}, errors.New("not found") + return user.User{}, user.ErrUserNotFound } return u, nil } @@ -65,7 +64,7 @@ func (r *fakeUserRepository) FindByEmail(ctx context.Context, email string) (use func (r *fakeUserRepository) FindByID(ctx context.Context, id string) (user.User, error) { u, ok := r.usersByID[id] if !ok { - return user.User{}, errors.New("not found") + return user.User{}, user.ErrUserNotFound } return u, nil } From 0b49b889592768e51abbbeaf036d6929cae2fce5 Mon Sep 17 00:00:00 2001 From: yifen9 Date: Sat, 16 May 2026 00:50:11 +0200 Subject: [PATCH 3/3] startup migrations Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- internal/app/app.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/app/app.go b/internal/app/app.go index 38b5d30..cc27926 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -11,6 +11,7 @@ import ( "github.com/gamidoc/backend/internal/auth" "github.com/gamidoc/backend/internal/bootstrap" apphttp "github.com/gamidoc/backend/internal/http" + "github.com/gamidoc/backend/internal/migrate" "github.com/gamidoc/backend/internal/pdf" "github.com/gamidoc/backend/internal/project" "github.com/gamidoc/backend/internal/recommendation" @@ -51,6 +52,13 @@ func New(cfg config.Config) (*App, error) { return nil, fmt.Errorf("postgres startup check failed: %w", err) } + migrator := migrate.NewMigrator(pg, cfg.MigrationsDir) + if _, err := migrator.Up(startupCtx); err != nil { + _ = pg.Close() + _ = redisClient.Close() + return nil, fmt.Errorf("postgres migration failed: %w", err) + } + if err := redisClient.Ready(startupCtx); err != nil { _ = pg.Close() _ = redisClient.Close()