generated from VLADIMIR/template
up
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"evening_detective_server/internal/modules/roles"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
"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 receiptClaims(ctx context.Context) error {
|
||||
claims, err := claimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) {
|
||||
return status.Error(codes.PermissionDenied, "нет доступа к печати чеков команды")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) PreviewTeamReceipt(ctx context.Context, req *proto.TeamReceiptReq) (*httpbody.HttpBody, error) {
|
||||
if err := receiptClaims(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := s.teamService.PreviewTeamReceipt(ctx, int(req.GetGameID()), req.GetTeamID())
|
||||
if err != nil {
|
||||
return nil, teamReceiptError(err)
|
||||
}
|
||||
if err := grpc.SetHeader(ctx, metadata.Pairs("Cache-Control", "no-store", "X-Content-Type-Options", "nosniff")); err != nil {
|
||||
return nil, status.Error(codes.Internal, "не удалось установить заголовки чека")
|
||||
}
|
||||
return &httpbody.HttpBody{
|
||||
ContentType: "image/png",
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) PrintTeamReceipt(ctx context.Context, req *proto.TeamReceiptReq) (*proto.PrintTeamReceiptRsp, error) {
|
||||
if err := receiptClaims(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, team_service.ReceiptTimeout)
|
||||
defer cancel()
|
||||
if err := s.teamService.PrintTeamReceipt(ctx, int(req.GetGameID()), req.GetTeamID()); err != nil {
|
||||
return nil, teamReceiptError(err)
|
||||
}
|
||||
return &proto.PrintTeamReceiptRsp{}, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func teamReceiptError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, team_service.ErrInvalidReceiptRequest):
|
||||
return status.Error(codes.InvalidArgument, team_service.ErrInvalidReceiptRequest.Error())
|
||||
case errors.Is(err, team_service.ErrGameNotFound), errors.Is(err, team_service.ErrTeamNotFound):
|
||||
return status.Error(codes.NotFound, "игра или команда не найдены")
|
||||
case errors.Is(err, team_service.ErrReceiptVenueOnly):
|
||||
return status.Error(codes.FailedPrecondition, team_service.ErrReceiptVenueOnly.Error())
|
||||
case errors.Is(err, team_service.ErrReceiptTeamCancelled):
|
||||
return status.Error(codes.FailedPrecondition, team_service.ErrReceiptTeamCancelled.Error())
|
||||
case errors.Is(err, team_service.ErrInvalidReceipt):
|
||||
return status.Error(codes.FailedPrecondition, team_service.ErrInvalidReceipt.Error())
|
||||
case errors.Is(err, context.Canceled):
|
||||
return status.Error(codes.Canceled, "печать прервана; проверьте, вышел ли чек, перед повторной печатью")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return status.Error(codes.DeadlineExceeded, "принтер не ответил вовремя; проверьте, вышел ли чек, перед повторной печатью")
|
||||
}
|
||||
code := team_service.ReceiptPrinterErrorCode(err)
|
||||
known := team_service.ReceiptPrinterError(code)
|
||||
if errors.Is(err, known) {
|
||||
grpcCode := codes.FailedPrecondition
|
||||
if code == "busy" {
|
||||
grpcCode = codes.ResourceExhausted
|
||||
}
|
||||
if code == "unavailable" || code == "protocol" || code == "incomplete" {
|
||||
grpcCode = codes.Unavailable
|
||||
}
|
||||
return status.Error(grpcCode, known.Error())
|
||||
}
|
||||
log.Printf("team receipt: %v", err)
|
||||
return status.Error(codes.Internal, "не удалось напечатать чек; проверьте, вышел ли он, перед повторной печатью")
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user