package app import ( "bytes" "context" "errors" "fmt" "io" "strings" "testing" "evening_detective_server/internal/repos" "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 receiptAppRepos struct{ team_service.ITeamsRepository } func (receiptAppRepos) GetGameByID(context.Context, int) (*repos.Game, error) { return &repos.Game{ID: 1}, nil } func (receiptAppRepos) GetTeamByID(context.Context, string) (*repos.Team, error) { return &repos.Team{ID: "019a1234-5678-7abc-8def-000000000007", GameID: 1, Name: "Сыщики", Password: "test-qr"}, nil } type receiptAppPrinter struct{ calls int } func (p *receiptAppPrinter) Print(context.Context, []byte) error { p.calls++; return nil } func TestTeamReceiptAccess(t *testing.T) { for _, tc := range []struct { name string ctx context.Context venue bool req *proto.TeamReceiptReq want codes.Code }{ {"anonymous", context.Background(), true, nil, codes.Unauthenticated}, {"user", ctxWithClaims("user"), true, nil, codes.PermissionDenied}, {"prod", ctxWithClaims("admin"), false, &proto.TeamReceiptReq{GameID: 1}, codes.FailedPrecondition}, {"empty", ctxWithClaims("organizer"), true, nil, codes.InvalidArgument}, {"bad team", ctxWithClaims("author"), true, &proto.TeamReceiptReq{GameID: 1, TeamID: "bad"}, codes.InvalidArgument}, } { t.Run(tc.name, func(t *testing.T) { s := &server{teamService: team_service.NewTeamService(nil, nil, nil, "", nil, tc.venue, nil)} _, err := s.PreviewTeamReceipt(tc.ctx, tc.req) if status.Code(err) != tc.want { t.Fatalf("preview code=%v", status.Code(err)) } _, err = s.PrintTeamReceipt(tc.ctx, tc.req) if status.Code(err) != tc.want { t.Fatalf("print code=%v", status.Code(err)) } }) } } func TestTeamReceiptResponse(t *testing.T) { for _, role := range []string{"admin", "organizer", "author"} { t.Run(role, func(t *testing.T) { repos := receiptAppRepos{} printer := &receiptAppPrinter{} s := &server{teamService: team_service.NewTeamService(repos, repos, nil, "https://game.test", nil, true, printer)} stream := &mockServerTransportStream{} ctx := grpc.NewContextWithServerTransportStream(ctxWithClaims(role), stream) req := &proto.TeamReceiptReq{GameID: 1, TeamID: "019a1234-5678-7abc-8def-000000000007"} body, err := s.PreviewTeamReceipt(ctx, req) if err != nil { t.Fatal(err) } if body.ContentType != "image/png" || !bytes.HasPrefix(body.Data, []byte("\x89PNG")) { t.Fatal("not a PNG") } if values := stream.header.Get("cache-control"); len(values) != 1 || values[0] != "no-store" { t.Fatal("preview may be cached") } if _, err := s.PrintTeamReceipt(ctx, req); err != nil { t.Fatal(err) } if printer.calls != 1 { t.Fatal("incorrect number of prints") } }) } } func TestTeamReceiptErrorMapping(t *testing.T) { for _, tc := range []struct { err error want codes.Code }{ {team_service.ErrInvalidReceiptRequest, codes.InvalidArgument}, {team_service.ErrGameNotFound, codes.NotFound}, {team_service.ErrTeamNotFound, codes.NotFound}, {team_service.ErrReceiptVenueOnly, codes.FailedPrecondition}, {team_service.ErrReceiptTeamCancelled, codes.FailedPrecondition}, {team_service.ErrInvalidReceipt, codes.FailedPrecondition}, {team_service.ErrPrinterNotConfigured, codes.FailedPrecondition}, {team_service.ErrPrinterBusy, codes.ResourceExhausted}, {team_service.ErrPrinterNoPaper, codes.FailedPrecondition}, {team_service.ErrPrinterOverheated, codes.FailedPrecondition}, {team_service.ErrPrinterLowBattery, codes.FailedPrecondition}, {team_service.ErrPrinterNotReady, codes.FailedPrecondition}, {team_service.ErrPrinterProtocol, codes.Unavailable}, {team_service.ErrPrinterIncomplete, codes.Unavailable}, {team_service.ErrPrinterUnavailable, codes.Unavailable}, {context.Canceled, codes.Canceled}, {context.DeadlineExceeded, codes.DeadlineExceeded}, {errors.New("private-device-address-and-token"), codes.Internal}, } { t.Run(tc.err.Error(), func(t *testing.T) { err := teamReceiptError(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() == tc.err.Error() { t.Fatal("private error exposed") } }) } } func TestTeamReceiptDisconnectWarnsBeforeRetry(t *testing.T) { for _, tc := range []struct { name string err error }{ {"Bluetooth disconnect", io.EOF}, {"worker response lost", fmt.Errorf("worker: %w", team_service.ErrPrinterUnavailable)}, } { t.Run(tc.name, func(t *testing.T) { code := team_service.ReceiptPrinterErrorCode(tc.err) err := teamReceiptError(team_service.ReceiptPrinterError(code)) if status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), "перед повторной печатью проверьте, вышел ли чек") { t.Fatalf("missing retry warning: %v", err) } }) } }