From 3f7731411b8674651f7d28c8a43a3e07f2c06089 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sat, 3 Oct 2026 01:09:06 +0700 Subject: [PATCH] fix --- internal/app/answer_errors.go | 23 ++++++++++ internal/app/answers.go | 12 ++--- internal/app/game_answer_errors_test.go | 51 +++++++++++++++++++++ internal/app/game_errors.go | 22 +++++++++ internal/app/games.go | 48 +++++-------------- internal/services/answer_service/service.go | 34 +++++++++++--- internal/services/game_service/service.go | 43 +++++++++++------ 7 files changed, 169 insertions(+), 64 deletions(-) create mode 100644 internal/app/answer_errors.go create mode 100644 internal/app/game_answer_errors_test.go create mode 100644 internal/app/game_errors.go diff --git a/internal/app/answer_errors.go b/internal/app/answer_errors.go new file mode 100644 index 0000000..ede6e51 --- /dev/null +++ b/internal/app/answer_errors.go @@ -0,0 +1,23 @@ +package app + +import ( + "errors" + + "evening_detective_server/internal/services/answer_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func answerErrorStatus(err error, internalMessage string) error { + switch { + case errors.Is(err, answer_service.ErrTeamNotFound), errors.Is(err, answer_service.ErrGameNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, answer_service.ErrQuestionNotFound), errors.Is(err, answer_service.ErrOfflineAnswers): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, answer_service.ErrGameFinished), errors.Is(err, answer_service.ErrGameNotStarted), errors.Is(err, answer_service.ErrGameNotFinished): + return status.Error(codes.FailedPrecondition, err.Error()) + default: + return status.Error(codes.Internal, internalMessage) + } +} diff --git a/internal/app/answers.go b/internal/app/answers.go index 6a011fc..1f9d591 100644 --- a/internal/app/answers.go +++ b/internal/app/answers.go @@ -17,9 +17,7 @@ func (s *server) GetTeamAnswers(ctx context.Context, req *proto.GetTeamAnswersRe req.Password, ) if err != nil { - return &proto.GetTeamAnswersRsp{ - Error: err.Error(), - }, nil + return nil, answerErrorStatus(err, "не удалось получить ответы команды") } return &proto.GetTeamAnswersRsp{ @@ -35,9 +33,7 @@ func (s *server) SubmitTeamAnswers(ctx context.Context, req *proto.SubmitTeamAns convertAnswers(req.Answers), ) if err != nil { - return &proto.SubmitTeamAnswersRsp{ - Error: err.Error(), - }, nil + return nil, answerErrorStatus(err, "не удалось сохранить ответы команды") } return &proto.SubmitTeamAnswersRsp{}, nil @@ -65,9 +61,7 @@ func (s *server) SetTeamAnswerScore(ctx context.Context, req *proto.SetTeamAnswe score, ) if err != nil { - return &proto.SetTeamAnswerScoreRsp{ - Error: err.Error(), - }, nil + return nil, answerErrorStatus(err, "не удалось сохранить оценку ответа") } return &proto.SetTeamAnswerScoreRsp{}, nil diff --git a/internal/app/game_answer_errors_test.go b/internal/app/game_answer_errors_test.go new file mode 100644 index 0000000..bd84fa1 --- /dev/null +++ b/internal/app/game_answer_errors_test.go @@ -0,0 +1,51 @@ +package app + +import ( + "errors" + "testing" + + "evening_detective_server/internal/services/answer_service" + "evening_detective_server/internal/services/game_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestGameErrorStatus(t *testing.T) { + for _, tc := range []struct { + name, message string + err error + code codes.Code + }{ + {"not found", "Игра не найдена", game_service.ErrGameNotFound, codes.NotFound}, + {"invalid format", "Формат игры должен быть online или offline", game_service.ErrInvalidGameFormat, codes.InvalidArgument}, + {"internal", "не удалось получить игру", errors.New("private database detail"), codes.Internal}, + } { + t.Run(tc.name, func(t *testing.T) { + err := gameErrorStatus(tc.err, "не удалось получить игру") + if status.Code(err) != tc.code || status.Convert(err).Message() != tc.message { + t.Fatalf("status=%v, want %v %q", err, tc.code, tc.message) + } + }) + } +} + +func TestAnswerErrorStatus(t *testing.T) { + for _, tc := range []struct { + name, message string + err error + code codes.Code + }{ + {"team not found", "Команда не найдена", answer_service.ErrTeamNotFound, codes.NotFound}, + {"question not found", "Вопрос не найден", answer_service.ErrQuestionNotFound, codes.InvalidArgument}, + {"game not started", "Игра не начата, отвечать на вопросы нельзя", answer_service.ErrGameNotStarted, codes.FailedPrecondition}, + {"internal", "не удалось получить ответы игры", errors.New("private database detail"), codes.Internal}, + } { + t.Run(tc.name, func(t *testing.T) { + err := answerErrorStatus(tc.err, "не удалось получить ответы игры") + if status.Code(err) != tc.code || status.Convert(err).Message() != tc.message { + t.Fatalf("status=%v, want %v %q", err, tc.code, tc.message) + } + }) + } +} diff --git a/internal/app/game_errors.go b/internal/app/game_errors.go new file mode 100644 index 0000000..52edb72 --- /dev/null +++ b/internal/app/game_errors.go @@ -0,0 +1,22 @@ +package app + +import ( + "errors" + + "evening_detective_server/internal/services/game_service" + "evening_detective_server/internal/services/scenarios_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func gameErrorStatus(err error, internalMessage string) error { + switch { + case errors.Is(err, game_service.ErrGameNotFound), errors.Is(err, scenarios_service.ErrScenarioNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, game_service.ErrInvalidGameFormat): + return status.Error(codes.InvalidArgument, err.Error()) + default: + return status.Error(codes.Internal, internalMessage) + } +} diff --git a/internal/app/games.go b/internal/app/games.go index 59a87f6..aa4913c 100644 --- a/internal/app/games.go +++ b/internal/app/games.go @@ -40,9 +40,7 @@ func (s *server) AddGame(ctx context.Context, req *proto.AddGameReq) (*proto.Add req.Format, ) if err != nil { - return &proto.AddGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось создать игру") } return &proto.AddGameRsp{ @@ -64,9 +62,7 @@ func (s *server) DeleteGame(ctx context.Context, req *proto.DeleteGameReq) (*pro int(req.ID), ) if err != nil { - return &proto.DeleteGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось удалить игру") } return &proto.DeleteGameRsp{}, nil @@ -83,9 +79,7 @@ func (s *server) GetGame(ctx context.Context, req *proto.GetGameReq) (*proto.Get game, err := s.gameService.GetFullGame(ctx, int(req.ID)) if err != nil { - return &proto.GetGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось получить игру") } return &proto.GetGameRsp{ @@ -104,9 +98,7 @@ func (s *server) GetGameAnswers(ctx context.Context, req *proto.GetGameAnswersRe gameAnswers, err := s.answerService.GetGameAnswers(ctx, int(req.GameID)) if err != nil { - return &proto.GetGameAnswersRsp{ - Error: err.Error(), - }, nil + return nil, answerErrorStatus(err, "не удалось получить ответы игры") } return &proto.GetGameAnswersRsp{ @@ -126,9 +118,7 @@ func (s *server) GetGameResults(ctx context.Context, req *proto.GetGameResultsRe results, err := s.answerService.GetGameResults(ctx, int(req.GameID)) if err != nil { - return &proto.GetGameResultsRsp{ - Error: err.Error(), - }, nil + return nil, answerErrorStatus(err, "не удалось получить результаты игры") } return &proto.GetGameResultsRsp{ @@ -148,9 +138,7 @@ func (s *server) GetGames(ctx context.Context, req *proto.GetGamesReq) (*proto.G games, err := s.gameService.GetGames(ctx) if err != nil { - return &proto.GetGamesRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось получить игры") } return &proto.GetGamesRsp{ @@ -182,9 +170,7 @@ func (s *server) UpdateGame(ctx context.Context, req *proto.UpdateGameReq) (*pro req.Format, ) if err != nil { - return &proto.UpdateGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось изменить игру") } return &proto.UpdateGameRsp{}, nil @@ -201,9 +187,7 @@ func (s *server) StartGame(ctx context.Context, req *proto.StartGameReq) (*proto err := s.gameService.StartGame(ctx, int(req.ID)) if err != nil { - return &proto.StartGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось запустить игру") } return &proto.StartGameRsp{}, nil @@ -220,9 +204,7 @@ func (s *server) PauseGame(ctx context.Context, req *proto.PauseGameReq) (*proto err := s.gameService.PauseGame(ctx, int(req.ID)) if err != nil { - return &proto.PauseGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось приостановить игру") } return &proto.PauseGameRsp{}, nil @@ -239,9 +221,7 @@ func (s *server) PlayGame(ctx context.Context, req *proto.PlayGameReq) (*proto.P err := s.gameService.PlayGame(ctx, int(req.ID)) if err != nil { - return &proto.PlayGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось продолжить игру") } return &proto.PlayGameRsp{}, nil @@ -258,9 +238,7 @@ func (s *server) FinishGame(ctx context.Context, req *proto.FinishGameReq) (*pro err := s.gameService.FinishGame(ctx, int(req.ID)) if err != nil { - return &proto.FinishGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось завершить игру") } return &proto.FinishGameRsp{}, nil @@ -277,9 +255,7 @@ func (s *server) ResetGame(ctx context.Context, req *proto.ResetGameReq) (*proto err := s.gameService.ResetGame(ctx, int(req.ID)) if err != nil { - return &proto.ResetGameRsp{ - Error: err.Error(), - }, nil + return nil, gameErrorStatus(err, "не удалось сбросить игру") } return &proto.ResetGameRsp{}, nil diff --git a/internal/services/answer_service/service.go b/internal/services/answer_service/service.go index 5c95abf..838416d 100644 --- a/internal/services/answer_service/service.go +++ b/internal/services/answer_service/service.go @@ -38,6 +38,10 @@ func NewAnswerService( } var ( + // ErrTeamNotFound — команда не найдена или пароль неверен. + ErrTeamNotFound = errors.New("Команда не найдена") + // ErrGameNotFound — игра не найдена. + ErrGameNotFound = errors.New("Игра не найдена") // ErrGameFinished — после завершения игры ответы команд больше нельзя // изменять: организатор оценивает их в панели игры. ErrGameFinished = errors.New("Игра завершена, ответы больше нельзя изменять") @@ -47,19 +51,27 @@ var ( ErrQuestionNotFound = errors.New("Вопрос не найден") // ErrGameNotFinished — итоги игры доступны только после её завершения. ErrGameNotFinished = errors.New("Итоги доступны после завершения игры") + // ErrOfflineAnswers — ответы для offline-игры недоступны через сайт. + ErrOfflineAnswers = errors.New("В offline-игре ответы через сайт недоступны") ) // getTeamAndGame проверяет команду по паролю и возвращает её вместе с игрой. func (s *AnswerService) getTeamAndGame(ctx context.Context, teamID int, password string) (*repos.Team, *repos.Game, error) { team, err := s.teamsRepo.GetTeamByID(ctx, teamID) if err != nil { + if errors.Is(err, teams_repo.ErrTeamNotFound) { + return nil, nil, ErrTeamNotFound + } return nil, nil, err } if team.Password != password || team.ParticipationStatus == "cancelled" { - return nil, nil, teams_repo.ErrTeamNotFound + return nil, nil, ErrTeamNotFound } game, err := s.gamesRepo.GetGameByID(ctx, team.GameID) if err != nil { + if errors.Is(err, games_repo.ErrGameNotFound) { + return nil, nil, ErrGameNotFound + } return nil, nil, err } return team, game, nil @@ -120,7 +132,7 @@ func (s *AnswerService) SubmitTeamAnswers(ctx context.Context, teamID int, passw return err } if game.Format == "offline" { - return errors.New("В offline-игре ответы через сайт недоступны") + return ErrOfflineAnswers } switch game.Status { case "run", "pause": @@ -157,7 +169,7 @@ func (s *AnswerService) SubmitTeamAnswers(ctx context.Context, teamID int, passw func (s *AnswerService) GetGameAnswers(ctx context.Context, gameID int) (*GameAnswers, error) { game, err := s.gamesRepo.GetGameByID(ctx, gameID) if err != nil { - return nil, err + return nil, mapAnswerError(err) } questions, byCode, err := s.getScenarioQuestionsByCode(ctx, game.ScenarioID) if err != nil { @@ -227,11 +239,11 @@ func (s *AnswerService) GetGameAnswers(ctx context.Context, gameID int) (*GameAn func (s *AnswerService) SetTeamAnswerScore(ctx context.Context, teamID int, questionCode string, score *int) error { team, err := s.teamsRepo.GetTeamByID(ctx, teamID) if err != nil { - return err + return mapAnswerError(err) } game, err := s.gamesRepo.GetGameByID(ctx, team.GameID) if err != nil { - return err + return mapAnswerError(err) } _, byCode, err := s.getScenarioQuestionsByCode(ctx, game.ScenarioID) if err != nil { @@ -251,7 +263,7 @@ func (s *AnswerService) SetTeamAnswerScore(ctx context.Context, teamID int, ques func (s *AnswerService) GetGameResults(ctx context.Context, gameID int) (*GameResults, error) { game, err := s.gamesRepo.GetGameByID(ctx, gameID) if err != nil { - return nil, err + return nil, mapAnswerError(err) } if game.Status != "finish" { return nil, ErrGameNotFinished @@ -301,3 +313,13 @@ func (s *AnswerService) GetGameResults(ctx context.Context, gameID int) (*GameRe Teams: rankTeams(buildTeamResults(questions, mappedTeams, answerByTeamAndCode)), }, nil } + +func mapAnswerError(err error) error { + if errors.Is(err, teams_repo.ErrTeamNotFound) { + return ErrTeamNotFound + } + if errors.Is(err, games_repo.ErrGameNotFound) { + return ErrGameNotFound + } + return err +} diff --git a/internal/services/game_service/service.go b/internal/services/game_service/service.go index bd20791..c98fef6 100644 --- a/internal/services/game_service/service.go +++ b/internal/services/game_service/service.go @@ -23,6 +23,13 @@ type GameService struct { applicationsRepo *applications_repo.ApplicationsRepo } +var ( + // ErrGameNotFound — игра не найдена. + ErrGameNotFound = errors.New("Игра не найдена") + // ErrInvalidGameFormat — формат игры не поддерживается. + ErrInvalidGameFormat = errors.New("Формат игры должен быть online или offline") +) + func NewGameService( gamesRepo *games_repo.GamesRepo, teamsRepo *teams_repo.TeamsRepo, @@ -56,7 +63,7 @@ func (s *GameService) AddGame( format = "online" } if format != "online" && format != "offline" { - return 0, errors.New("Формат игры должен быть online или offline") + return 0, ErrInvalidGameFormat } id, err := s.gamesRepo.AddGame(ctx, name, description, location, startAt, scenarioID, format) if err != nil { @@ -68,7 +75,7 @@ func (s *GameService) AddGame( func (s *GameService) GetGame(ctx context.Context, id int) (*game_model.Game, error) { game, err := s.gamesRepo.GetGameByID(ctx, id) if err != nil { - return nil, err + return nil, mapGameError(err) } res := game_model.MapGame(game, s.domain) return res, nil @@ -77,7 +84,7 @@ func (s *GameService) GetGame(ctx context.Context, id int) (*game_model.Game, er func (s *GameService) GetGameMedia(ctx context.Context, id int) (*game_model.Game, error) { game, err := s.gamesRepo.GetGameByID(ctx, id) if err != nil { - return nil, err + return nil, mapGameError(err) } gameResult := game_model.MapGame(game, s.domain) gameResult.Scenario, err = s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioID) @@ -93,10 +100,13 @@ func (s *GameService) GetFullGame( ) (*game_model.Game, error) { game, err := s.gamesRepo.GetGameByID(ctx, id) if err != nil { - return nil, err + return nil, mapGameError(err) } scenario, err := s.scenariosRepo.GetScenarioByID(ctx, game.ScenarioID) if err != nil { + if errors.Is(err, scenarios_repo.ErrScenarioNotFound) { + return nil, scenarios_service.ErrScenarioNotFound + } return nil, err } game.Scenario = scenario @@ -135,7 +145,7 @@ func (s *GameService) DeleteGame( ctx context.Context, id int, ) error { - return s.gamesRepo.DeleteGameByID(ctx, id) + return mapGameError(s.gamesRepo.DeleteGameByID(ctx, id)) } func (s *GameService) GetGames( @@ -161,32 +171,39 @@ func (s *GameService) UpdateGame( if format == "" { game, err := s.gamesRepo.GetGameByID(ctx, id) if err != nil { - return err + return mapGameError(err) } format = game.Format } if format != "online" && format != "offline" { - return errors.New("Формат игры должен быть online или offline") + return ErrInvalidGameFormat } - return s.gamesRepo.UpdateGame(ctx, id, name, description, location, startAt, scenarioID, format) + return mapGameError(s.gamesRepo.UpdateGame(ctx, id, name, description, location, startAt, scenarioID, format)) } func (s *GameService) StartGame(ctx context.Context, id int) error { - return s.gamesRepo.StartGame(ctx, id) + return mapGameError(s.gamesRepo.StartGame(ctx, id)) } func (s *GameService) PauseGame(ctx context.Context, id int) error { - return s.gamesRepo.PauseGame(ctx, id) + return mapGameError(s.gamesRepo.PauseGame(ctx, id)) } func (s *GameService) PlayGame(ctx context.Context, id int) error { - return s.gamesRepo.PlayGame(ctx, id) + return mapGameError(s.gamesRepo.PlayGame(ctx, id)) } func (s *GameService) FinishGame(ctx context.Context, id int) error { - return s.gamesRepo.FinishGame(ctx, id) + return mapGameError(s.gamesRepo.FinishGame(ctx, id)) } func (s *GameService) ResetGame(ctx context.Context, id int) error { - return s.gamesRepo.ResetGame(ctx, id) + return mapGameError(s.gamesRepo.ResetGame(ctx, id)) +} + +func mapGameError(err error) error { + if errors.Is(err, games_repo.ErrGameNotFound) { + return ErrGameNotFound + } + return err }