This commit is contained in:
2026-09-09 12:30:40 +07:00
parent 94fc85f074
commit 4feece62c8
21 changed files with 854 additions and 85 deletions
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"io"
"mime"
"net"
"net/http"
"net/http/httptest"
@@ -13,6 +14,7 @@ import (
"testing"
"evening_detective_server/internal/modules/scenario_archive"
"evening_detective_server/internal/modules/team_qr_pdf"
proto "evening_detective_server/proto"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
@@ -78,6 +80,54 @@ func (s *testStub) getUploaded() []byte {
return append([]byte(nil), s.uploaded...)
}
func (s *testStub) DownloadTeamQR(ctx context.Context, req *proto.GetGameReq) (*httpbody.HttpBody, error) {
if req.Id != 7 {
return nil, status.Error(codes.NotFound, "Игра не найдена")
}
data, err := team_qr_pdf.NewGenerator().Generate([]team_qr_pdf.Card{{Name: "Знатоки", URL: "https://example.org/team-story/1?password=test"}})
if err != nil {
return nil, err
}
if err := grpc.SetHeader(ctx, metadata.Pairs(
"Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": "2026-09-08_Дело №1.pdf"}),
"Cache-Control", "no-store",
)); err != nil {
return nil, err
}
return &httpbody.HttpBody{Data: data, ContentType: "application/pdf"}, nil
}
func TestGatewayDownloadTeamQR(t *testing.T) {
ts := newTestGateway(t, &testStub{})
resp, err := http.Get(ts.URL + "/api/games/7/teams/qr.pdf")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK || resp.Header.Get("Content-Type") != "application/pdf" || !bytes.HasPrefix(data, []byte("%PDF-")) {
t.Fatalf("response: %d, %v", resp.StatusCode, resp.Header)
}
mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
if err != nil || mediaType != "attachment" || params["filename"] != "2026-09-08_Дело №1.pdf" {
t.Fatalf("filename header: %v (%v)", resp.Header, err)
}
if resp.Header.Get("Cache-Control") != "no-store" {
t.Fatal("PDF with passwords must not be cached")
}
resp2, err := http.Get(ts.URL + "/api/games/8/teams/qr.pdf")
if err != nil {
t.Fatal(err)
}
defer resp2.Body.Close()
if resp2.StatusCode != http.StatusNotFound {
t.Fatalf("missing game: %d", resp2.StatusCode)
}
}
// newTestGateway поднимает gRPC-сервер со stub и grpc-gateway с теми же
// опциями, что и в main.go (outgoing matcher, error handler, лимиты,
// rawBodyMarshaler).
@@ -117,6 +167,8 @@ func newTestGatewayWithLimit(t *testing.T, stub proto.EveningDetectiveServerServ
return "Content-Disposition", true
case strings.EqualFold(key, "X-Content-Type-Options"):
return "X-Content-Type-Options", true
case strings.EqualFold(key, "Cache-Control"):
return "Cache-Control", true
}
return runtime.DefaultHeaderMatcher(key)
}),
+5
View File
@@ -13,6 +13,7 @@ import (
"evening_detective_server/internal/modules/scenario_archive"
"evening_detective_server/internal/modules/storytelling"
"evening_detective_server/internal/modules/string_tools"
"evening_detective_server/internal/modules/team_qr_pdf"
"evening_detective_server/internal/repos/actions_repo"
"evening_detective_server/internal/repos/applications_repo"
"evening_detective_server/internal/repos/games_repo"
@@ -139,6 +140,8 @@ func main() {
applicationsRepo,
answersRepo,
cleaner,
team_qr_pdf.NewGenerator(),
os.Getenv("APP_BASE_URL"),
)
// Create a listener on TCP port
@@ -239,6 +242,8 @@ func main() {
return "Content-Disposition", true
case strings.EqualFold(key, "X-Content-Type-Options"):
return "X-Content-Type-Options", true
case strings.EqualFold(key, "Cache-Control"):
return "Cache-Control", true
}
return runtime.DefaultHeaderMatcher(key)
}),
@@ -720,6 +720,38 @@
]
}
},
"/api/games/{id}/teams/qr.pdf": {
"get": {
"summary": "Скачать PDF с QR-кодами всех команд игры",
"operationId": "EveningDetectiveServer_DownloadTeamQR",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiHttpBody"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
}
],
"tags": [
"Игры"
]
}
},
"/api/my-scenarios": {
"get": {
"summary": "Получить свои сценарии",