From fc2586f72a56c746eb02e7e5c23d9f4fe6f6331f Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sat, 3 Oct 2026 00:55:28 +0700 Subject: [PATCH] fix --- internal/app/scenario_archive.go | 15 +++---- internal/app/scenario_errors.go | 25 ++++++++++++ internal/app/scenario_errors_test.go | 35 ++++++++++++++++ internal/app/scenario_story.go | 34 +++++----------- internal/app/scenarios.go | 40 +++++-------------- .../services/scenarios_service/service.go | 29 ++++++++++---- 6 files changed, 109 insertions(+), 69 deletions(-) create mode 100644 internal/app/scenario_errors.go create mode 100644 internal/app/scenario_errors_test.go diff --git a/internal/app/scenario_archive.go b/internal/app/scenario_archive.go index fcba3ea..3951e8c 100644 --- a/internal/app/scenario_archive.go +++ b/internal/app/scenario_archive.go @@ -36,11 +36,11 @@ func (s *server) DownloadScenarioArchive(ctx context.Context, req *proto.Downloa if err != nil { switch { case errors.Is(err, scenarios_service.ErrScenarioNotFound): - return nil, status.Errorf(codes.NotFound, "%v", err) + return nil, status.Error(codes.NotFound, err.Error()) case errors.Is(err, scenarios_service.ErrScenarioNotOwner): - return nil, status.Errorf(codes.PermissionDenied, "%v", err) + return nil, status.Error(codes.PermissionDenied, err.Error()) default: - return nil, status.Errorf(codes.Internal, "%v", err) + return nil, status.Error(codes.Internal, "не удалось скачать архив сценария") } } @@ -52,7 +52,7 @@ func (s *server) DownloadScenarioArchive(ctx context.Context, req *proto.Downloa } filename := transliterated + ".zip" if err := grpc.SetHeader(ctx, metadata.Pairs("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))); err != nil { - return nil, status.Errorf(codes.Internal, "failed to set response header: %v", err) + return nil, status.Error(codes.Internal, "не удалось установить заголовки архива") } return &httpbody.HttpBody{ @@ -78,9 +78,10 @@ func (s *server) UploadScenarioArchive(ctx context.Context, req *httpbody.HttpBo id, err := s.scenarioService.UploadArchive(ctx, req.Data, claims.UserID) if err != nil { - return &proto.UploadScenarioArchiveRsp{ - Error: err.Error(), - }, nil + if errors.Is(err, scenarios_service.ErrInvalidArchive) { + return nil, status.Error(codes.InvalidArgument, scenarios_service.ErrInvalidArchive.Error()) + } + return nil, status.Error(codes.Internal, "не удалось загрузить архив сценария") } return &proto.UploadScenarioArchiveRsp{ ID: int32(id), diff --git a/internal/app/scenario_errors.go b/internal/app/scenario_errors.go new file mode 100644 index 0000000..41713c0 --- /dev/null +++ b/internal/app/scenario_errors.go @@ -0,0 +1,25 @@ +package app + +import ( + "errors" + + "evening_detective_server/internal/services/scenarios_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func scenarioErrorStatus(err error, internalMessage string) error { + switch { + case errors.Is(err, scenarios_service.ErrScenarioNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, scenarios_service.ErrScenarioNotOwner): + return status.Error(codes.PermissionDenied, err.Error()) + case errors.Is(err, scenarios_service.ErrScenarioPublished): + return status.Error(codes.FailedPrecondition, err.Error()) + case errors.Is(err, scenarios_service.ErrInvalidScenarioData): + return status.Error(codes.InvalidArgument, err.Error()) + default: + return status.Error(codes.Internal, internalMessage) + } +} diff --git a/internal/app/scenario_errors_test.go b/internal/app/scenario_errors_test.go new file mode 100644 index 0000000..bec8889 --- /dev/null +++ b/internal/app/scenario_errors_test.go @@ -0,0 +1,35 @@ +package app + +import ( + "errors" + "testing" + + "evening_detective_server/internal/services/scenarios_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestScenarioErrorStatus(t *testing.T) { + for _, tc := range []struct { + name, internalMessage string + err error + code codes.Code + message string + }{ + {"not found", "scenario failed", scenarios_service.ErrScenarioNotFound, codes.NotFound, "Сценарий не найден"}, + {"not owner", "scenario failed", scenarios_service.ErrScenarioNotOwner, codes.PermissionDenied, "Сценарий принадлежит другому пользователю"}, + {"published", "scenario failed", scenarios_service.ErrScenarioPublished, codes.FailedPrecondition, scenarios_service.ErrScenarioPublished.Error()}, + {"internal", "scenario failed", errors.New("private database detail"), codes.Internal, "scenario failed"}, + } { + t.Run(tc.name, func(t *testing.T) { + err := scenarioErrorStatus(tc.err, tc.internalMessage) + if status.Code(err) != tc.code || status.Convert(err).Message() != tc.message { + t.Fatalf("status=%v, want %v %q", err, tc.code, tc.message) + } + if status.Convert(err).Message() == "private database detail" { + t.Fatal("private error exposed") + } + }) + } +} diff --git a/internal/app/scenario_story.go b/internal/app/scenario_story.go index 08459d3..8af822a 100644 --- a/internal/app/scenario_story.go +++ b/internal/app/scenario_story.go @@ -27,9 +27,7 @@ func (s *server) AddScenarioPlace(ctx context.Context, req *proto.AddScenarioPla roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.AddScenarioPlaceRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось добавить место сценария") } return &proto.AddScenarioPlaceRsp{}, nil @@ -53,9 +51,7 @@ func (s *server) UpdateScenarioPlace(ctx context.Context, req *proto.UpdateScena roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.UpdateScenarioPlaceRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось изменить место сценария") } return &proto.UpdateScenarioPlaceRsp{}, nil @@ -78,9 +74,7 @@ func (s *server) DeleteScenarioPlace(ctx context.Context, req *proto.DeleteScena roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.DeleteScenarioPlaceRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось удалить место сценария") } return &proto.DeleteScenarioPlaceRsp{}, nil @@ -103,9 +97,7 @@ func (s *server) AddScenarioQuestion(ctx context.Context, req *proto.AddScenario roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.AddScenarioQuestionRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось добавить вопрос сценария") } return &proto.AddScenarioQuestionRsp{}, nil @@ -129,9 +121,7 @@ func (s *server) UpdateScenarioQuestion(ctx context.Context, req *proto.UpdateSc roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.UpdateScenarioQuestionRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось изменить вопрос сценария") } return &proto.UpdateScenarioQuestionRsp{}, nil @@ -154,9 +144,7 @@ func (s *server) DeleteScenarioQuestion(ctx context.Context, req *proto.DeleteSc roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.DeleteScenarioQuestionRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось удалить вопрос сценария") } return &proto.DeleteScenarioQuestionRsp{}, nil @@ -179,9 +167,7 @@ func (s *server) UpdateScenarioBackground(ctx context.Context, req *proto.Update roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.UpdateScenarioBackgroundRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось изменить фон сценария") } return &proto.UpdateScenarioBackgroundRsp{}, nil @@ -204,9 +190,7 @@ func (s *server) UpdateScenarioIntro(ctx context.Context, req *proto.UpdateScena roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.UpdateScenarioIntroRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось изменить введение сценария") } return &proto.UpdateScenarioIntroRsp{}, nil @@ -221,7 +205,7 @@ func (s *server) UpdateScenarioConclusion(ctx context.Context, req *proto.Update return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if err := s.scenarioService.UpdateScenarioConclusion(ctx, int(req.ID), convertConclusion(req.Conclusion), claims.UserID, roles.HasRole(claims, roles.Admin)); err != nil { - return &proto.UpdateScenarioConclusionRsp{Error: err.Error()}, nil + return nil, scenarioErrorStatus(err, "не удалось изменить заключение сценария") } return &proto.UpdateScenarioConclusionRsp{}, nil } diff --git a/internal/app/scenarios.go b/internal/app/scenarios.go index bbe74a0..ac7140e 100644 --- a/internal/app/scenarios.go +++ b/internal/app/scenarios.go @@ -22,9 +22,7 @@ func (s *server) AddScenario(ctx context.Context, req *proto.AddScenarioReq) (*p id, err := s.scenarioService.AddScenario(ctx, req.Name, claims.UserID) if err != nil { - return &proto.AddScenarioRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось создать сценарий") } return &proto.AddScenarioRsp{ ID: int32(id), @@ -42,9 +40,7 @@ func (s *server) GetMyScenarios(ctx context.Context, req *proto.GetMyScenariosRe scenarios, err := s.scenarioService.GetScenariosByAuthorID(ctx, claims.UserID) if err != nil { - return &proto.GetMyScenariosRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось получить сценарии автора") } return &proto.GetMyScenariosRsp{ @@ -55,12 +51,10 @@ func (s *server) GetMyScenarios(ctx context.Context, req *proto.GetMyScenariosRe func (s *server) GetScenariosCatalog(ctx context.Context, req *proto.GetScenariosCatalogReq) (*proto.GetScenariosCatalogRsp, error) { scenarios, err := s.scenarioService.GetScenariosCatalog(ctx) if err != nil { - return &proto.GetScenariosCatalogRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось получить каталог сценариев") } if err := s.addScenarioRatings(ctx, scenarios); err != nil { - return &proto.GetScenariosCatalogRsp{Error: err.Error()}, nil + return nil, scenarioErrorStatus(err, "не удалось получить рейтинг сценариев") } return &proto.GetScenariosCatalogRsp{ @@ -79,9 +73,7 @@ func (s *server) GetFullScenario(ctx context.Context, req *proto.GetScenarioReq) scenario, err := s.scenarioService.GetFullScenarioByID(ctx, int(req.ID)) if err != nil { - return &proto.GetScenarioRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось получить сценарий") } return &proto.GetScenarioRsp{ @@ -92,12 +84,10 @@ func (s *server) GetFullScenario(ctx context.Context, req *proto.GetScenarioReq) func (s *server) GetScenario(ctx context.Context, req *proto.GetScenarioReq) (*proto.GetScenarioRsp, error) { scenario, err := s.scenarioService.GetScenarioByID(ctx, int(req.ID)) if err != nil { - return &proto.GetScenarioRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось получить сценарий") } if err := s.addScenarioRatings(ctx, []*scenarios_service.Scenario{scenario}); err != nil { - return &proto.GetScenarioRsp{Error: err.Error()}, nil + return nil, scenarioErrorStatus(err, "не удалось получить рейтинг сценария") } return &proto.GetScenarioRsp{ @@ -136,9 +126,7 @@ func (s *server) UpdateScenario(ctx context.Context, req *proto.UpdateScenarioRe roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.UpdateScenarioRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось изменить сценарий") } return &proto.UpdateScenarioRsp{}, nil @@ -161,9 +149,7 @@ func (s *server) PublicScenario(ctx context.Context, req *proto.PublicScenarioRe roles.HasRole(claims, roles.Admin), // публикация = момент отчуждения прав ) if err != nil { - return &proto.PublicScenarioRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось опубликовать сценарий") } return &proto.PublicScenarioRsp{}, nil @@ -189,9 +175,7 @@ func (s *server) DraftScenario(ctx context.Context, req *proto.DraftScenarioReq) true, ) if err != nil { - return &proto.DraftScenarioRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось снять сценарий с публикации") } return &proto.DraftScenarioRsp{}, nil @@ -215,9 +199,7 @@ func (s *server) DeleteScenario(ctx context.Context, req *proto.DeleteScenarioRe roles.HasRole(claims, roles.Admin), ) if err != nil { - return &proto.DeleteScenarioRsp{ - Error: err.Error(), - }, nil + return nil, scenarioErrorStatus(err, "не удалось удалить сценарий") } return &proto.DeleteScenarioRsp{}, nil diff --git a/internal/services/scenarios_service/service.go b/internal/services/scenarios_service/service.go index c532dec..8a84686 100644 --- a/internal/services/scenarios_service/service.go +++ b/internal/services/scenarios_service/service.go @@ -91,7 +91,7 @@ func (s *ScenarioService) GetFullScenarioByID( ) (*Scenario, error) { scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id) if err != nil { - return nil, err + return nil, mapScenarioError(err) } return mapScenario(scenario, s.domain) } @@ -102,7 +102,7 @@ func (s *ScenarioService) GetScenarioByID( ) (*Scenario, error) { scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id) if err != nil { - return nil, err + return nil, mapScenarioError(err) } return MapScenarioLight(scenario, s.domain), nil } @@ -134,6 +134,12 @@ var ErrScenarioNotOwner = errors.New("Сценарий принадлежит д // ErrScenarioNotFound — сценарий не найден или уже удалён. var ErrScenarioNotFound = errors.New("Сценарий не найден") +// ErrInvalidArchive — архив не соответствует формату сценария. +var ErrInvalidArchive = errors.New("Некорректный архив сценария") + +// ErrInvalidScenarioData — данные истории сценария не проходят валидацию. +var ErrInvalidScenarioData = errors.New("Некорректные данные сценария") + // MaxArchiveSize возвращает максимальный размер загружаемого архива. func MaxArchiveSize() int { return scenario_archive.MaxArchiveSize() @@ -150,10 +156,10 @@ func (s *ScenarioService) getScenarioForChange( ) (*repos.Scenario, error) { scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id) if err != nil { - return nil, err + return nil, mapScenarioError(err) } if scenario.IsDeleted { - return nil, scenarios_repo.ErrScenarioNotFound + return nil, ErrScenarioNotFound } if isAdmin { return scenario, nil @@ -164,6 +170,13 @@ func (s *ScenarioService) getScenarioForChange( return scenario, nil } +func mapScenarioError(err error) error { + if errors.Is(err, scenarios_repo.ErrScenarioNotFound) { + return ErrScenarioNotFound + } + return err +} + func (s *ScenarioService) UpdateScenarioStatusByID( ctx context.Context, id int, @@ -455,7 +468,7 @@ func (s *ScenarioService) UploadArchive( ) (int, error) { bundle, err := s.scenarioArchive.Unpack(data) if err != nil { - return 0, err + return 0, fmt.Errorf("%w: %v", ErrInvalidArchive, err) } scenario := bundle.Scenario @@ -482,7 +495,7 @@ func (s *ScenarioService) UploadArchive( } content, exists := bundle.Files[archiveRef] if !exists { - return "", fmt.Errorf("изображение %q не найдено в архиве", archiveRef) + return "", fmt.Errorf("%w: изображение %q не найдено в архиве", ErrInvalidArchive, archiveRef) } name, err := file_storage.NewName() if err != nil { @@ -535,7 +548,7 @@ func (s *ScenarioService) UploadArchive( } storyJSON, err := normalizeStory(scenario.Story) if err != nil { - return 0, err + return 0, fmt.Errorf("%w: %v", ErrInvalidArchive, err) } id, err := s.scenariosRepo.AddScenario(ctx, scenario.Name, authorID) @@ -571,7 +584,7 @@ func (s *ScenarioService) updateStory(ctx context.Context, id int, story *storyt } storyString, err := normalizeStory(story) if err != nil { - return err + return fmt.Errorf("%w: %v", ErrInvalidScenarioData, err) } return s.scenariosRepo.UpdateStoryByScenarioID(ctx, id, storyString)