diff --git a/internal/app/participation.go b/internal/app/participation.go index 4ce150f..0f83314 100644 --- a/internal/app/participation.go +++ b/internal/app/participation.go @@ -31,6 +31,9 @@ func participationError(err error) string { } func (s *server) GetParticipation(ctx context.Context, req *proto.GetParticipationReq) (*proto.ParticipationRsp, error) { _ = grpc.SetHeader(ctx, metadata.Pairs("Cache-Control", "no-store")) + if req == nil { + return &proto.ParticipationRsp{Error: participationError(participation_service.ErrParticipationLink)}, nil + } p, err := s.participationService.GetParticipation(ctx, req.Token) result := mapParticipationResponse(p) if err != nil { @@ -40,8 +43,11 @@ func (s *server) GetParticipation(ctx context.Context, req *proto.GetParticipati } func (s *server) AnswerParticipation(ctx context.Context, req *proto.AnswerParticipationReq) (*proto.ParticipationRsp, error) { _ = grpc.SetHeader(ctx, metadata.Pairs("Cache-Control", "no-store")) + if req == nil { + return &proto.ParticipationRsp{Error: participationError(participation_service.ErrParticipationLink)}, nil + } if req.Answer != "confirmed" && req.Answer != "cancelled" { - return &proto.ParticipationRsp{Error: "Выберите подтверждение или отмену участия"}, nil + return &proto.ParticipationRsp{Error: participation_service.ErrInvalidAnswer.Error()}, nil } p, err := s.participationService.AnswerParticipation(ctx, req.Token, req.Answer) result := mapParticipationResponse(p) diff --git a/internal/app/participation_test.go b/internal/app/participation_test.go new file mode 100644 index 0000000..3b7e1ea --- /dev/null +++ b/internal/app/participation_test.go @@ -0,0 +1,31 @@ +package app + +import ( + "errors" + "strings" + "testing" + + "evening_detective_server/internal/services/participation_service" +) + +func TestParticipationError(t *testing.T) { + for _, tc := range []struct { + name string + err error + want string + }{ + {"invalid link", participation_service.ErrParticipationLink, "Ссылка недействительна"}, + {"closed", participation_service.ErrParticipationClosed, "Приём ответов закрыт"}, + {"final", participation_service.ErrParticipationFinal, "Решение уже принято и не может быть изменено"}, + {"internal", errors.New("private database detail"), "Не удалось обработать ответ. Попробуйте позже."}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := participationError(tc.err); got != tc.want { + t.Fatalf("message=%q, want %q", got, tc.want) + } + if strings.Contains(participationError(tc.err), "private database detail") { + t.Fatal("private error exposed") + } + }) + } +} diff --git a/internal/services/participation_service/participation.go b/internal/services/participation_service/participation.go index 42886e5..0cf9755 100644 --- a/internal/services/participation_service/participation.go +++ b/internal/services/participation_service/participation.go @@ -1,9 +1,8 @@ package participation_service import ( + "errors" "time" - - "evening_detective_server/internal/repos/teams_repo" ) // Participation — данные участия, доступные транспортному и email-сценариям. @@ -20,7 +19,8 @@ type Participation struct { } var ( - ErrParticipationLink = teams_repo.ErrParticipationLink - ErrParticipationClosed = teams_repo.ErrParticipationClosed - ErrParticipationFinal = teams_repo.ErrParticipationFinal + ErrParticipationLink = errors.New("Ссылка недействительна") + ErrParticipationClosed = errors.New("Приём ответов закрыт") + ErrParticipationFinal = errors.New("Решение уже принято и не может быть изменено") + ErrInvalidAnswer = errors.New("Выберите подтверждение или отмену участия") ) diff --git a/internal/services/participation_service/service.go b/internal/services/participation_service/service.go index 099fdf4..2a023a1 100644 --- a/internal/services/participation_service/service.go +++ b/internal/services/participation_service/service.go @@ -30,11 +30,26 @@ func NewParticipationService(teamsRepo *teams_repo.TeamsRepo, appBaseURL string) func (s *ParticipationService) GetParticipation(ctx context.Context, token string) (*Participation, error) { p, err := s.teamsRepo.GetParticipation(ctx, token) - return mapParticipation(p), err + return mapParticipation(p), mapParticipationError(err) } func (s *ParticipationService) AnswerParticipation(ctx context.Context, token, answer string) (*Participation, error) { p, err := s.teamsRepo.AnswerParticipation(ctx, token, answer) - return mapParticipation(p), err + return mapParticipation(p), mapParticipationError(err) +} + +func mapParticipationError(err error) error { + switch { + case errors.Is(err, teams_repo.ErrParticipationLink): + return ErrParticipationLink + case errors.Is(err, teams_repo.ErrParticipationClosed): + return ErrParticipationClosed + case errors.Is(err, teams_repo.ErrParticipationFinal): + return ErrParticipationFinal + case err != nil && err.Error() == ErrInvalidAnswer.Error(): + return ErrInvalidAnswer + default: + return err + } } // Стабильные токены для разных целей сохраняют работоспособность ссылок при