This commit is contained in:
2026-10-03 02:38:17 +07:00
parent 1375ec9218
commit 9ab0a76e98
10 changed files with 473 additions and 1437 deletions
-3
View File
@@ -131,9 +131,6 @@ func TestUploadFileSuccess(t *testing.T) {
if rsp.FileType != "pdf" {
t.Errorf("FileType = %q, want pdf", rsp.FileType)
}
if rsp.Error != "" {
t.Errorf("Error = %q, want пусто", rsp.Error)
}
}
// mockServerTransportStream — заглушка для grpc.SetHeader в юнит-тестах.
+5 -3
View File
@@ -5,6 +5,8 @@ import (
"evening_detective_server/docs"
proto "evening_detective_server/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// GetTerms возвращает текст Пользовательского соглашения (постоянный URL
@@ -12,7 +14,7 @@ import (
func (s *server) GetTerms(_ context.Context, _ *proto.GetTermsReq) (*proto.GetTermsRsp, error) {
text, err := docs.FS.ReadFile("TERMS.md")
if err != nil {
return &proto.GetTermsRsp{Error: "Не удалось получить документ"}, nil
return nil, status.Error(codes.Internal, "не удалось получить документ")
}
return &proto.GetTermsRsp{Text: string(text)}, nil
}
@@ -22,7 +24,7 @@ func (s *server) GetTerms(_ context.Context, _ *proto.GetTermsReq) (*proto.GetTe
func (s *server) GetPrivacy(_ context.Context, _ *proto.GetPrivacyReq) (*proto.GetPrivacyRsp, error) {
text, err := docs.FS.ReadFile("PRIVACY.md")
if err != nil {
return &proto.GetPrivacyRsp{Error: "Не удалось получить документ"}, nil
return nil, status.Error(codes.Internal, "не удалось получить документ")
}
return &proto.GetPrivacyRsp{Text: string(text)}, nil
}
@@ -32,7 +34,7 @@ func (s *server) GetPrivacy(_ context.Context, _ *proto.GetPrivacyReq) (*proto.G
func (s *server) GetConsent(_ context.Context, _ *proto.GetConsentReq) (*proto.GetConsentRsp, error) {
text, err := docs.FS.ReadFile("CONSENT.md")
if err != nil {
return &proto.GetConsentRsp{Error: "Не удалось получить документ"}, nil
return nil, status.Error(codes.Internal, "не удалось получить документ")
}
return &proto.GetConsentRsp{Text: string(text)}, nil
}
+8 -11
View File
@@ -13,39 +13,39 @@ func TestGetLegalDocs(t *testing.T) {
tests := []struct {
name string
call func() (text, errText string)
call func() string
wantPart string
}{
{
name: "GetTerms отдаёт Пользовательское соглашение",
call: func() (string, string) {
call: func() string {
rsp, err := s.GetTerms(context.Background(), &proto.GetTermsReq{})
if err != nil {
t.Fatalf("GetTerms error: %v", err)
}
return rsp.Text, rsp.Error
return rsp.Text
},
wantPart: "СОГЛАШЕНИЕ",
},
{
name: "GetPrivacy отдаёт Политику конфиденциальности",
call: func() (string, string) {
call: func() string {
rsp, err := s.GetPrivacy(context.Background(), &proto.GetPrivacyReq{})
if err != nil {
t.Fatalf("GetPrivacy error: %v", err)
}
return rsp.Text, rsp.Error
return rsp.Text
},
wantPart: "КОНФИДЕНЦИАЛЬНОСТИ",
},
{
name: "GetConsent отдаёт текст согласия на обработку ПДн",
call: func() (string, string) {
call: func() string {
rsp, err := s.GetConsent(context.Background(), &proto.GetConsentReq{})
if err != nil {
t.Fatalf("GetConsent error: %v", err)
}
return rsp.Text, rsp.Error
return rsp.Text
},
wantPart: "СОГЛАСИЕ",
},
@@ -53,10 +53,7 @@ func TestGetLegalDocs(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
text, errText := tt.call()
if errText != "" {
t.Fatalf("ожидался успех, получена ошибка: %s", errText)
}
text := tt.call()
if !strings.Contains(text, tt.wantPart) {
t.Errorf("текст документа не содержит %q", tt.wantPart)
}
+19 -13
View File
@@ -6,7 +6,9 @@ import (
"evening_detective_server/internal/services/participation_service"
"evening_detective_server/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -23,36 +25,40 @@ func mapParticipationResponse(p *participation_service.Participation) *proto.Par
return result
}
func participationError(err error) string {
if errors.Is(err, participation_service.ErrParticipationLink) || errors.Is(err, participation_service.ErrParticipationFinal) || errors.Is(err, participation_service.ErrParticipationClosed) {
return err.Error()
func participationErrorStatus(err error, internalMessage string) error {
switch {
case errors.Is(err, participation_service.ErrParticipationLink):
return status.Error(codes.NotFound, err.Error())
case errors.Is(err, participation_service.ErrInvalidAnswer):
return status.Error(codes.InvalidArgument, err.Error())
case errors.Is(err, participation_service.ErrParticipationFinal), errors.Is(err, participation_service.ErrParticipationClosed):
return status.Error(codes.FailedPrecondition, err.Error())
default:
return status.Error(codes.Internal, internalMessage)
}
return "Не удалось обработать ответ. Попробуйте позже."
}
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
return nil, participationErrorStatus(participation_service.ErrParticipationLink, "не удалось получить данные об участии")
}
p, err := s.participationService.GetParticipation(ctx, req.Token)
result := mapParticipationResponse(p)
if err != nil {
result.Error = participationError(err)
return nil, participationErrorStatus(err, "не удалось получить данные об участии")
}
return result, nil
return mapParticipationResponse(p), nil
}
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
return nil, participationErrorStatus(participation_service.ErrParticipationLink, "не удалось сохранить ответ")
}
if req.Answer != "confirmed" && req.Answer != "cancelled" {
return &proto.ParticipationRsp{Error: participation_service.ErrInvalidAnswer.Error()}, nil
return nil, participationErrorStatus(participation_service.ErrInvalidAnswer, "не удалось сохранить ответ")
}
p, err := s.participationService.AnswerParticipation(ctx, req.Token, req.Answer)
result := mapParticipationResponse(p)
if err != nil {
result.Error = participationError(err)
return nil, participationErrorStatus(err, "не удалось сохранить ответ")
}
return result, nil
return mapParticipationResponse(p), nil
}
+13 -8
View File
@@ -6,24 +6,29 @@ import (
"testing"
"evening_detective_server/internal/services/participation_service"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestParticipationError(t *testing.T) {
func TestParticipationErrorStatus(t *testing.T) {
for _, tc := range []struct {
name string
err error
code codes.Code
want string
}{
{"invalid link", participation_service.ErrParticipationLink, "Ссылка недействительна"},
{"closed", participation_service.ErrParticipationClosed, "Приём ответов закрыт"},
{"final", participation_service.ErrParticipationFinal, "Решение уже принято и не может быть изменено"},
{"internal", errors.New("private database detail"), "Не удалось обработать ответ. Попробуйте позже."},
{"invalid link", participation_service.ErrParticipationLink, codes.NotFound, "Ссылка недействительна"},
{"invalid answer", participation_service.ErrInvalidAnswer, codes.InvalidArgument, "Выберите подтверждение или отмену участия"},
{"closed", participation_service.ErrParticipationClosed, codes.FailedPrecondition, "Приём ответов закрыт"},
{"final", participation_service.ErrParticipationFinal, codes.FailedPrecondition, "Решение уже принято и не может быть изменено"},
{"internal", errors.New("private database detail"), codes.Internal, "participation failed"},
} {
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)
got := participationErrorStatus(tc.err, "participation failed")
if status.Code(got) != tc.code || status.Convert(got).Message() != tc.want {
t.Fatalf("status=%v, want %s %q", got, tc.code, tc.want)
}
if strings.Contains(participationError(tc.err), "private database detail") {
if tc.code == codes.Internal && strings.Contains(status.Convert(got).Message(), "private database detail") {
t.Fatal("private error exposed")
}
})
+69
View File
@@ -0,0 +1,69 @@
package app
import (
"context"
"strings"
"testing"
"evening_detective_server/internal/services/participation_service"
"evening_detective_server/internal/services/ticket_service"
"evening_detective_server/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func assertGRPCStatus(t *testing.T, err error, wantCode codes.Code, wantMessage string) {
t.Helper()
if status.Code(err) != wantCode {
t.Fatalf("status code = %v, want %v", status.Code(err), wantCode)
}
if status.Convert(err).Message() != wantMessage {
t.Fatalf("status message = %q, want %q", status.Convert(err).Message(), wantMessage)
}
}
func TestGetTicketNotFoundUsesGRPCStatus(t *testing.T) {
rsp, err := (&server{}).GetTicket(context.Background(), nil)
if rsp != nil {
t.Fatalf("GetTicket response = %v, want nil", rsp)
}
assertGRPCStatus(t, err, codes.NotFound, ticket_service.ErrTicketNotFound.Error())
}
func TestAcceptTicketNilRequestUsesGRPCStatus(t *testing.T) {
rsp, err := (&server{}).AcceptTicket(ctxWithClaims("organizer"), nil)
if rsp != nil {
t.Fatalf("AcceptTicket response = %v, want nil", rsp)
}
assertGRPCStatus(t, err, codes.NotFound, ticket_service.ErrTicketNotFound.Error())
}
func TestGetParticipationInvalidRequestUsesGRPCStatus(t *testing.T) {
rsp, err := (&server{}).GetParticipation(context.Background(), nil)
if rsp != nil {
t.Fatalf("GetParticipation response = %v, want nil", rsp)
}
assertGRPCStatus(t, err, codes.NotFound, participation_service.ErrParticipationLink.Error())
}
func TestAnswerParticipationInvalidAnswerUsesGRPCStatus(t *testing.T) {
rsp, err := (&server{}).AnswerParticipation(context.Background(), &proto.AnswerParticipationReq{Answer: "unknown"})
if rsp != nil {
t.Fatalf("AnswerParticipation response = %v, want nil", rsp)
}
assertGRPCStatus(t, err, codes.InvalidArgument, participation_service.ErrInvalidAnswer.Error())
}
func TestResponseMessagesDoNotExposePayloadError(t *testing.T) {
for i := 0; i < proto.File_main_proto.Messages().Len(); i++ {
message := proto.File_main_proto.Messages().Get(i)
if !strings.HasSuffix(string(message.Name()), "Rsp") {
continue
}
for j := 0; j < message.Fields().Len(); j++ {
if message.Fields().Get(j).Name() == "error" {
t.Errorf("%s still exposes payload error field", message.FullName())
}
}
}
}
+3 -3
View File
@@ -17,11 +17,11 @@ import (
func (s *server) GetTicket(ctx context.Context, req *proto.GetTicketReq) (*proto.GetTicketRsp, error) {
_ = grpc.SetHeader(ctx, metadata.Pairs("Cache-Control", "no-store"))
if req == nil {
return &proto.GetTicketRsp{Error: ticket_service.ErrTicketNotFound.Error()}, nil
return nil, ticketErrorStatus(ticket_service.ErrTicketNotFound, "не удалось получить билет")
}
page, err := s.ticketService.Get(ctx, req.Token)
if err != nil {
return &proto.GetTicketRsp{Error: ticket_service.ErrTicketNotFound.Error()}, nil
return nil, ticketErrorStatus(err, "не удалось получить билет")
}
result := &proto.TicketPage{
@@ -49,7 +49,7 @@ func (s *server) AcceptTicket(ctx context.Context, req *proto.AcceptTicketReq) (
return nil, status.Error(codes.PermissionDenied, "permission denied")
}
if req == nil {
return &proto.AcceptTicketRsp{Error: ticket_service.ErrTicketNotFound.Error()}, nil
return nil, ticketErrorStatus(ticket_service.ErrTicketNotFound, "не удалось принять билет")
}
page, err := s.ticketService.Accept(ctx, req.Token, claims.UserID)
if err != nil {