generated from VLADIMIR/template
up
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mime"
|
||||
|
||||
"evening_detective_server/internal/modules/roles"
|
||||
"evening_detective_server/proto"
|
||||
|
||||
"google.golang.org/genproto/googleapis/api/httpbody"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (s *server) DownloadGamePrintMaterials(ctx context.Context, req *proto.GetGameReq) (*httpbody.HttpBody, error) {
|
||||
claims, err := claimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) {
|
||||
return nil, status.Error(codes.PermissionDenied, "нет доступа к материалам игры")
|
||||
}
|
||||
if req == nil || req.ID <= 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры")
|
||||
}
|
||||
data, filename, err := s.gamePrintService.Download(ctx, int(req.ID))
|
||||
if err != nil {
|
||||
return nil, gamePrintError(err)
|
||||
}
|
||||
if err := grpc.SetHeader(ctx, metadata.Pairs(
|
||||
"Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}),
|
||||
"X-Content-Type-Options", "nosniff",
|
||||
"Cache-Control", "no-store",
|
||||
)); err != nil {
|
||||
return nil, status.Error(codes.Internal, "не удалось установить заголовки архива")
|
||||
}
|
||||
return &httpbody.HttpBody{
|
||||
ContentType: "application/zip",
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"evening_detective_server/internal/modules/file_storage"
|
||||
"evening_detective_server/internal/services/game_print_service"
|
||||
"evening_detective_server/internal/services/scenarios_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func gamePrintError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, team_service.ErrGameNotFound), errors.Is(err, scenarios_service.ErrScenarioNotFound):
|
||||
return status.Error(codes.NotFound, "игра или сценарий не найдены")
|
||||
case errors.Is(err, game_print_service.ErrPackageTooLarge):
|
||||
return status.Error(codes.ResourceExhausted, game_print_service.ErrPackageTooLarge.Error())
|
||||
case errors.Is(err, game_print_service.ErrInvalidPrintText):
|
||||
return status.Error(codes.FailedPrecondition, "текст не помещается на лист или содержит неподдерживаемые символы")
|
||||
case errors.Is(err, file_storage.ErrFileNotFound):
|
||||
return status.Error(codes.FailedPrecondition, "не найден файл улики; проверьте материалы сценария")
|
||||
case errors.Is(err, context.Canceled):
|
||||
return status.Error(codes.Canceled, "скачивание отменено")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return status.Error(codes.DeadlineExceeded, "превышено время подготовки материалов")
|
||||
default:
|
||||
log.Printf("game print materials: %v", err)
|
||||
return status.Error(codes.Internal, "не удалось подготовить материалы для печати")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"evening_detective_server/internal/modules/file_storage"
|
||||
"evening_detective_server/internal/repos"
|
||||
"evening_detective_server/internal/services/game_print_service"
|
||||
"evening_detective_server/internal/services/scenarios_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
"evening_detective_server/proto"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type printGameStub struct{}
|
||||
|
||||
func (printGameStub) GetGameByID(context.Context, int) (*repos.Game, error) {
|
||||
return &repos.Game{ID: 1, Name: "Дело №1", Format: "offline"}, nil
|
||||
}
|
||||
|
||||
func (printGameStub) GetFullScenarioByID(context.Context, int) (*scenarios_service.Scenario, error) {
|
||||
return &scenarios_service.Scenario{}, nil
|
||||
}
|
||||
|
||||
func (printGameStub) GetActiveTeamsByGameID(context.Context, int) ([]*repos.Team, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestDownloadGamePrintMaterialsHeaders(t *testing.T) {
|
||||
for _, role := range []string{"organizer", "author", "admin"} {
|
||||
t.Run(role, func(t *testing.T) {
|
||||
stub := printGameStub{}
|
||||
s := &server{gamePrintService: game_print_service.NewGamePrintService(stub, stub, stub, nil, nil, "")}
|
||||
stream := &mockServerTransportStream{}
|
||||
ctx := grpc.NewContextWithServerTransportStream(ctxWithClaims(role), stream)
|
||||
body, err := s.DownloadGamePrintMaterials(ctx, &proto.GetGameReq{ID: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.ContentType != "application/zip" || !bytes.HasPrefix(body.Data, []byte("PK")) {
|
||||
t.Fatal("invalid ZIP response")
|
||||
}
|
||||
for key, want := range map[string]string{
|
||||
"content-disposition": "attachment; filename=game-1-print-materials.zip",
|
||||
"cache-control": "no-store",
|
||||
"x-content-type-options": "nosniff",
|
||||
} {
|
||||
values := stream.header.Get(key)
|
||||
if len(values) != 1 || values[0] != want {
|
||||
t.Errorf("%s = %v, want %s", key, values, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadGamePrintMaterialsAccess(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
request *proto.GetGameReq
|
||||
want codes.Code
|
||||
}{
|
||||
{"anonymous", context.Background(), &proto.GetGameReq{ID: 1}, codes.Unauthenticated},
|
||||
{"player", ctxWithClaims("user"), &proto.GetGameReq{ID: 1}, codes.PermissionDenied},
|
||||
{"developer", ctxWithClaims("developer"), &proto.GetGameReq{ID: 1}, codes.PermissionDenied},
|
||||
{"organizer invalid ID", ctxWithClaims("organizer"), &proto.GetGameReq{}, codes.InvalidArgument},
|
||||
{"author invalid ID", ctxWithClaims("author"), &proto.GetGameReq{ID: -1}, codes.InvalidArgument},
|
||||
{"admin nil request", ctxWithClaims("admin"), nil, codes.InvalidArgument},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := (&server{}).DownloadGamePrintMaterials(tc.ctx, tc.request)
|
||||
if status.Code(err) != tc.want {
|
||||
t.Fatalf("code = %v, want %v", status.Code(err), tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGamePrintErrorMapping(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
err error
|
||||
want codes.Code
|
||||
}{
|
||||
{team_service.ErrGameNotFound, codes.NotFound},
|
||||
{scenarios_service.ErrScenarioNotFound, codes.NotFound},
|
||||
{game_print_service.ErrPackageTooLarge, codes.ResourceExhausted},
|
||||
{game_print_service.ErrInvalidPrintText, codes.FailedPrecondition},
|
||||
{file_storage.ErrFileNotFound, codes.FailedPrecondition},
|
||||
{context.Canceled, codes.Canceled},
|
||||
{context.DeadlineExceeded, codes.DeadlineExceeded},
|
||||
{errors.New("private storage detail"), codes.Internal},
|
||||
} {
|
||||
t.Run(tc.err.Error(), func(t *testing.T) {
|
||||
err := gamePrintError(fmt.Errorf("wrapped: %w", tc.err))
|
||||
if status.Code(err) != tc.want {
|
||||
t.Fatalf("code = %v, want %v", status.Code(err), tc.want)
|
||||
}
|
||||
if tc.want == codes.Internal && status.Convert(err).Message() != "не удалось подготовить материалы для печати" {
|
||||
t.Fatal("private error exposed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"evening_detective_server/internal/services/file_service"
|
||||
"evening_detective_server/internal/services/game_export_service"
|
||||
"evening_detective_server/internal/services/game_import_service"
|
||||
"evening_detective_server/internal/services/game_print_service"
|
||||
"evening_detective_server/internal/services/game_results_package_service"
|
||||
"evening_detective_server/internal/services/game_review_service"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
@@ -40,6 +41,7 @@ type server struct {
|
||||
gameResultsService *game_results_package_service.GameResultsPackageService
|
||||
gameResultsApplyService *game_results_package_service.GameResultsApplyService
|
||||
gameService *game_service.GameService
|
||||
gamePrintService *game_print_service.GamePrintService
|
||||
gameplayService *gameplay_service.GameplayService
|
||||
answerService *answer_service.AnswerService
|
||||
reviewService *game_review_service.GameReviewService
|
||||
@@ -76,8 +78,10 @@ func NewServer(
|
||||
gameResultsApplyService *game_results_package_service.GameResultsApplyService,
|
||||
devicePresenceService *device_presence_service.DevicePresenceService,
|
||||
wifiService *wifi_service.Service,
|
||||
gamePrintService *game_print_service.GamePrintService,
|
||||
) proto.EveningDetectiveServerServer {
|
||||
return &server{
|
||||
gamePrintService: gamePrintService,
|
||||
devicePresenceService: devicePresenceService,
|
||||
wifiService: wifiService,
|
||||
gameImportService: gameImportService,
|
||||
|
||||
Reference in New Issue
Block a user