This commit is contained in:
2026-10-23 00:45:30 +07:00
parent 60119a07e8
commit 146700cf4c
34 changed files with 622 additions and 124 deletions
@@ -34,7 +34,8 @@ func TestAnswerAccessPostgres(t *testing.T) {
format TEXT, status TEXT, scenario_id INT, started_at TIMESTAMP, ended_at TIMESTAMP);
CREATE TEMP TABLE teams (id UUID, name TEXT, captain_email TEXT, participants_count INT,
status TEXT, password TEXT, game_id INT);
INSERT INTO games VALUES (1, 'Game', '', NULL, NOW(), 'online', 'finish', 1, NULL, NULL);
ALTER TABLE games ADD COLUMN wifi_name TEXT NOT NULL DEFAULT '', ADD COLUMN wifi_password TEXT NOT NULL DEFAULT '';
INSERT INTO games (id, name, description, location, start_at, format, status, scenario_id, started_at, ended_at) VALUES (1, 'Game', '', NULL, NOW(), 'online', 'finish', 1, NULL, NULL);
INSERT INTO teams VALUES ('019a1234-5678-7abc-8def-000000000001', 'Team', '', 1, 'register', 'secret', 1);
ALTER TABLE teams ADD COLUMN participation_status TEXT DEFAULT 'pending', ADD COLUMN participation_source TEXT DEFAULT '',
ADD COLUMN participation_email_error TEXT DEFAULT '', ADD COLUMN participation_sent_at TIMESTAMPTZ;`)
@@ -46,7 +46,8 @@ func TestExportAnyStatusProducesFreshSetup(t *testing.T) {
t.Run(format+"/"+state, func(t *testing.T) {
ctx := context.Background()
f := newServiceFixture(t)
check(t, f.games.UpdateGame(ctx, f.gameID, "Test game", "Description", nil, time.Now(), int32(f.scenarioID), format))
wifiName, wifiPassword := "Детектив", " secret "
check(t, f.games.UpdateGame(ctx, f.gameID, "Test game", "Description", nil, time.Now(), int32(f.scenarioID), format, &wifiName, &wifiPassword))
if state != "draft" {
check(t, f.games.StartGame(ctx, f.gameID))
if state == "pause" {
@@ -102,7 +103,7 @@ func TestExportAnyStatusProducesFreshSetup(t *testing.T) {
check(t, err)
local, err := venue.games.GetGameByID(ctx, imported.GameID)
check(t, err)
if local.Status != "draft" || local.StartedAt != nil || local.EndedAt != nil {
if local.Status != "draft" || local.StartedAt != nil || local.EndedAt != nil || local.WiFiName != wifiName || local.WiFiPassword != wifiPassword {
t.Fatalf("import did not start fresh: %+v", local)
}
actions, err = actions_repo.NewActionsRepo(venue.pool).GetActionsByTeamID(ctx, f.teamID)
@@ -42,7 +42,7 @@ func newReturnFixture(t *testing.T, format string) returnFixture {
t.Helper()
ctx := context.Background()
f := newServiceFixture(t)
check(t, f.games.UpdateGame(ctx, f.gameID, "Test game", "Description", nil, time.Now().Add(time.Hour), int32(f.scenarioID), format))
check(t, f.games.UpdateGame(ctx, f.gameID, "Test game", "Description", nil, time.Now().Add(time.Hour), int32(f.scenarioID), format, nil, nil))
check(t, f.scenarios.UpdateStoryByScenarioID(ctx, f.scenarioID, `{"places":[{"code":"p","name":"Place","text":"Text","applications":[{"name":"Clue","image":"original.png"}]}],"questions":[{"code":"q","text":"Who?","answer":"Butler"},{"code":"q2","text":"Why?","answer":"Money"}]}`))
_, err := f.teams.AddTeam(ctx, f.userID, f.gameID, "Removed", "removed-qr")
check(t, err)
@@ -125,7 +125,7 @@ func TestReturnAtomicLifecycle(t *testing.T) {
mapRepo := game_map_repo.NewGameMapRepo(f.pool)
check(t, mapRepo.Ensure(ctx, f.gameID, "team", f.data.DeletedTeamIDs[0]))
check(t, mapRepo.Ensure(ctx, f.gameID, "team", f.teamID))
check(t, f.games.UpdateGame(ctx, f.gameID, "Online title", "Online description", nil, time.Now().Add(72*time.Hour), int32(f.scenarioID), format))
check(t, f.games.UpdateGame(ctx, f.gameID, "Online title", "Online description", nil, time.Now().Add(72*time.Hour), int32(f.scenarioID), format, nil, nil))
token := hex.EncodeToString(bytes.Repeat([]byte{9}, 32))
check(t, f.teams.SetParticipationToken(ctx, f.teamID, token))
raw := f.pack(t)
@@ -454,7 +454,7 @@ func TestReturnOverwritesOnlineEditsAndIsolatesScenario(t *testing.T) {
check(t, f.scenarios.UpdateStoryByScenarioID(ctx, selected, changedStory))
other, err := f.games.AddGame(ctx, "Other game", "", nil, time.Now(), int32(selected), "offline")
check(t, err)
check(t, f.games.UpdateGame(ctx, f.gameID, "Online title", "Online description", nil, time.Now(), int32(selected), "offline"))
check(t, f.games.UpdateGame(ctx, f.gameID, "Online title", "Online description", nil, time.Now(), int32(selected), "offline", nil, nil))
check(t, f.teams.UpdateTeam(ctx, f.teamID, "Online team name"))
extraTeam, err := f.teams.AddTeam(ctx, f.userID, f.gameID, "Online extra team", "online-qr")
check(t, err)
+14 -12
View File
@@ -9,16 +9,18 @@ import (
// Game describes a game exposed by the service layer.
type Game struct {
Export *repos.GameExport
Format string
ID int
Name string
Description string
Location *string
StartAt time.Time
Status string
Scenario *scenarios_service.Scenario
Teams []*Team
StartedAt *time.Time
EndedAt *time.Time
Export *repos.GameExport
Format string
ID int
Name string
WiFiName string
WiFiPassword string
Description string
Location *string
StartAt time.Time
Status string
Scenario *scenarios_service.Scenario
Teams []*Team
StartedAt *time.Time
EndedAt *time.Time
}
+14 -12
View File
@@ -11,18 +11,20 @@ func MapGame(
domain string,
) *Game {
return &Game{
ID: o.ID,
Export: o.Export,
Name: o.Name,
Description: o.Description,
Location: o.Location,
StartAt: o.StartAt,
Status: o.Status,
Format: o.Format,
Scenario: scenarios_service.MapScenarioLight(o.Scenario, domain),
Teams: MapTeams(o.Teams),
StartedAt: o.StartedAt,
EndedAt: o.EndedAt,
ID: o.ID,
Export: o.Export,
Name: o.Name,
Description: o.Description,
WiFiName: o.WiFiName,
WiFiPassword: o.WiFiPassword,
Location: o.Location,
StartAt: o.StartAt,
Status: o.Status,
Format: o.Format,
Scenario: scenarios_service.MapScenarioLight(o.Scenario, domain),
Teams: MapTeams(o.Teams),
StartedAt: o.StartedAt,
EndedAt: o.EndedAt,
}
}
+13 -11
View File
@@ -51,17 +51,19 @@ type Snapshot struct {
}
type Game struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Location *string `json:"location,omitempty"`
StartAt time.Time `json:"start_at"`
Status string `json:"status"`
Format string `json:"format"`
ScenarioID int `json:"scenario_id"`
PlaylistID *int `json:"playlist_id,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
EndedAt *time.Time `json:"ended_at,omitempty"`
WiFiName string `json:"wifi_name,omitempty"`
WiFiPassword string `json:"wifi_password,omitempty"`
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Location *string `json:"location,omitempty"`
StartAt time.Time `json:"start_at"`
Status string `json:"status"`
Format string `json:"format"`
ScenarioID int `json:"scenario_id"`
PlaylistID *int `json:"playlist_id,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
EndedAt *time.Time `json:"ended_at,omitempty"`
}
type Scenario struct {
@@ -90,6 +90,10 @@ func TestPackagePreservesPregameData(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
snapshot, files := fixture()
snapshot.Data.Game.Format = tc.format
if tc.format == "offline" {
snapshot.Data.Game.WiFiName = "Детектив"
snapshot.Data.Game.WiFiPassword = " secret "
}
snapshot.Data.Map = append(snapshot.Data.Map, MapItem{GameID: 10, Kind: "screen", ItemID: "019a1234-5678-7abc-8def-000000000063"})
before, _ := json.Marshal(snapshot)
calls := map[string]int{}
@@ -9,6 +9,8 @@ import (
"strings"
"unicode"
"evening_detective_server/internal/modules/team_qr_pdf"
"github.com/google/uuid"
)
@@ -18,6 +20,9 @@ func invalid(reason string) error {
func validateData(data *Data) error {
g := data.Game
if err := team_qr_pdf.ValidateWiFi(g.WiFiName, g.WiFiPassword); err != nil {
return invalid("некорректные параметры Wi-Fi")
}
if g.ID <= 0 || strings.TrimSpace(g.Name) == "" || g.StartAt.IsZero() ||
g.Status != "draft" || g.StartedAt != nil || g.EndedAt != nil ||
(g.Format != "online" && g.Format != "offline") {
@@ -64,7 +64,7 @@ func TestProdFlowReturns(t *testing.T) {
t.Fatal(err)
}
games := games_repo.NewGamesRepo(pool)
if err := games.UpdateGame(ctx, gameID, "TEST Online title after export", "TEST Online description", nil, time.Now().Add(72*time.Hour), int32(original.Game.ScenarioID), original.Game.Format); err != nil {
if err := games.UpdateGame(ctx, gameID, "TEST Online title after export", "TEST Online description", nil, time.Now().Add(72*time.Hour), int32(original.Game.ScenarioID), original.Game.Format, nil, nil); err != nil {
t.Fatal(err)
}
} else if err != nil {
@@ -58,7 +58,7 @@ func TestServicesLifecyclePostgres(t *testing.T) {
CREATE TEMP TABLE team_answers(team_id UUID, question_code TEXT, answer TEXT, score INT, updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(team_id, question_code));
INSERT INTO scenarios(id,name,scenario) VALUES(1,'Case','{"introduction":{"text":"Intro"},"places":[{"code":"p","name":"Place","applications":[{"name":"Clue","image":"clue.pdf"}]}],"questions":[{"code":"q","text":"Who?","answer":"Butler"}]}');`)
check(err)
for _, name := range []string{"20260913130000_add_team_registration.sql", "20260915120000_add_team_participation.sql"} {
for _, name := range []string{"20260913130000_add_team_registration.sql", "20260915120000_add_team_participation.sql", "20261023120000_game_wifi.sql"} {
data, err := os.ReadFile("../../../migrations/" + name)
check(err)
_, err = pool.Exec(ctx, strings.Split(string(data), "-- +goose Down")[0])
@@ -77,7 +77,23 @@ func TestServicesLifecyclePostgres(t *testing.T) {
start := time.Now().Add(48 * time.Hour)
id, err := game.AddGame(ctx, "Game", "", nil, start, 1, "online")
check(err)
check(game.UpdateGame(ctx, id, "Game", "Updated", nil, start, 1, ""))
check(game.UpdateGame(ctx, id, "Game", "Updated", nil, start, 1, "", nil, nil))
wifiName, wifiPassword := "Детектив", " secret "
check(game.UpdateGame(ctx, id, "Game", "Updated", nil, start, 1, "online", &wifiName, &wifiPassword))
check(game.UpdateGame(ctx, id, "Game", "Updated", nil, start, 1, "online", nil, nil))
stored, err := game.GetGame(ctx, id)
check(err)
if stored.WiFiName != wifiName || stored.WiFiPassword != wifiPassword {
t.Fatal("old client erased Wi-Fi settings")
}
empty := ""
check(game.UpdateGame(ctx, id, "Game", "Updated", nil, start, 1, "online", &empty, &empty))
stored, err = game.GetGame(ctx, id)
check(err)
if stored.WiFiName != "" || stored.WiFiPassword != "" {
t.Fatal("Wi-Fi settings not cleared")
}
check(game.UpdateGame(ctx, id, "Game", "Updated", nil, start, 1, "online", &wifiName, &wifiPassword))
registration := team_service.Registration{Name: "Team", Email: "captain@example.org", ParticipantsCount: 3}
teamID, err := team.RegisterTeam(ctx, id, "delivery", registration)
check(err)
+15 -2
View File
@@ -3,6 +3,9 @@ package game_service
import (
"context"
"errors"
"time"
"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"
@@ -10,7 +13,6 @@ import (
"evening_detective_server/internal/repos/teams_repo"
"evening_detective_server/internal/services/game_model"
"evening_detective_server/internal/services/scenarios_service"
"time"
)
// GameService handles game lifecycle operations.
@@ -174,7 +176,18 @@ func (s *GameService) UpdateGame(
startAt time.Time,
scenarioID int32,
format string,
wifiName, wifiPassword *string,
) error {
nameValue, passwordValue := "", ""
if wifiName != nil {
nameValue = *wifiName
}
if wifiPassword != nil {
passwordValue = *wifiPassword
}
if err := team_qr_pdf.ValidateWiFi(nameValue, passwordValue); err != nil {
return err
}
if format == "" {
game, err := s.gamesRepo.GetGameByID(ctx, id)
if err != nil {
@@ -185,7 +198,7 @@ func (s *GameService) UpdateGame(
if format != "online" && format != "offline" {
return ErrInvalidGameFormat
}
return mapGameError(s.gamesRepo.UpdateGame(ctx, id, name, description, location, startAt, scenarioID, format))
return mapGameError(s.gamesRepo.UpdateGame(ctx, id, name, description, location, startAt, scenarioID, format, wifiName, wifiPassword))
}
// StartGame marks a game as started.
@@ -0,0 +1,24 @@
package game_service
import (
"context"
"errors"
"strings"
"testing"
"time"
"evening_detective_server/internal/modules/team_qr_pdf"
)
func TestUpdateGameRejectsInvalidWiFiBeforeWriting(t *testing.T) {
s := &GameService{}
for _, tc := range []struct{ name, password string }{
{name: strings.Repeat("x", 33)},
{password: "bad\n"},
} {
err := s.UpdateGame(context.Background(), 1, "Game", "", nil, time.Now(), 1, "online", &tc.name, &tc.password)
if !errors.Is(err, team_qr_pdf.ErrInvalidWiFi) {
t.Fatal("invalid settings reached repository")
}
}
}
@@ -37,7 +37,8 @@ func TestFinishedTeamStoryPostgres(t *testing.T) {
format TEXT, status TEXT, scenario_id INT, started_at TIMESTAMP, ended_at TIMESTAMP);
CREATE TEMP TABLE teams (id UUID, name TEXT, captain_email TEXT, participants_count INT,
status TEXT, password TEXT, game_id INT);
INSERT INTO games VALUES (1, 'Game', '', NULL, NOW(), 'online', 'finish', 1, NULL, NULL);
ALTER TABLE games ADD COLUMN wifi_name TEXT NOT NULL DEFAULT '', ADD COLUMN wifi_password TEXT NOT NULL DEFAULT '';
INSERT INTO games (id, name, description, location, start_at, format, status, scenario_id, started_at, ended_at) VALUES (1, 'Game', '', NULL, NOW(), 'online', 'finish', 1, NULL, NULL);
INSERT INTO teams VALUES ('019a1234-5678-7abc-8def-000000000001', 'Team', '', 1, 'register', 'secret', 1);
ALTER TABLE teams ADD COLUMN participation_status TEXT DEFAULT 'pending', ADD COLUMN participation_source TEXT DEFAULT '',
ADD COLUMN participation_email_error TEXT DEFAULT '', ADD COLUMN participation_sent_at TIMESTAMPTZ;`)
+64 -5
View File
@@ -103,6 +103,9 @@ func (s *TeamService) teamReceipt(ctx context.Context, gameID int, teamID string
}
func renderReceipt(card team_qr_pdf.Card) (*image.Gray, error) {
if err := team_qr_pdf.ValidateWiFi(card.WiFiName, card.WiFiPassword); err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidReceipt, err)
}
name := strings.Join(strings.Fields(card.Name), " ")
if name == "" || len([]rune(name)) > 255 {
return nil, ErrInvalidReceipt
@@ -111,17 +114,34 @@ func renderReceipt(card team_qr_pdf.Card) (*image.Gray, error) {
if err != nil {
return nil, fmt.Errorf("parse receipt font: %w", err)
}
face, err := opentype.NewFace(ttf, &opentype.FaceOptions{Size: 26, DPI: 72, Hinting: font.HintingFull})
nameSize, nameLineHeight := 26.0, 34
if card.WiFiName != "" {
nameSize, nameLineHeight = 20, 26
}
face, err := opentype.NewFace(ttf, &opentype.FaceOptions{Size: nameSize, DPI: 72, Hinting: font.HintingFull})
if err != nil {
return nil, fmt.Errorf("open receipt font: %w", err)
}
defer face.Close()
defer func() {
if face != nil {
_ = face.Close()
}
}()
for _, r := range name {
if _, ok := face.GlyphAdvance(r); !ok {
return nil, ErrInvalidReceipt
}
}
lines := wrapReceiptName(name, face)
instructionFace, err := opentype.NewFace(ttf, &opentype.FaceOptions{Size: 20, DPI: 72, Hinting: font.HintingFull})
if err != nil {
return nil, fmt.Errorf("open Wi-Fi receipt font: %w", err)
}
defer instructionFace.Close()
var instructions []string
for _, paragraph := range team_qr_pdf.WiFiInstructions(card.WiFiName, card.WiFiPassword) {
instructions = append(instructions, wrapReceiptText(paragraph, instructionFace)...)
}
qr, err := qrcode.New(card.URL, qrcode.Medium)
if err != nil {
return nil, ErrInvalidReceipt
@@ -132,8 +152,20 @@ func renderReceipt(card team_qr_pdf.Card) (*image.Gray, error) {
return nil, ErrInvalidReceipt
}
qrSize := len(bitmap) * scale
qrY := 16 + len(lines)*34 + 16
height := qrY + qrSize + 24
qrY := 16 + len(lines)*nameLineHeight + 16
height := qrY + qrSize + 24 + len(instructions)*26
for card.WiFiName != "" && height > receiptMaxHeight && nameSize > 16 {
_ = face.Close()
nameSize -= 2
nameLineHeight = int(nameSize) + 6
face, err = opentype.NewFace(ttf, &opentype.FaceOptions{Size: nameSize, DPI: 72, Hinting: font.HintingFull})
if err != nil {
return nil, fmt.Errorf("resize receipt font: %w", err)
}
lines = wrapReceiptName(name, face)
qrY = 16 + len(lines)*nameLineHeight + 16
height = qrY + qrSize + 24 + len(instructions)*26
}
if height > receiptMaxHeight {
return nil, ErrInvalidReceipt
}
@@ -142,7 +174,7 @@ func renderReceipt(card team_qr_pdf.Card) (*image.Gray, error) {
drawer := font.Drawer{Dst: img, Src: image.Black, Face: face}
for i, line := range lines {
width := drawer.MeasureString(line).Ceil()
drawer.Dot = fixed.P((receiptWidth-width)/2, 16+face.Metrics().Ascent.Ceil()+i*34)
drawer.Dot = fixed.P((receiptWidth-width)/2, 16+face.Metrics().Ascent.Ceil()+i*nameLineHeight)
drawer.DrawString(line)
}
x := (receiptWidth - qrSize) / 2
@@ -153,6 +185,11 @@ func renderReceipt(card team_qr_pdf.Card) (*image.Gray, error) {
}
}
}
drawer.Face = instructionFace
for i, line := range instructions {
drawer.Dot = fixed.P(16, qrY+qrSize+instructionFace.Metrics().Ascent.Ceil()+i*26)
drawer.DrawString(line)
}
for i, pixel := range img.Pix {
if pixel < 128 {
img.Pix[i] = 0
@@ -211,3 +248,25 @@ func validateReceiptRaster(data []byte) error {
}
return nil
}
// Preserve spaces in credentials when wrapping the printed instructions.
func wrapReceiptText(text string, face font.Face) []string {
var lines []string
line := ""
for _, r := range text {
for line != "" && font.MeasureString(face, line+string(r)).Ceil() > receiptWidth-32 {
if space := strings.LastIndexByte(line, ' '); space >= 0 {
lines = append(lines, line[:space+1])
line = line[space+1:]
} else {
lines = append(lines, line)
line = ""
}
}
line += string(r)
}
if line != "" {
lines = append(lines, line)
}
return lines
}
+53 -4
View File
@@ -14,18 +14,21 @@ import (
"evening_detective_server/internal/repos"
"evening_detective_server/internal/repos/games_repo"
"evening_detective_server/internal/repos/teams_repo"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
)
const receiptTestTeamID = "019a1234-5678-7abc-8def-000000000007"
type receiptReposStub struct {
ITeamsRepository
team *repos.Team
gameErr, teamErr error
team *repos.Team
gameErr, teamErr error
wifiName, wifiPassword string
}
func (s *receiptReposStub) GetGameByID(context.Context, int) (*repos.Game, error) {
return &repos.Game{ID: 1, Name: "Проверка чеков"}, s.gameErr
return &repos.Game{ID: 1, Name: "Проверка чеков", WiFiName: s.wifiName, WiFiPassword: s.wifiPassword}, s.gameErr
}
func (s *receiptReposStub) GetTeamByID(context.Context, string) (*repos.Team, error) {
@@ -81,7 +84,7 @@ func TestTeamReceiptValidation(t *testing.T) {
func TestTeamReceiptPreviewMatchesPrintedRaster(t *testing.T) {
for _, name := range []string{"Вечерние сыщики", "Очень длинное название команды БезПробеловОченьДлинноеНазваниеКоманды"} {
t.Run(name, func(t *testing.T) {
repos := &receiptReposStub{team: &repos.Team{ID: receiptTestTeamID, GameID: 1, Name: name, Password: "a+b&c?", ParticipationStatus: "confirmed"}}
repos := &receiptReposStub{wifiName: "Детектив", wifiPassword: " secret ", team: &repos.Team{ID: receiptTestTeamID, GameID: 1, Name: name, Password: "a+b&c?", ParticipationStatus: "confirmed"}}
printer := &receiptPrinterStub{}
s := NewTeamService(repos, repos, nil, "https://game.test/app", nil, true, printer)
data, err := s.PreviewTeamReceipt(context.Background(), 1, receiptTestTeamID)
@@ -149,3 +152,49 @@ func TestReceiptPrinterErrorsRoundTrip(t *testing.T) {
})
}
}
func TestReceiptWiFiFitsAndPreservesCredentials(t *testing.T) {
for _, name := range []string{"Команда", strings.Repeat("Ш", 255), strings.Repeat("W", 255)} {
for _, password := range []string{"", " secret ", strings.Repeat("W", 64)} {
card := team_qr_pdf.Card{
Name: name,
URL: "https://example.org/team-story/019a1234-5678-7abc-8def-000000000007?password=abc123",
WiFiName: strings.Repeat("W", 32),
WiFiPassword: password,
}
img, err := renderReceipt(card)
if err != nil {
t.Fatalf("name runes=%d, password bytes=%d: %v", len([]rune(name)), len(password), err)
}
if img.Bounds().Dy() > receiptMaxHeight {
t.Fatal("receipt exceeds printer limit")
}
if err := validateReceiptRaster(packReceipt(img)); err != nil {
t.Fatal(err)
}
}
}
}
func TestWrapReceiptTextPreservesCredentials(t *testing.T) {
ttf, err := opentype.Parse(team_qr_pdf.FontData())
if err != nil {
t.Fatal(err)
}
face, err := opentype.NewFace(ttf, &opentype.FaceOptions{Size: 20, DPI: 72, Hinting: font.HintingFull})
if err != nil {
t.Fatal(err)
}
defer face.Close()
for _, text := range []string{" secret password ", " " + strings.Repeat("W", 64), "Пароль: «" + strings.Repeat("W", 64) + "»", "Подключитесь к Wi-Fi организатора."} {
lines := wrapReceiptText(text, face)
if strings.Join(lines, "") != text {
t.Fatal("wrapping changed credentials")
}
for _, line := range lines {
if font.MeasureString(face, line).Ceil() > receiptWidth-32 {
t.Fatal("text clipped at receipt edge")
}
}
}
}
+6 -1
View File
@@ -29,7 +29,12 @@ func teamQRData(baseURL string, game *repos.Game, teams []*repos.Team) ([]team_q
link.Path = strings.TrimRight(base.Path, "/") + "/team-story/" + team.ID
link.RawPath = ""
link.RawQuery = url.Values{"password": {team.Password}}.Encode()
cards = append(cards, team_qr_pdf.Card{Name: team.Name, URL: link.String()})
cards = append(cards, team_qr_pdf.Card{
Name: team.Name,
URL: link.String(),
WiFiName: game.WiFiName,
WiFiPassword: game.WiFiPassword,
})
}
name := strings.Map(func(r rune) rune {
if unicode.IsControl(r) || strings.ContainsRune(`\/:*?"<>|`, r) {
@@ -9,7 +9,7 @@ import (
)
func TestTeamQRData(t *testing.T) {
game := &repos.Game{Name: "Дело / №1\r\n", StartAt: time.Date(2026, 9, 8, 14, 0, 0, 0, time.UTC)}
game := &repos.Game{WiFiName: "Детектив", WiFiPassword: " secret ", Name: "Дело / №1\r\n", StartAt: time.Date(2026, 9, 8, 14, 0, 0, 0, time.UTC)}
teams := []*repos.Team{{ID: "019a1234-5678-7abc-8def-000000000007", Name: "Знатоки", Password: "a+b&c?"}}
cards, filename, err := teamQRData("https://example.org/app/", game, teams)
if err != nil {
@@ -18,6 +18,9 @@ func TestTeamQRData(t *testing.T) {
if len(cards) != 1 || cards[0].Name != "Знатоки" {
t.Fatalf("cards: %+v", cards)
}
if cards[0].WiFiName != game.WiFiName || cards[0].WiFiPassword != game.WiFiPassword {
t.Fatal("Wi-Fi settings missing from print card")
}
u, err := url.Parse(cards[0].URL)
if err != nil || u.Path != "/app/team-story/019a1234-5678-7abc-8def-000000000007" || u.Query().Get("password") != teams[0].Password {
t.Fatalf("wrong team link: %s", cards[0].URL)