diff --git a/internal/app/game_reviews.go b/internal/app/game_reviews.go index e77c034..859edfd 100644 --- a/internal/app/game_reviews.go +++ b/internal/app/game_reviews.go @@ -36,7 +36,7 @@ func mapGameReviews(reviews []*game_review_service.Review) []*proto.GameReview { func (s *server) CreateGameReview(ctx context.Context, req *proto.CreateGameReviewReq) (*proto.CreateGameReviewRsp, error) { review, err := s.reviewService.Create(ctx, int(req.GameID), int(req.TeamID), req.Password, req.Rating, req.Text) if err != nil { - return &proto.CreateGameReviewRsp{Error: err.Error()}, nil + return nil, reviewErrorStatus(err, "не удалось создать отзыв") } return &proto.CreateGameReviewRsp{Review: mapGameReview(review)}, nil } @@ -44,7 +44,7 @@ func (s *server) CreateGameReview(ctx context.Context, req *proto.CreateGameRevi func (s *server) GetGameReviews(ctx context.Context, req *proto.GetGameReviewsReq) (*proto.GetGameReviewsRsp, error) { reviews, average, count, err := s.reviewService.List(ctx, int(req.GameID), false) if err != nil { - return &proto.GetGameReviewsRsp{Error: err.Error()}, nil + return nil, reviewErrorStatus(err, "не удалось получить отзывы игры") } return &proto.GetGameReviewsRsp{AverageRating: average, ReviewsCount: count, Reviews: mapGameReviews(reviews)}, nil } @@ -52,7 +52,7 @@ func (s *server) GetGameReviews(ctx context.Context, req *proto.GetGameReviewsRe func (s *server) GetScenarioReviews(ctx context.Context, req *proto.GetScenarioReviewsReq) (*proto.GetGameReviewsRsp, error) { reviews, average, count, err := s.reviewService.ListByScenario(ctx, int(req.ScenarioID)) if err != nil { - return &proto.GetGameReviewsRsp{Error: err.Error()}, nil + return nil, reviewErrorStatus(err, "не удалось получить отзывы сценария") } return &proto.GetGameReviewsRsp{AverageRating: average, ReviewsCount: count, Reviews: mapGameReviews(reviews)}, nil } @@ -67,7 +67,7 @@ func (s *server) GetGameReviewsForModeration(ctx context.Context, req *proto.Get } reviews, average, count, err := s.reviewService.List(ctx, int(req.GameID), true) if err != nil { - return &proto.GetGameReviewsRsp{Error: err.Error()}, nil + return nil, reviewErrorStatus(err, "не удалось получить отзывы для модерации") } return &proto.GetGameReviewsRsp{AverageRating: average, ReviewsCount: count, Reviews: mapGameReviews(reviews)}, nil } @@ -82,7 +82,7 @@ func (s *server) GetReviewsForModeration(ctx context.Context, req *proto.GetRevi } reviews, err := s.reviewService.ListForModeration(ctx) if err != nil { - return &proto.GetGameReviewsRsp{Error: err.Error()}, nil + return nil, reviewErrorStatus(err, "не удалось получить отзывы для модерации") } return &proto.GetGameReviewsRsp{Reviews: mapGameReviews(reviews)}, nil } @@ -97,7 +97,7 @@ func (s *server) ModerateGameReview(ctx context.Context, req *proto.ModerateGame } review, err := s.reviewService.Moderate(ctx, int(req.ID), req.Status) if err != nil { - return &proto.ModerateGameReviewRsp{Error: err.Error()}, nil + return nil, reviewErrorStatus(err, "не удалось изменить статус отзыва") } return &proto.ModerateGameReviewRsp{Review: mapGameReview(review)}, nil } diff --git a/internal/app/review_errors.go b/internal/app/review_errors.go new file mode 100644 index 0000000..1f34733 --- /dev/null +++ b/internal/app/review_errors.go @@ -0,0 +1,23 @@ +package app + +import ( + "errors" + + "evening_detective_server/internal/services/game_review_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func reviewErrorStatus(err error, internalMessage string) error { + switch { + case errors.Is(err, game_review_service.ErrTeamNotFound), errors.Is(err, game_review_service.ErrGameNotFound), errors.Is(err, game_review_service.ErrReviewNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, game_review_service.ErrInvalidReview), errors.Is(err, game_review_service.ErrInvalidStatus): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, game_review_service.ErrGameNotFinished), errors.Is(err, game_review_service.ErrReviewExists): + return status.Error(codes.FailedPrecondition, err.Error()) + default: + return status.Error(codes.Internal, internalMessage) + } +} diff --git a/internal/app/review_ticket_user_errors_test.go b/internal/app/review_ticket_user_errors_test.go new file mode 100644 index 0000000..941bd25 --- /dev/null +++ b/internal/app/review_ticket_user_errors_test.go @@ -0,0 +1,48 @@ +package app + +import ( + "errors" + "strings" + "testing" + + "evening_detective_server/internal/services/game_review_service" + "evening_detective_server/internal/services/ticket_service" + "evening_detective_server/internal/services/users_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestReviewErrorStatus(t *testing.T) { + err := reviewErrorStatus(game_review_service.ErrInvalidReview, "review failed") + if status.Code(err) != codes.InvalidArgument || status.Convert(err).Message() != game_review_service.ErrInvalidReview.Error() { + t.Fatalf("status=%v", err) + } + err = reviewErrorStatus(errors.New("private database detail"), "review failed") + if status.Code(err) != codes.Internal || status.Convert(err).Message() != "review failed" { + t.Fatalf("internal status=%v", err) + } +} + +func TestTicketErrorStatus(t *testing.T) { + err := ticketErrorStatus(ticket_service.ErrTicketUsed, "ticket failed") + if status.Code(err) != codes.FailedPrecondition || status.Convert(err).Message() != ticket_service.ErrTicketUsed.Error() { + t.Fatalf("status=%v", err) + } + err = ticketErrorStatus(errors.New("private database detail"), "ticket failed") + if status.Code(err) != codes.Internal || status.Convert(err).Message() != "ticket failed" { + t.Fatalf("internal status=%v", err) + } +} + +func TestUserErrorStatusDoesNotExposeCredentials(t *testing.T) { + err := userErrorStatus(users_service.ErrInvalidCredentials, "login failed") + if status.Code(err) != codes.Unauthenticated || status.Convert(err).Message() != users_service.ErrInvalidCredentials.Error() { + t.Fatalf("status=%v", err) + } + for _, secret := range []string{"password", "token", "private database detail"} { + if strings.Contains(status.Convert(err).Message(), secret) { + t.Fatalf("secret %q exposed", secret) + } + } +} diff --git a/internal/app/ticket_errors.go b/internal/app/ticket_errors.go new file mode 100644 index 0000000..316aceb --- /dev/null +++ b/internal/app/ticket_errors.go @@ -0,0 +1,25 @@ +package app + +import ( + "errors" + + "evening_detective_server/internal/services/ticket_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func ticketErrorStatus(err error, internalMessage string) error { + switch { + case errors.Is(err, ticket_service.ErrTicketNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, ticket_service.ErrInvalidTicketCount): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, ticket_service.ErrTicketUsed), errors.Is(err, ticket_service.ErrGameFinished): + return status.Error(codes.FailedPrecondition, err.Error()) + case errors.Is(err, ticket_service.ErrInvalidBaseURL): + return status.Error(codes.FailedPrecondition, err.Error()) + default: + return status.Error(codes.Internal, internalMessage) + } +} diff --git a/internal/app/tickets.go b/internal/app/tickets.go index 7fc13d6..5536182 100644 --- a/internal/app/tickets.go +++ b/internal/app/tickets.go @@ -53,7 +53,7 @@ func (s *server) AcceptTicket(ctx context.Context, req *proto.AcceptTicketReq) ( } page, err := s.ticketService.Accept(ctx, req.Token, claims.UserID) if err != nil { - return &proto.AcceptTicketRsp{Error: err.Error()}, nil + return nil, ticketErrorStatus(err, "не удалось принять билет") } result := &proto.TicketPage{ID: int32(page.ID), GameName: page.GameName, StartAt: timestamppb.New(page.StartAt), Status: page.Status, GameStatus: page.GameStatus} if page.Location != nil { @@ -82,7 +82,7 @@ func (s *server) IssueGameTickets(ctx context.Context, req *proto.IssueGameTicke tickets, err := s.ticketService.Issue(ctx, int(req.GameID), int(req.Count)) if err != nil { - return &proto.IssueGameTicketsRsp{Error: err.Error()}, nil + return nil, ticketErrorStatus(err, "не удалось выпустить билеты") } result := make([]*proto.Ticket, 0, len(tickets)) diff --git a/internal/app/user_errors.go b/internal/app/user_errors.go new file mode 100644 index 0000000..d23c07d --- /dev/null +++ b/internal/app/user_errors.go @@ -0,0 +1,26 @@ +package app + +import ( + "errors" + + "evening_detective_server/internal/repos/users_repo" + "evening_detective_server/internal/services/users_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func userErrorStatus(err error, internalMessage string) error { + switch { + case errors.Is(err, users_service.ErrUserNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, users_service.ErrTermsNotAccepted), errors.Is(err, users_service.ErrInvalidEmail), errors.Is(err, users_service.ErrInvalidRole), errors.Is(err, users_service.ErrInvalidPasswordChange): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, users_service.ErrInvalidCredentials), errors.Is(err, users_repo.ErrUserNotFound): + return status.Error(codes.Unauthenticated, users_service.ErrInvalidCredentials.Error()) + case errors.Is(err, users_service.ErrInvalidAccountPassword): + return status.Error(codes.PermissionDenied, err.Error()) + default: + return status.Error(codes.Internal, internalMessage) + } +} diff --git a/internal/app/users.go b/internal/app/users.go index b51781d..c94a578 100644 --- a/internal/app/users.go +++ b/internal/app/users.go @@ -28,9 +28,7 @@ func (s *server) Signup(ctx context.Context, req *proto.SignupReq) (*proto.Signu userAgent, ) if err != nil { - return &proto.SignupRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось зарегистрировать пользователя") } return &proto.SignupRsp{}, nil } @@ -51,9 +49,7 @@ func (s *server) DeleteAccount(ctx context.Context, req *proto.DeleteAccountReq) } err := s.usersService.DeleteAccount(ctx, claims.UserID, password) if err != nil { - return &proto.DeleteAccountRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось удалить аккаунт") } return &proto.DeleteAccountRsp{}, nil } @@ -117,9 +113,7 @@ func clientInfoFromContext(ctx context.Context) (ip string, userAgent string) { func (s *server) RefreshPassword(ctx context.Context, req *proto.RefreshPasswordReq) (*proto.RefreshPasswordRsp, error) { err := s.usersService.RefreshPassword(ctx, req.Email) if err != nil { - return &proto.RefreshPasswordRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось обновить пароль") } return &proto.RefreshPasswordRsp{}, nil } @@ -127,9 +121,7 @@ func (s *server) RefreshPassword(ctx context.Context, req *proto.RefreshPassword func (s *server) Login(ctx context.Context, req *proto.LoginReq) (*proto.LoginRsp, error) { accessToken, refreshToken, err := s.usersService.Login(ctx, req.Email, req.Password) if err != nil { - return &proto.LoginRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось выполнить вход") } return &proto.LoginRsp{ AccessToken: accessToken, @@ -140,9 +132,10 @@ func (s *server) Login(ctx context.Context, req *proto.LoginReq) (*proto.LoginRs func (s *server) Refresh(ctx context.Context, req *proto.RefreshReq) (*proto.RefreshRsp, error) { accessToken, refreshToken, err := s.usersService.Refresh(ctx, req.RefreshToken) if err != nil { - return &proto.RefreshRsp{ - Error: err.Error(), - }, nil + if errors.Is(err, users_service.ErrInvalidRefreshToken) { + return nil, status.Error(codes.Unauthenticated, err.Error()) + } + return nil, userErrorStatus(err, "не удалось обновить сессию") } return &proto.RefreshRsp{ AccessToken: accessToken, @@ -160,9 +153,7 @@ func (s *server) GetUsers(ctx context.Context, req *proto.GetUsersReq) (*proto.G } users, err := s.usersService.GetUsers(ctx) if err != nil { - return &proto.GetUsersRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось получить пользователей") } resUsers := make([]*proto.User, 0, len(users)) @@ -177,9 +168,7 @@ func (s *server) GetUsers(ctx context.Context, req *proto.GetUsersReq) (*proto.G func (s *server) GetUserByID(ctx context.Context, req *proto.GetUserByIDReq) (*proto.GetUserByIDRsp, error) { user, err := s.usersService.GetUserByID(ctx, int(req.ID)) if err != nil { - return &proto.GetUserByIDRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось получить пользователя") } return &proto.GetUserByIDRsp{ @@ -194,9 +183,7 @@ func (s *server) GetMe(ctx context.Context, req *proto.GetMeReq) (*proto.GetMeRs } user, err := s.usersService.GetUserByID(ctx, claims.UserID) if err != nil { - return &proto.GetMeRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось получить профиль") } return &proto.GetMeRsp{ @@ -213,9 +200,7 @@ func (s *server) AddUserRole(ctx context.Context, req *proto.AddUserRoleReq) (*p return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if err := s.usersService.AddUserRole(ctx, int(req.ID), req.Role); err != nil { - return &proto.AddUserRoleRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось добавить роль") } return &proto.AddUserRoleRsp{}, nil } @@ -229,9 +214,7 @@ func (s *server) DeleteUserRole(ctx context.Context, req *proto.DeleteUserRoleRe return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if err := s.usersService.DeleteUserRole(ctx, int(req.ID), req.Role); err != nil { - return &proto.DeleteUserRoleRsp{ - Error: err.Error(), - }, nil + return nil, userErrorStatus(err, "не удалось удалить роль") } return &proto.DeleteUserRoleRsp{}, nil } diff --git a/internal/services/game_review_service/service.go b/internal/services/game_review_service/service.go index f500cfd..1e59cf9 100644 --- a/internal/services/game_review_service/service.go +++ b/internal/services/game_review_service/service.go @@ -18,6 +18,11 @@ import ( ) var ( + ErrTeamNotFound = errors.New("Команда не найдена") + ErrGameNotFound = errors.New("Игра не найдена") + ErrReviewNotFound = errors.New("Отзыв не найден") + ErrReviewExists = errors.New("Отзыв от команды уже оставлен") + ErrInvalidStatus = errors.New("Недопустимый статус модерации") ErrGameNotFinished = errors.New("Оценить можно только завершённую игру") ErrInvalidReview = errors.New("Оценка должна быть от 1 до 5, текст обязателен") ) @@ -39,11 +44,20 @@ func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, pass return nil, ErrInvalidReview } team, err := s.teams.GetTeamByID(ctx, teamID) - if err != nil || team.GameID != gameID || team.Password != password || team.ParticipationStatus == "cancelled" { - return nil, teams_repo.ErrTeamNotFound + if err != nil { + if errors.Is(err, teams_repo.ErrTeamNotFound) { + return nil, ErrTeamNotFound + } + return nil, err + } + if team.GameID != gameID || team.Password != password || team.ParticipationStatus == "cancelled" { + return nil, ErrTeamNotFound } game, err := s.games.GetGameByID(ctx, gameID) if err != nil { + if errors.Is(err, games_repo.ErrGameNotFound) { + return nil, ErrGameNotFound + } return nil, err } if game.Status != "finish" { @@ -51,6 +65,9 @@ func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, pass } review, err := s.reviews.Create(ctx, gameID, teamID, rating, strings.TrimSpace(text)) if err != nil { + if errors.Is(err, game_reviews_repo.ErrReviewExists) { + return nil, ErrReviewExists + } return nil, err } return mapReview(review), nil @@ -58,6 +75,9 @@ func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, pass func (s *GameReviewService) List(ctx context.Context, gameID int, all bool) ([]*Review, float64, int32, error) { if _, err := s.games.GetGameByID(ctx, gameID); err != nil { + if errors.Is(err, games_repo.ErrGameNotFound) { + return nil, 0, 0, ErrGameNotFound + } return nil, 0, 0, err } reviews, average, count, err := s.reviews.List(ctx, gameID, all) @@ -72,6 +92,12 @@ func (s *GameReviewService) ListForModeration(ctx context.Context) ([]*Review, e func (s *GameReviewService) Moderate(ctx context.Context, id int, status string) (*Review, error) { review, err := s.reviews.Moderate(ctx, id, status) if err != nil { + if errors.Is(err, game_reviews_repo.ErrReviewNotFound) { + return nil, ErrReviewNotFound + } + if errors.Is(err, game_reviews_repo.ErrInvalidStatus) { + return nil, ErrInvalidStatus + } return nil, err } return mapReview(review), nil diff --git a/internal/services/ticket_service/service.go b/internal/services/ticket_service/service.go index 6b204fb..c867691 100644 --- a/internal/services/ticket_service/service.go +++ b/internal/services/ticket_service/service.go @@ -19,9 +19,9 @@ const maxTicketsPerRequest = 100 var ( ErrInvalidTicketCount = errors.New("количество билетов должно быть от 1 до 100") ErrInvalidBaseURL = errors.New("необходимо настроить APP_BASE_URL — публичный адрес приложения") - ErrTicketNotFound = tickets_repo.ErrTicketNotFound - ErrTicketUsed = tickets_repo.ErrTicketUsed - ErrGameFinished = tickets_repo.ErrGameFinished + ErrTicketNotFound = errors.New("Билет не найден") + ErrTicketUsed = errors.New("Билет уже использован") + ErrGameFinished = errors.New("Игра уже завершена") ) type Ticket struct { @@ -93,7 +93,7 @@ func (s *TicketService) Get(ctx context.Context, token string) (*Page, error) { } info, err := s.repo.GetByHash(ctx, hash) if err != nil { - return nil, err + return nil, mapTicketError(err) } return &Page{ ID: info.ID, @@ -114,11 +114,24 @@ func (s *TicketService) Accept(ctx context.Context, token string, userID int) (* } info, err := s.repo.Accept(ctx, hash, userID) if err != nil { - return nil, err + return nil, mapTicketError(err) } return &Page{ID: info.ID, Status: info.Status, GameName: info.GameName, StartAt: info.StartAt, Location: info.Location, GameStatus: info.GameStatus, UsedAt: info.UsedAt, UsedBy: info.UsedBy}, nil } +func mapTicketError(err error) error { + switch { + case errors.Is(err, tickets_repo.ErrTicketNotFound): + return ErrTicketNotFound + case errors.Is(err, tickets_repo.ErrTicketUsed): + return ErrTicketUsed + case errors.Is(err, tickets_repo.ErrGameFinished): + return ErrGameFinished + default: + return err + } +} + func newToken() (string, error) { data := make([]byte, 32) if _, err := rand.Read(data); err != nil { diff --git a/internal/services/users_service/service.go b/internal/services/users_service/service.go index bf61da3..28abaad 100644 --- a/internal/services/users_service/service.go +++ b/internal/services/users_service/service.go @@ -82,8 +82,15 @@ func NewUsersService( } } -// ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений. -var ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности") +var ( + // ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений. + ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности") + ErrInvalidEmail = errors.New("Некорректный email") + ErrInvalidCredentials = errors.New("Не верен email или пароль") + ErrInvalidAccountPassword = errors.New("Неверный пароль") + ErrInvalidRefreshToken = errors.New("Пользователь не найден") + ErrInvalidRole = errors.New("Недопустимая роль") +) func (s *UsersService) AddUser( ctx context.Context, @@ -170,12 +177,19 @@ func (s *UsersService) DeleteAccount( ) error { user, err := s.usersRepo.GetUserByID(ctx, userID) if err != nil { + if errors.Is(err, users_repo.ErrUserNotFound) { + return ErrUserNotFound + } return err } if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { - return errors.New("Неверный пароль") + return ErrInvalidAccountPassword } - return s.usersRepo.DeleteUser(ctx, userID) + err = s.usersRepo.DeleteUser(ctx, userID) + if errors.Is(err, users_repo.ErrUserNotFound) { + return ErrUserNotFound + } + return err } func (s *UsersService) RefreshPassword( @@ -225,15 +239,15 @@ func (s *UsersService) RefreshPassword( // validateEmail проверяет, что строка является одиночным email-адресом. func validateEmail(email string) error { if email == "" { - return errors.New("Email не указан") + return ErrInvalidEmail } addr, err := mail.ParseAddress(email) if err != nil { - return errors.New("Некорректный email") + return ErrInvalidEmail } // Отклоняем форму "Имя " и всё, что не является чистым адресом. if !strings.EqualFold(addr.Address, email) { - return errors.New("Некорректный email") + return ErrInvalidEmail } return nil } @@ -249,11 +263,14 @@ func (s *UsersService) Login( strings.TrimSpace(email), ) if err != nil { + if errors.Is(err, users_repo.ErrUserNotFound) { + return "", "", err + } return "", "", err } if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { - return "", "", errors.New("Не верен email или пароль") + return "", "", ErrInvalidCredentials } accessToken, err := s.processorJWT.GenerateAccessToken(user.ID, user.Email, user.Roles) @@ -283,7 +300,7 @@ func (s *UsersService) Refresh( ) (string, string, error) { claims, err := s.processorJWT.GetClaims(token) if err != nil { - return "", "", errors.New("Пользователь не найден") + return "", "", ErrInvalidRefreshToken } now := time.Now() @@ -292,7 +309,7 @@ func (s *UsersService) Refresh( return "", "", err } if !ok { - return "", "", errors.New("Пользователь не найден") + return "", "", ErrInvalidRefreshToken } if err := s.refreshTokensRepo.DeleteRefreshTokenByJti(ctx, claims.ID); err != nil { return "", "", err @@ -300,6 +317,9 @@ func (s *UsersService) Refresh( user, err := s.usersRepo.GetUserByID(ctx, claims.UserID) if err != nil { + if errors.Is(err, users_repo.ErrUserNotFound) { + return "", "", ErrInvalidRefreshToken + } return "", "", err } @@ -340,6 +360,9 @@ func (s *UsersService) GetUserByID( ) (*User, error) { user, err := s.usersRepo.GetUserByID(ctx, userID) if err != nil { + if errors.Is(err, users_repo.ErrUserNotFound) { + return nil, ErrUserNotFound + } return nil, err } return mapUser(user), nil @@ -351,9 +374,13 @@ func (s *UsersService) AddUserRole( role string, ) error { if !slices.Contains(roles.AllRoles, role) { - return fmt.Errorf("role %s not support", role) + return ErrInvalidRole } - return s.usersRepo.AddUserRole(ctx, userID, role) + err := s.usersRepo.AddUserRole(ctx, userID, role) + if errors.Is(err, users_repo.ErrUserNotFound) { + return ErrUserNotFound + } + return err } func (s *UsersService) DeleteUserRole( @@ -362,9 +389,13 @@ func (s *UsersService) DeleteUserRole( role string, ) error { if !slices.Contains(roles.AllRoles, role) { - return fmt.Errorf("role %s not support", role) + return ErrInvalidRole } - return s.usersRepo.DeleteUserRole(ctx, userID, role) + err := s.usersRepo.DeleteUserRole(ctx, userID, role) + if errors.Is(err, users_repo.ErrUserNotFound) { + return ErrUserNotFound + } + return err } var (