From ee15d149bd36d78ff5dbd22f0c7205b9200abd18 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Thu, 1 Oct 2026 16:42:39 +0700 Subject: [PATCH] fix --- cmd/evening_detective_server/main.go | 8 ------- docs/GO_STANDARD.md | 2 ++ docs/embed.go | 2 +- internal/app/change_password_test.go | 3 ++- internal/app/team_registration.go | 2 +- internal/modules/email_sender/interface.go | 7 ++---- internal/modules/email_sender/template.go | 2 +- internal/modules/file_storage/storage.go | 4 ++-- internal/modules/network_utils/addresses.go | 2 +- internal/modules/processor_jwt/context.go | 2 +- internal/modules/roles/subject.go | 2 -- .../modules/scenario_archive/archive_test.go | 7 +----- .../modules/scenario_archive/interface.go | 1 - internal/modules/storytelling/interface.go | 22 ------------------- internal/modules/storytelling/story.go | 1 - .../modules/string_tools/transliterate.go | 4 ---- internal/modules/team_qr_pdf/generator.go | 2 +- internal/repos/game_map_repo/repo.go | 1 - internal/repos/teams_repo/participation.go | 5 +++-- internal/repos/teams_repo/repo.go | 2 +- internal/repos/users_repo/repo.go | 2 +- .../services/answer_service/service_test.go | 2 +- .../services/game_review_service/mapper.go | 1 - .../services/game_review_service/service.go | 1 - .../services/game_service/lifecycle_test.go | 2 +- internal/services/mcp_service/service.go | 11 +--------- internal/services/mcp_service/service_test.go | 2 +- .../participation_test.go | 8 +++---- .../services/participation_service/service.go | 8 ++++--- .../users_service/change_password_test.go | 5 +++-- .../services/users_service/emails_test.go | 2 +- 31 files changed, 37 insertions(+), 88 deletions(-) diff --git a/cmd/evening_detective_server/main.go b/cmd/evening_detective_server/main.go index 1508a83..f55f133 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -81,7 +81,6 @@ const grpcMsgLimit = maxFileSize + (1 << 20) func main() { _ = godotenv.Load() - // PostgreSQL dbpool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL")) if err != nil { log.Fatalf("Unable to create connection pool: %v\n", err) @@ -164,13 +163,11 @@ func main() { go participationService.RunParticipationEmails(context.Background(), emailSender, os.Getenv("PARTICIPATION_SECRET"), os.Getenv("GAME_TIMEZONE")) go reviewService.RunReviewReminderEmails(context.Background(), emailSender) - // Create a listener on TCP port lis, err := net.Listen("tcp", ":8080") if err != nil { log.Fatalln("Failed to listen:", err) } - // Create a gRPC server object s := grpc.NewServer( grpc.MaxRecvMsgSize(grpcMsgLimit), grpc.UnaryInterceptor( @@ -205,7 +202,6 @@ func main() { ), ), ) - // Attach the Greeter service to the server proto.RegisterEveningDetectiveServerServer( s, app.NewServer( @@ -225,14 +221,11 @@ func main() { ), ) - // Serve gRPC server log.Println("Serving gRPC on 0.0.0.0:8080") go func() { log.Fatalln(s.Serve(lis)) }() - // Create a client connection to the gRPC server we just started - // This is where the gRPC-Gateway proxies the requests conn, err := grpc.NewClient( "0.0.0.0:8080", grpc.WithTransportCredentials(insecure.NewCredentials()), @@ -288,7 +281,6 @@ func main() { runtime.WithMarshalerOption("application/octet-stream", rawBody), runtime.WithMarshalerOption("application/x-zip-compressed", rawBody), ) - // Register Greeter err = proto.RegisterEveningDetectiveServerHandler(context.Background(), gwmux, conn) if err != nil { log.Fatalln("Failed to register gateway:", err) diff --git a/docs/GO_STANDARD.md b/docs/GO_STANDARD.md index 5f4d57a..2470b16 100644 --- a/docs/GO_STANDARD.md +++ b/docs/GO_STANDARD.md @@ -266,6 +266,8 @@ go test ./... ## 11. Именование и стиль +- Все комментарии в исходном Go-коде пиши на русском языке. Сгенерированные + файлы и комментарии внешних библиотек не редактируй. - Имена короткие и предметные: `gameID`, `teamID`, `ctx`, `err` допустимы по общему Go-стилю. - Аббревиатуры пишутся единообразно: `ID`, `URL`, `HTTP`, `JWT`, `SQL`. diff --git a/docs/embed.go b/docs/embed.go index 0e3b0cb..affd1a5 100644 --- a/docs/embed.go +++ b/docs/embed.go @@ -1,4 +1,4 @@ -// Package docs встраивает юридические документы в бинарник и раздаёт их +// Пакет docs встраивает юридические документы в бинарник и раздаёт их // по постоянным URL (/terms, /privacy, /consent), на которые сервис // ссылается при фиксации акцептов соглашений (user_agreements). package docs diff --git a/internal/app/change_password_test.go b/internal/app/change_password_test.go index b12d62d..a62249c 100644 --- a/internal/app/change_password_test.go +++ b/internal/app/change_password_test.go @@ -42,7 +42,8 @@ func TestChangeUserPasswordAccess(t *testing.T) { } } -// Embed the real repository so unexpected calls fail rather than silently pass. +// Встраиваем настоящий репозиторий, чтобы неожиданные вызовы завершались +// ошибкой, а не проходили незаметно. type passwordRepo struct { *users_repo.UsersRepo role string diff --git a/internal/app/team_registration.go b/internal/app/team_registration.go index 7149814..2185637 100644 --- a/internal/app/team_registration.go +++ b/internal/app/team_registration.go @@ -16,7 +16,7 @@ import ( "evening_detective_server/internal/repos/teams_repo" ) -// TeamRegistrationHandler принимает webhook Яндекс Форм отдельно от пользовательского JWT. +// TeamRegistrationHandler принимает вебхук Яндекс Форм отдельно от пользовательского JWT. func TeamRegistrationHandler(token string, register func(context.Context, int, string, team_service.Registration) (int, error)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/internal/modules/email_sender/interface.go b/internal/modules/email_sender/interface.go index 6973c4a..8fa40f4 100644 --- a/internal/modules/email_sender/interface.go +++ b/internal/modules/email_sender/interface.go @@ -5,11 +5,8 @@ import "context" type Message struct { To string Subject string - // Body — текстовая версия письма (text/plain). - Body string - // HTML — версия письма для почтовых клиентов (text/html). - // Если пусто, письмо уходит только в text/plain. - HTML string + Body string + HTML string } type IEmailSender interface { diff --git a/internal/modules/email_sender/template.go b/internal/modules/email_sender/template.go index 3932ba4..9f6da90 100644 --- a/internal/modules/email_sender/template.go +++ b/internal/modules/email_sender/template.go @@ -9,7 +9,7 @@ func ButtonHTML(link, label string) string { ` } -// WrapHTML is the shared light/dark email layout used by all service emails. +// WrapHTML возвращает общую светлую/тёмную HTML-обёртку для писем сервиса. func WrapHTML(content, baseURL string) string { fontURL := "" if baseURL != "" { diff --git a/internal/modules/file_storage/storage.go b/internal/modules/file_storage/storage.go index 61b9aa9..bd7e7ba 100644 --- a/internal/modules/file_storage/storage.go +++ b/internal/modules/file_storage/storage.go @@ -95,9 +95,9 @@ func mimeFor(f *File) string { return mimeFromData(f.Data, f.Name) } -// mimeFromData — Content-Type по содержимому (первые 512 байт) с fallback +// mimeFromData — Content-Type по содержимому (первые 512 байт) с запасным вариантом // на расширение. text/* (например, HTML) трактуется как octet-stream и -// уходит в fallback: при публичном скачивании не должно отдаваться как +// уходит в запасной вариант: при публичном скачивании не должно отдаваться как // html с доверенного домена. func mimeFromData(data []byte, filename string) string { detected := http.DetectContentType(data) diff --git a/internal/modules/network_utils/addresses.go b/internal/modules/network_utils/addresses.go index 9f99c18..3c680b6 100644 --- a/internal/modules/network_utils/addresses.go +++ b/internal/modules/network_utils/addresses.go @@ -2,7 +2,7 @@ package network_utils import "net" -// GetLocalIPv4Addresses returns non-loopback IPv4 addresses of the host. +// GetLocalIPv4Addresses возвращает IPv4-адреса хоста, кроме loopback-адресов. func GetLocalIPv4Addresses() ([]string, error) { addrs, err := net.InterfaceAddrs() if err != nil { diff --git a/internal/modules/processor_jwt/context.go b/internal/modules/processor_jwt/context.go index f588e0e..280e511 100644 --- a/internal/modules/processor_jwt/context.go +++ b/internal/modules/processor_jwt/context.go @@ -9,7 +9,7 @@ func ContextWithClaims(ctx context.Context, claims *JWTClaims) context.Context { return context.WithValue(ctx, claimsContextKey{}, claims) } -// ClaimsFromContext returns authenticated claims stored in ctx. +// ClaimsFromContext возвращает данные аутентификации, сохранённые в ctx. func ClaimsFromContext(ctx context.Context) (*JWTClaims, bool) { claims, ok := ctx.Value(claimsContextKey{}).(*JWTClaims) return claims, ok && claims != nil diff --git a/internal/modules/roles/subject.go b/internal/modules/roles/subject.go index ac1c113..d95fb9c 100644 --- a/internal/modules/roles/subject.go +++ b/internal/modules/roles/subject.go @@ -7,8 +7,6 @@ type subject struct { roles []string } -// NewSubject создаёт субъект с заданными ролями (например, для тестов -// авторизации или in-memory проверок прав). func NewSubject(roles ...string) IHasRole { return &subject{roles: roles} } diff --git a/internal/modules/scenario_archive/archive_test.go b/internal/modules/scenario_archive/archive_test.go index 4781019..9d971fc 100644 --- a/internal/modules/scenario_archive/archive_test.go +++ b/internal/modules/scenario_archive/archive_test.go @@ -16,10 +16,8 @@ import ( const testDomain = "http://storage.test/api/files/" -// testArchive — экземпляр модуля для тестов. var testArchive = NewScenarioArchive() -// buildScenario собирает сценарий с историей и ссылками на изображения. func buildScenario() *repos.Scenario { description := "Детективная история" image := "cover.png" @@ -35,7 +33,6 @@ func buildScenario() *repos.Scenario { } } -// memoryFiles — замена хранилища для Pack: имя → содержимое. type memoryFiles map[string][]byte func (m memoryFiles) get(_ context.Context, name string) ([]byte, error) { @@ -183,7 +180,7 @@ func TestPackExternalURLsKeptAndNotFetched(t *testing.T) { } func TestPackLegacyFullURLImage(t *testing.T) { - // Legacy-данные: полный URL хранилища срезается до базового имени файла. + // Устаревшие данные: полный URL хранилища срезается до базового имени файла. description := "desc" scenario := &repos.Scenario{ ID: 1, @@ -258,7 +255,6 @@ func TestPackImageNameCollision(t *testing.T) { } } -// writeZip собирает архив с заданными записями (для негативных тестов Unpack). func writeZip(t *testing.T, entries map[string][]byte) []byte { t.Helper() var buf bytes.Buffer @@ -278,7 +274,6 @@ func writeZip(t *testing.T, entries map[string][]byte) []byte { return buf.Bytes() } -// validScenarioJSON — минимальный корректный scenario.json. func validScenarioJSON() []byte { b, _ := json.Marshal(ScenarioJSON{ Version: Version, diff --git a/internal/modules/scenario_archive/interface.go b/internal/modules/scenario_archive/interface.go index ecde2e8..1509de6 100644 --- a/internal/modules/scenario_archive/interface.go +++ b/internal/modules/scenario_archive/interface.go @@ -23,7 +23,6 @@ type ScenarioJSON struct { Story *storytelling.Story `json:"story"` } -// Bundle — результат разбора входящего архива. type Bundle struct { Scenario *ScenarioJSON Files map[string][]byte diff --git a/internal/modules/storytelling/interface.go b/internal/modules/storytelling/interface.go index 60a33a6..4da0bf0 100644 --- a/internal/modules/storytelling/interface.go +++ b/internal/modules/storytelling/interface.go @@ -8,13 +8,11 @@ type IStory interface { type Story struct { Background string `json:"background,omitempty"` - // Введение - то, что игрок получает в начале игры Introduction *Introduction `json:"introduction,omitempty"` // Закрывашка доступна в редакторе сценария, игрокам не передаётся. Conclusion *Conclusion `json:"conclusion,omitempty"` - // Список точек Places []*Place `json:"places"` // Вопросы дела - список вопросов, на которые команда должна ответить @@ -24,11 +22,8 @@ type Story struct { // Вопрос дела - вопрос, который автор сценария добавляет вместе с // правильным ответом; команда отвечает на вопросы в конце игры type Question struct { - - // Код - идентификатор вопроса, используется при отправке ответов Code string `json:"code"` - // Текст вопроса Text string `json:"text"` // Правильный ответ - виден только автору и организаторам, @@ -44,11 +39,8 @@ type Conclusion struct { // Введение - текст и аудио, которые игрок получает в начале игры. type Introduction struct { - - // Текст введения Text string `json:"text"` - // Аудио введения Audio string `json:"audio,omitempty"` } @@ -57,23 +49,17 @@ type Introduction struct { // Это может быть персонаж - пострадавший, продавец, полицейский... // Это может быть описание действия игрока - ответ на вопрос, реакция на выбитую дверь... type Place struct { - - // Код - то что вбивает игрок Code string `json:"code"` - // Название места, имя персонажа, заданный игроком вопрос... Name string `json:"name"` - // Текст - получаемая текстовая информация Text string `json:"text"` - // Картинка - получаемая визуальная информация Image string `json:"image"` // Признак скрытой карточки - некоторые места изначально не доступны Hidden bool `json:"hidden"` - // Приложения - получаемые улики Applications []*Application `json:"applications,omitempty"` // Двери к другим точкам - действия или диалоги, открывающие другие точки @@ -86,11 +72,8 @@ type Place struct { // Приложение - улика // Это может быть оружие, отпечатки, фотография... type Application struct { - - // Название улики Name string `json:"name"` - // Картинка Image string `json:"image"` // Тип файла улики: image | pdf | audio. @@ -99,11 +82,8 @@ type Application struct { // Дверь - действие или диалог type Door struct { - - // Код Code string `json:"code"` - // Название - заданный вопрос, проделанное действие... Name string `json:"name"` // Некоторые двери изначально не доступны @@ -114,7 +94,5 @@ type Door struct { // Аналог карт допуска в "Тайны Аркхема" type Key struct { - - // Название Name string `json:"name"` } diff --git a/internal/modules/storytelling/story.go b/internal/modules/storytelling/story.go index 42b3fad..1caa66e 100644 --- a/internal/modules/storytelling/story.go +++ b/internal/modules/storytelling/story.go @@ -67,7 +67,6 @@ func (s *story) GetStory( } } } - // Собираем ключи if currentPlace != nil { for _, key := range currentPlace.Keys { keys[key.Name] = struct{}{} diff --git a/internal/modules/string_tools/transliterate.go b/internal/modules/string_tools/transliterate.go index 6dfe414..d607c3c 100644 --- a/internal/modules/string_tools/transliterate.go +++ b/internal/modules/string_tools/transliterate.go @@ -48,18 +48,14 @@ var ( // заменяет пробелы на _, удаляет все остальные символы, кроме цифр (знаки препинания и т.д.) func Transliterate(text string) string { - // Приводим к нижнему регистру и транслитерируем result := replacer.Replace(strings.ToLower(text)) - // Удаляем все символы, кроме букв латиницы, подчеркивания и цифр reg := regexp.MustCompile(`[^a-z0-9_]`) result = reg.ReplaceAllString(result, "") - // Удаляем множественные подчеркивания (опционально) regMulti := regexp.MustCompile(`_+`) result = regMulti.ReplaceAllString(result, "_") - // Удаляем подчеркивания в начале и конце строки (опционально) result = strings.Trim(result, "_") return result diff --git a/internal/modules/team_qr_pdf/generator.go b/internal/modules/team_qr_pdf/generator.go index dee04c2..a28019e 100644 --- a/internal/modules/team_qr_pdf/generator.go +++ b/internal/modules/team_qr_pdf/generator.go @@ -57,7 +57,7 @@ func Generate(cards []Card) ([]byte, error) { } x := margin + float64(i%3)*width y := margin + float64(i%12/3)*height - // Bitmap включает свободное поле QR; векторные квадраты сохраняют + // Растровый режим включает свободное поле QR; векторные квадраты сохраняют // чёткость при печати без растрового масштабирования. bitmap := qr.Bitmap() pixel := qrSize / float64(len(bitmap)) diff --git a/internal/repos/game_map_repo/repo.go b/internal/repos/game_map_repo/repo.go index 1d95c37..7e7e5a3 100644 --- a/internal/repos/game_map_repo/repo.go +++ b/internal/repos/game_map_repo/repo.go @@ -25,7 +25,6 @@ type Repo struct { pool *pgxpool.Pool } -// NewGameMapRepo создаёт repository объектов карты. func NewGameMapRepo(pool *pgxpool.Pool) *Repo { return &Repo{pool: pool} } diff --git a/internal/repos/teams_repo/participation.go b/internal/repos/teams_repo/participation.go index f7fed82..de8f95f 100644 --- a/internal/repos/teams_repo/participation.go +++ b/internal/repos/teams_repo/participation.go @@ -76,7 +76,7 @@ func (r *TeamsRepo) AnswerParticipation(ctx context.Context, token, answer strin if err != nil { return nil, err } - // The condition is rechecked after a concurrent answer releases its row lock. + // Условие проверяется повторно после снятия блокировки строки конкурентным ответом. _, err = r.pool.Exec(ctx, `WITH eligible_game AS (SELECT g.id FROM games g JOIN teams t ON t.game_id=g.id WHERE t.participation_token_hash=$1 AND g.status='draft' AND g.start_at > (clock_timestamp() AT TIME ZONE 'UTC') FOR SHARE OF g) UPDATE teams t SET participation_status=$2, @@ -99,7 +99,8 @@ func (r *TeamsRepo) AnswerParticipation(ctx context.Context, token, answer strin return p, ErrParticipationClosed } -// A five-minute lease survives restarts and keeps concurrent workers from sending the same job. +// Пятиминутная аренда переживает перезапуски и не позволяет рабочим процессам +// отправить одну задачу повторно. func (r *TeamsRepo) ClaimParticipationEmail(ctx context.Context) (*Participation, error) { p := &Participation{} err := r.pool.QueryRow(ctx, `WITH candidate AS ( diff --git a/internal/repos/teams_repo/repo.go b/internal/repos/teams_repo/repo.go index d2be9e2..cb4a29c 100644 --- a/internal/repos/teams_repo/repo.go +++ b/internal/repos/teams_repo/repo.go @@ -212,7 +212,7 @@ func (r *TeamsRepo) RegisterTeamWithParticipation(ctx context.Context, gameID in return id, err } -// GetActiveTeamsByGameID excludes cancelled registrations from gameplay exports and results. +// GetActiveTeamsByGameID исключает отменённые регистрации из игровых выгрузок и результатов. func (r *TeamsRepo) GetActiveTeamsByGameID(ctx context.Context, gameID int) ([]*repos.Team, error) { teams, err := r.GetTeamsByGameID(ctx, gameID) active := teams[:0] diff --git a/internal/repos/users_repo/repo.go b/internal/repos/users_repo/repo.go index 759a272..1ba7e1b 100644 --- a/internal/repos/users_repo/repo.go +++ b/internal/repos/users_repo/repo.go @@ -77,7 +77,7 @@ func (s *UsersRepo) AddUserWithAgreements( // создать пользователя с тем же username/email — нарушение // уникальности. Возвращаем тот же понятный клиенту ответ. var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == "23505" { // unique_violation + if errors.As(err, &pgErr) && pgErr.Code == "23505" { // нарушение уникальности return 0, errors.New("Username или email уже используется") } return 0, err diff --git a/internal/services/answer_service/service_test.go b/internal/services/answer_service/service_test.go index 48dbf40..3886644 100644 --- a/internal/services/answer_service/service_test.go +++ b/internal/services/answer_service/service_test.go @@ -39,7 +39,7 @@ func TestAnswerAccessPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - // Rejected requests must not reach scenario or answer storage. + // Отклонённые запросы не должны доходить до хранилищ сценариев и ответов. service := NewAnswerService(games_repo.NewGamesRepo(pool), teams_repo.NewTeamsRepo(pool), nil, nil, nil) for _, tc := range []struct { status string diff --git a/internal/services/game_review_service/mapper.go b/internal/services/game_review_service/mapper.go index d64c397..d9dffaf 100644 --- a/internal/services/game_review_service/mapper.go +++ b/internal/services/game_review_service/mapper.go @@ -6,7 +6,6 @@ import ( "evening_detective_server/internal/repos/game_reviews_repo" ) -// Review is the application model returned by game review use cases. type Review struct { ID int GameID int diff --git a/internal/services/game_review_service/service.go b/internal/services/game_review_service/service.go index 9268b12..f500cfd 100644 --- a/internal/services/game_review_service/service.go +++ b/internal/services/game_review_service/service.go @@ -22,7 +22,6 @@ var ( ErrInvalidReview = errors.New("Оценка должна быть от 1 до 5, текст обязателен") ) -// GameReviewService handles game review use cases and reminder emails. type GameReviewService struct { reviews *game_reviews_repo.Repo games *games_repo.GamesRepo diff --git a/internal/services/game_service/lifecycle_test.go b/internal/services/game_service/lifecycle_test.go index dda0527..1714a8e 100644 --- a/internal/services/game_service/lifecycle_test.go +++ b/internal/services/game_service/lifecycle_test.go @@ -40,7 +40,7 @@ func TestServicesLifecyclePostgres(t *testing.T) { } config, err := pgxpool.ParseConfig(dsn) check(err) - config.MaxConns = 1 // Temporary tables stay on the same connection. + config.MaxConns = 1 // Временные таблицы должны оставаться в одном соединении. pool, err := pgxpool.NewWithConfig(ctx, config) check(err) defer pool.Close() diff --git a/internal/services/mcp_service/service.go b/internal/services/mcp_service/service.go index 6f8b56d..736c525 100644 --- a/internal/services/mcp_service/service.go +++ b/internal/services/mcp_service/service.go @@ -1,4 +1,4 @@ -// Package mcp_service собирает MCP-сервер (Model Context Protocol) для +// Пакет mcp_service собирает MCP-сервер (Model Context Protocol) для // игры «Вечерний детектив»: инструменты MCP вызывают игровые сервисы // напрямую (через интерфейс GamePlayer), без HTTP-прослойки. MCP-сервер // монтируется в основной процесс как HTTP-эндпоинт /mcp (streamable HTTP) @@ -49,13 +49,10 @@ type MCPService struct { answers AnswerSubmitter } -// NewMCPService создаёт сервис поверх игровых сервисов. func NewMCPService(game GamePlayer, answers AnswerSubmitter) *MCPService { return &MCPService{game: game, answers: answers} } -// Server собирает и возвращает MCP-сервер с зарегистрированными -// инструментами. func (s *MCPService) Server() *server.MCPServer { srv := server.NewMCPServer( "evening-detective-mcp", @@ -124,7 +121,6 @@ func (s *MCPService) Server() *server.MCPServer { return srv } -// connectArgs — результат разбора ссылки на игру. type connectArgs struct { teamID int64 password string @@ -168,21 +164,17 @@ func parseConnectURL(rawURL string) (connectArgs, error) { return connectArgs{teamID: teamID, password: password}, nil } -// gameResponse — компактное описание игры для ответа инструментов -// (game_model.Game JSON-тегов не имеет). type gameResponse struct { ID int `json:"id"` Name string `json:"name"` Status string `json:"status"` } -// storyResponse — JSON-ответ инструментов: история команды + инфо об игре. type storyResponse struct { Story *storytelling.Story `json:"story"` Game *gameResponse `json:"game"` } -// storyJSON сериализует историю команды и игру в JSON. func storyJSON(story *storytelling.Story, game *game_model.Game) (string, error) { resp := storyResponse{ Story: story, @@ -284,7 +276,6 @@ func (s *MCPService) handleMakeMove(ctx context.Context, req mcp.CallToolRequest return mcp.NewToolResultText(jsonBody), nil } -// submitAnswersArgs — разобранные аргументы инструмента submit_answers. type submitAnswersArgs struct { teamID int64 password string diff --git a/internal/services/mcp_service/service_test.go b/internal/services/mcp_service/service_test.go index ee5615c..352331f 100644 --- a/internal/services/mcp_service/service_test.go +++ b/internal/services/mcp_service/service_test.go @@ -82,7 +82,7 @@ func defaultGame() *game_model.Game { } } -// mcpFixture поднимает in-process MCP-клиент над сервисом с моком +// mcpFixture поднимает встроенный MCP-клиент над сервисом с моком // GamePlayer. Каждый кейс создаёт СВЕЖИЙ сервис. type mcpFixture struct { client *client.Client diff --git a/internal/services/participation_service/participation_test.go b/internal/services/participation_service/participation_test.go index 9589c3d..2445b24 100644 --- a/internal/services/participation_service/participation_test.go +++ b/internal/services/participation_service/participation_test.go @@ -99,7 +99,7 @@ func TestParticipationPostgres(t *testing.T) { } } state(1, "pending") - state(2, "pending") // Existing teams are not changed by the migration. + state(2, "pending") // Существующие команды миграция не изменяет. // Model registration output explicitly: direct SQL inserts do not auto-confirm teams. exec(`UPDATE teams SET captain_email='captain@example.org',participants_count=5 WHERE id=1; INSERT INTO teams(game_id,created_at,captain_email) SELECT 1,start_at-INTERVAL '24 hours','boundary@example.org' FROM games WHERE id=1; @@ -113,7 +113,7 @@ func TestParticipationPostgres(t *testing.T) { t.Fatal(err) } state(5, "confirmed") - // Independent workers must claim each due team just once. + // Независимые рабочие процессы должны захватывать каждую подходящую команду только один раз. var wg sync.WaitGroup ids := make(chan int, 8) errs := make(chan error, 8) @@ -158,7 +158,7 @@ func TestParticipationPostgres(t *testing.T) { t.Fatalf("failed send not recorded: %+v %v", team, err) } sender.err = nil - // Expired lease is recovered after a worker restart. + // Истёкшая аренда восстанавливается после перезапуска рабочего процесса. exec(`UPDATE teams SET participation_next_attempt_at=NOW()-INTERVAL '1 minute' WHERE id IN (1,3)`) if err := service.sendParticipationEmails(ctx, sender, "secret", time.UTC); err != nil { t.Fatal(err) @@ -209,7 +209,7 @@ func TestParticipationPostgres(t *testing.T) { if _, err := service.AnswerParticipation(ctx, token, p.Status); err != nil { t.Fatalf("same answer not idempotent: %v", err) } - // Cancellation excludes teams from shared active listings and password-based gameplay. + // Отмена исключает команды из общих активных списков и игры по паролю. token3 := participationToken("secret", 3) if _, err := service.AnswerParticipation(ctx, token3, "cancelled"); err != nil { t.Fatal(err) diff --git a/internal/services/participation_service/service.go b/internal/services/participation_service/service.go index 2b29f24..dcef1af 100644 --- a/internal/services/participation_service/service.go +++ b/internal/services/participation_service/service.go @@ -35,8 +35,10 @@ func (s *ParticipationService) AnswerParticipation(ctx context.Context, token, a return s.teamsRepo.AnswerParticipation(ctx, token, answer) } -// Stable, purpose-separated tokens keep links valid when an ambiguous SMTP failure is retried. -// Only the hash is stored in the database; the signing secret must stay stable across workers/restarts. +// Стабильные токены для разных целей сохраняют работоспособность ссылок при +// повторе после неоднозначной ошибки SMTP. +// В базе хранится только хеш; секрет подписи должен быть постоянным между +// рабочими процессами и перезапусками. func participationToken(secret string, id int) string { mac := hmac.New(sha256.New, []byte(secret)) fmt.Fprintf(mac, "team-participation:v1:%d", id) @@ -102,7 +104,7 @@ func (s *ParticipationService) sendParticipationEmails(ctx context.Context, send err = sender.Send(jobCtx, participationEmail(p, s.appBaseURL, token, location)) } cancel() - // Persist even when sending consumed the job's deadline. + // Сохраняем результат даже если отправка исчерпала время задачи. saveCtx, saveCancel := context.WithTimeout(ctx, 10*time.Second) saveErr := s.teamsRepo.FinishParticipationEmail(saveCtx, p.TeamID, p.Attempts, err) saveCancel() diff --git a/internal/services/users_service/change_password_test.go b/internal/services/users_service/change_password_test.go index 23da43c..86bd6b3 100644 --- a/internal/services/users_service/change_password_test.go +++ b/internal/services/users_service/change_password_test.go @@ -47,7 +47,8 @@ func TestChangeUserPassword(t *testing.T) { r.byID[u.ID] = u r.users[u.Email] = u } - // Nil JWT, token repository and mail sender ensure this operation does not use them. + // Пустые JWT, репозиторий токенов и отправитель писем подтверждают, + // что операция их не использует. s := &UsersService{usersRepo: r} err = s.ChangeUserPassword(context.Background(), tc.actor, tc.target, tc.password) if !errors.Is(err, tc.want) { @@ -89,7 +90,7 @@ func TestLoginAfterPasswordChangePreservesEmailCase(t *testing.T) { if err := svc.ChangeUserPassword(ctx, 1, 2, "new password 123"); err != nil { t.Fatal(err) } - // This error can only occur after email lookup and bcrypt verification succeed. + // Эта ошибка возможна только после успешного поиска email и проверки bcrypt. _, _, err := svc.Login(ctx, " User@Example.COM ", "new password 123") if err == nil || err.Error() != "Не получилось получить доступ" { t.Fatalf("lookup/password verification failed: %v", err) diff --git a/internal/services/users_service/emails_test.go b/internal/services/users_service/emails_test.go index 4948f96..e602e00 100644 --- a/internal/services/users_service/emails_test.go +++ b/internal/services/users_service/emails_test.go @@ -40,7 +40,7 @@ func TestEmailThemeAndContent(t *testing.T) { } } if n.Type == html.ElementNode && n.Data == "a" { - protected = true // The button has its own paired background and text colors. + protected = true // У кнопки собственные согласованные цвета фона и текста. } if paper && n.Type == html.TextNode && strings.TrimSpace(n.Data) != "" { if !protected {