This commit is contained in:
2026-10-03 00:55:28 +07:00
parent bd1120d10f
commit fc2586f72a
6 changed files with 109 additions and 69 deletions
+8 -7
View File
@@ -36,11 +36,11 @@ func (s *server) DownloadScenarioArchive(ctx context.Context, req *proto.Downloa
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, scenarios_service.ErrScenarioNotFound): 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): case errors.Is(err, scenarios_service.ErrScenarioNotOwner):
return nil, status.Errorf(codes.PermissionDenied, "%v", err) return nil, status.Error(codes.PermissionDenied, err.Error())
default: 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" filename := transliterated + ".zip"
if err := grpc.SetHeader(ctx, metadata.Pairs("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))); err != nil { 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{ 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) id, err := s.scenarioService.UploadArchive(ctx, req.Data, claims.UserID)
if err != nil { if err != nil {
return &proto.UploadScenarioArchiveRsp{ if errors.Is(err, scenarios_service.ErrInvalidArchive) {
Error: err.Error(), return nil, status.Error(codes.InvalidArgument, scenarios_service.ErrInvalidArchive.Error())
}, nil }
return nil, status.Error(codes.Internal, "не удалось загрузить архив сценария")
} }
return &proto.UploadScenarioArchiveRsp{ return &proto.UploadScenarioArchiveRsp{
ID: int32(id), ID: int32(id),
+25
View File
@@ -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)
}
}
+35
View File
@@ -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")
}
})
}
}
+9 -25
View File
@@ -27,9 +27,7 @@ func (s *server) AddScenarioPlace(ctx context.Context, req *proto.AddScenarioPla
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.AddScenarioPlaceRsp{ return nil, scenarioErrorStatus(err, "не удалось добавить место сценария")
Error: err.Error(),
}, nil
} }
return &proto.AddScenarioPlaceRsp{}, nil return &proto.AddScenarioPlaceRsp{}, nil
@@ -53,9 +51,7 @@ func (s *server) UpdateScenarioPlace(ctx context.Context, req *proto.UpdateScena
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.UpdateScenarioPlaceRsp{ return nil, scenarioErrorStatus(err, "не удалось изменить место сценария")
Error: err.Error(),
}, nil
} }
return &proto.UpdateScenarioPlaceRsp{}, nil return &proto.UpdateScenarioPlaceRsp{}, nil
@@ -78,9 +74,7 @@ func (s *server) DeleteScenarioPlace(ctx context.Context, req *proto.DeleteScena
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.DeleteScenarioPlaceRsp{ return nil, scenarioErrorStatus(err, "не удалось удалить место сценария")
Error: err.Error(),
}, nil
} }
return &proto.DeleteScenarioPlaceRsp{}, nil return &proto.DeleteScenarioPlaceRsp{}, nil
@@ -103,9 +97,7 @@ func (s *server) AddScenarioQuestion(ctx context.Context, req *proto.AddScenario
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.AddScenarioQuestionRsp{ return nil, scenarioErrorStatus(err, "не удалось добавить вопрос сценария")
Error: err.Error(),
}, nil
} }
return &proto.AddScenarioQuestionRsp{}, nil return &proto.AddScenarioQuestionRsp{}, nil
@@ -129,9 +121,7 @@ func (s *server) UpdateScenarioQuestion(ctx context.Context, req *proto.UpdateSc
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.UpdateScenarioQuestionRsp{ return nil, scenarioErrorStatus(err, "не удалось изменить вопрос сценария")
Error: err.Error(),
}, nil
} }
return &proto.UpdateScenarioQuestionRsp{}, nil return &proto.UpdateScenarioQuestionRsp{}, nil
@@ -154,9 +144,7 @@ func (s *server) DeleteScenarioQuestion(ctx context.Context, req *proto.DeleteSc
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.DeleteScenarioQuestionRsp{ return nil, scenarioErrorStatus(err, "не удалось удалить вопрос сценария")
Error: err.Error(),
}, nil
} }
return &proto.DeleteScenarioQuestionRsp{}, nil return &proto.DeleteScenarioQuestionRsp{}, nil
@@ -179,9 +167,7 @@ func (s *server) UpdateScenarioBackground(ctx context.Context, req *proto.Update
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.UpdateScenarioBackgroundRsp{ return nil, scenarioErrorStatus(err, "не удалось изменить фон сценария")
Error: err.Error(),
}, nil
} }
return &proto.UpdateScenarioBackgroundRsp{}, nil return &proto.UpdateScenarioBackgroundRsp{}, nil
@@ -204,9 +190,7 @@ func (s *server) UpdateScenarioIntro(ctx context.Context, req *proto.UpdateScena
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.UpdateScenarioIntroRsp{ return nil, scenarioErrorStatus(err, "не удалось изменить введение сценария")
Error: err.Error(),
}, nil
} }
return &proto.UpdateScenarioIntroRsp{}, nil 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") 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 { 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 return &proto.UpdateScenarioConclusionRsp{}, nil
} }
+11 -29
View File
@@ -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) id, err := s.scenarioService.AddScenario(ctx, req.Name, claims.UserID)
if err != nil { if err != nil {
return &proto.AddScenarioRsp{ return nil, scenarioErrorStatus(err, "не удалось создать сценарий")
Error: err.Error(),
}, nil
} }
return &proto.AddScenarioRsp{ return &proto.AddScenarioRsp{
ID: int32(id), 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) scenarios, err := s.scenarioService.GetScenariosByAuthorID(ctx, claims.UserID)
if err != nil { if err != nil {
return &proto.GetMyScenariosRsp{ return nil, scenarioErrorStatus(err, "не удалось получить сценарии автора")
Error: err.Error(),
}, nil
} }
return &proto.GetMyScenariosRsp{ 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) { func (s *server) GetScenariosCatalog(ctx context.Context, req *proto.GetScenariosCatalogReq) (*proto.GetScenariosCatalogRsp, error) {
scenarios, err := s.scenarioService.GetScenariosCatalog(ctx) scenarios, err := s.scenarioService.GetScenariosCatalog(ctx)
if err != nil { if err != nil {
return &proto.GetScenariosCatalogRsp{ return nil, scenarioErrorStatus(err, "не удалось получить каталог сценариев")
Error: err.Error(),
}, nil
} }
if err := s.addScenarioRatings(ctx, scenarios); err != nil { if err := s.addScenarioRatings(ctx, scenarios); err != nil {
return &proto.GetScenariosCatalogRsp{Error: err.Error()}, nil return nil, scenarioErrorStatus(err, "не удалось получить рейтинг сценариев")
} }
return &proto.GetScenariosCatalogRsp{ 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)) scenario, err := s.scenarioService.GetFullScenarioByID(ctx, int(req.ID))
if err != nil { if err != nil {
return &proto.GetScenarioRsp{ return nil, scenarioErrorStatus(err, "не удалось получить сценарий")
Error: err.Error(),
}, nil
} }
return &proto.GetScenarioRsp{ 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) { func (s *server) GetScenario(ctx context.Context, req *proto.GetScenarioReq) (*proto.GetScenarioRsp, error) {
scenario, err := s.scenarioService.GetScenarioByID(ctx, int(req.ID)) scenario, err := s.scenarioService.GetScenarioByID(ctx, int(req.ID))
if err != nil { if err != nil {
return &proto.GetScenarioRsp{ return nil, scenarioErrorStatus(err, "не удалось получить сценарий")
Error: err.Error(),
}, nil
} }
if err := s.addScenarioRatings(ctx, []*scenarios_service.Scenario{scenario}); err != nil { 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{ return &proto.GetScenarioRsp{
@@ -136,9 +126,7 @@ func (s *server) UpdateScenario(ctx context.Context, req *proto.UpdateScenarioRe
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.UpdateScenarioRsp{ return nil, scenarioErrorStatus(err, "не удалось изменить сценарий")
Error: err.Error(),
}, nil
} }
return &proto.UpdateScenarioRsp{}, nil return &proto.UpdateScenarioRsp{}, nil
@@ -161,9 +149,7 @@ func (s *server) PublicScenario(ctx context.Context, req *proto.PublicScenarioRe
roles.HasRole(claims, roles.Admin), // публикация = момент отчуждения прав roles.HasRole(claims, roles.Admin), // публикация = момент отчуждения прав
) )
if err != nil { if err != nil {
return &proto.PublicScenarioRsp{ return nil, scenarioErrorStatus(err, "не удалось опубликовать сценарий")
Error: err.Error(),
}, nil
} }
return &proto.PublicScenarioRsp{}, nil return &proto.PublicScenarioRsp{}, nil
@@ -189,9 +175,7 @@ func (s *server) DraftScenario(ctx context.Context, req *proto.DraftScenarioReq)
true, true,
) )
if err != nil { if err != nil {
return &proto.DraftScenarioRsp{ return nil, scenarioErrorStatus(err, "не удалось снять сценарий с публикации")
Error: err.Error(),
}, nil
} }
return &proto.DraftScenarioRsp{}, nil return &proto.DraftScenarioRsp{}, nil
@@ -215,9 +199,7 @@ func (s *server) DeleteScenario(ctx context.Context, req *proto.DeleteScenarioRe
roles.HasRole(claims, roles.Admin), roles.HasRole(claims, roles.Admin),
) )
if err != nil { if err != nil {
return &proto.DeleteScenarioRsp{ return nil, scenarioErrorStatus(err, "не удалось удалить сценарий")
Error: err.Error(),
}, nil
} }
return &proto.DeleteScenarioRsp{}, nil return &proto.DeleteScenarioRsp{}, nil
+21 -8
View File
@@ -91,7 +91,7 @@ func (s *ScenarioService) GetFullScenarioByID(
) (*Scenario, error) { ) (*Scenario, error) {
scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id) scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id)
if err != nil { if err != nil {
return nil, err return nil, mapScenarioError(err)
} }
return mapScenario(scenario, s.domain) return mapScenario(scenario, s.domain)
} }
@@ -102,7 +102,7 @@ func (s *ScenarioService) GetScenarioByID(
) (*Scenario, error) { ) (*Scenario, error) {
scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id) scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id)
if err != nil { if err != nil {
return nil, err return nil, mapScenarioError(err)
} }
return MapScenarioLight(scenario, s.domain), nil return MapScenarioLight(scenario, s.domain), nil
} }
@@ -134,6 +134,12 @@ var ErrScenarioNotOwner = errors.New("Сценарий принадлежит д
// ErrScenarioNotFound — сценарий не найден или уже удалён. // ErrScenarioNotFound — сценарий не найден или уже удалён.
var ErrScenarioNotFound = errors.New("Сценарий не найден") var ErrScenarioNotFound = errors.New("Сценарий не найден")
// ErrInvalidArchive — архив не соответствует формату сценария.
var ErrInvalidArchive = errors.New("Некорректный архив сценария")
// ErrInvalidScenarioData — данные истории сценария не проходят валидацию.
var ErrInvalidScenarioData = errors.New("Некорректные данные сценария")
// MaxArchiveSize возвращает максимальный размер загружаемого архива. // MaxArchiveSize возвращает максимальный размер загружаемого архива.
func MaxArchiveSize() int { func MaxArchiveSize() int {
return scenario_archive.MaxArchiveSize() return scenario_archive.MaxArchiveSize()
@@ -150,10 +156,10 @@ func (s *ScenarioService) getScenarioForChange(
) (*repos.Scenario, error) { ) (*repos.Scenario, error) {
scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id) scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id)
if err != nil { if err != nil {
return nil, err return nil, mapScenarioError(err)
} }
if scenario.IsDeleted { if scenario.IsDeleted {
return nil, scenarios_repo.ErrScenarioNotFound return nil, ErrScenarioNotFound
} }
if isAdmin { if isAdmin {
return scenario, nil return scenario, nil
@@ -164,6 +170,13 @@ func (s *ScenarioService) getScenarioForChange(
return scenario, nil return scenario, nil
} }
func mapScenarioError(err error) error {
if errors.Is(err, scenarios_repo.ErrScenarioNotFound) {
return ErrScenarioNotFound
}
return err
}
func (s *ScenarioService) UpdateScenarioStatusByID( func (s *ScenarioService) UpdateScenarioStatusByID(
ctx context.Context, ctx context.Context,
id int, id int,
@@ -455,7 +468,7 @@ func (s *ScenarioService) UploadArchive(
) (int, error) { ) (int, error) {
bundle, err := s.scenarioArchive.Unpack(data) bundle, err := s.scenarioArchive.Unpack(data)
if err != nil { if err != nil {
return 0, err return 0, fmt.Errorf("%w: %v", ErrInvalidArchive, err)
} }
scenario := bundle.Scenario scenario := bundle.Scenario
@@ -482,7 +495,7 @@ func (s *ScenarioService) UploadArchive(
} }
content, exists := bundle.Files[archiveRef] content, exists := bundle.Files[archiveRef]
if !exists { if !exists {
return "", fmt.Errorf("изображение %q не найдено в архиве", archiveRef) return "", fmt.Errorf("%w: изображение %q не найдено в архиве", ErrInvalidArchive, archiveRef)
} }
name, err := file_storage.NewName() name, err := file_storage.NewName()
if err != nil { if err != nil {
@@ -535,7 +548,7 @@ func (s *ScenarioService) UploadArchive(
} }
storyJSON, err := normalizeStory(scenario.Story) storyJSON, err := normalizeStory(scenario.Story)
if err != nil { if err != nil {
return 0, err return 0, fmt.Errorf("%w: %v", ErrInvalidArchive, err)
} }
id, err := s.scenariosRepo.AddScenario(ctx, scenario.Name, authorID) 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) storyString, err := normalizeStory(story)
if err != nil { if err != nil {
return err return fmt.Errorf("%w: %v", ErrInvalidScenarioData, err)
} }
return s.scenariosRepo.UpdateStoryByScenarioID(ctx, id, storyString) return s.scenariosRepo.UpdateStoryByScenarioID(ctx, id, storyString)