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") } }) } }