Files
2026-10-13 13:50:13 +07:00

144 lines
5.0 KiB
Go

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