Files
Ilia Denisov e9f836db87
Tests · Go / test (push) Successful in 9s
Tests · Integration / integration (push) Successful in 10s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 8s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Stage 15: dual Telegram bots & language-gated variants
Service-agnostic refinement of the owner's idea: the sign-in service returns a
set of supported game languages with the user identity, and the lobby gates the
New Game variant choice by it (en -> English; ru -> Russian + Эрудит).

- Connector hosts two bots in one container (one per service language, each its
  own token + game channel; the same telegram_id spans both). ValidateInitData
  tries each token and returns the validating bot's service_language +
  supported_languages. Per-language config (TELEGRAM_BOT_TOKEN_EN/_RU, channels).
- supported_languages rides the Session (fbs, session-scoped, not persisted); the
  UI offers only the matching variants on New Game — gating only the START of a
  new game (auto-match + friend invite), not accept/open/play; backend does not
  enforce.
- service_language persisted (accounts.service_language, migration 00010, written
  every login, last-login-wins) and routes the user-facing Notify push back
  through the right bot (push-target coalesces with preferred_language).
- Admin SendToUser/SendToGameChannel gain an operator-chosen language selector in
  the console (unrelated to ValidateInitData).
- Non-Telegram logins carry the gateway default set
  (GATEWAY_DEFAULT_SUPPORTED_LANGUAGES, all variants).

Wire (committed regen): ValidateInitDataResponse +service_language
+supported_languages; Session +supported_languages; SendToUser/SendToGameChannel
+language. Docs (ARCHITECTURE/FUNCTIONAL/_ru/READMEs) + PLAN updated; stage marked done.
2026-06-05 09:35:53 +02:00

130 lines
4.8 KiB
Go

package transcode_test
import (
"context"
"encoding/json"
"net/http"
"testing"
flatbuffers "github.com/google/flatbuffers/go"
"scrabble/gateway/internal/connector"
"scrabble/gateway/internal/transcode"
fb "scrabble/pkg/fbs/scrabblefb"
)
// fakeValidator stands in for the connector's ValidateInitData RPC.
type fakeValidator struct {
user connector.User
err error
}
func (f fakeValidator) ValidateInitData(context.Context, string) (connector.User, error) {
return f.user, f.err
}
func (f fakeValidator) ValidateLoginWidget(context.Context, string) (connector.User, error) {
return f.user, f.err
}
func telegramLoginPayload(initData string) []byte {
b := flatbuffers.NewBuilder(0)
off := b.CreateString(initData)
fb.TelegramLoginRequestStart(b)
fb.TelegramLoginRequestAddInitData(b, off)
b.Finish(fb.TelegramLoginRequestEnd(b))
return b.FinishedBytes()
}
func TestTelegramAuthForwardsSeedFields(t *testing.T) {
var gotBody map[string]string
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/internal/sessions/telegram" {
t.Errorf("unexpected path %q", r.URL.Path)
}
_ = json.NewDecoder(r.Body).Decode(&gotBody)
_, _ = w.Write([]byte(`{"token":"tok-tg","user_id":"u-tg","is_guest":false,"display_name":"Иван"}`))
})
defer cleanup()
v := fakeValidator{user: connector.User{ExternalID: "42", Username: "neo", FirstName: "Иван", LanguageCode: "ru", ServiceLanguage: "ru", SupportedLanguages: []string{"ru"}}}
reg := transcode.NewRegistry(backend, v)
op, ok := reg.Lookup(transcode.MsgAuthTelegram)
if !ok {
t.Fatal("auth.telegram not registered")
}
payload, err := op.Handler(context.Background(), transcode.Request{Payload: telegramLoginPayload("init")})
if err != nil {
t.Fatalf("handler: %v", err)
}
sess := fb.GetRootAsSession(payload, 0)
if string(sess.Token()) != "tok-tg" || string(sess.UserId()) != "u-tg" {
t.Fatalf("session decoded wrong: token=%q user=%q", sess.Token(), sess.UserId())
}
// The validating bot's supported languages ride the Session so the UI gates the
// New Game variant choice (here: ru -> Russian + Эрудит only).
if got := sessionLanguages(sess); len(got) != 1 || got[0] != "ru" {
t.Errorf("session supported_languages = %v, want [ru]", got)
}
// The validated launch fields are forwarded so the backend can seed a new account;
// service_language is recorded to route the account's later out-of-app push.
if gotBody["external_id"] != "42" || gotBody["language_code"] != "ru" || gotBody["first_name"] != "Иван" || gotBody["service_language"] != "ru" {
t.Errorf("forwarded body = %+v, want external_id=42 language_code=ru first_name=Иван service_language=ru", gotBody)
}
}
// sessionLanguages reads the supported_languages vector off a decoded Session.
func sessionLanguages(s *fb.Session) []string {
out := make([]string, s.SupportedLanguagesLength())
for i := range out {
out[i] = string(s.SupportedLanguages(i))
}
return out
}
// TestGuestAuthSeedsDefaultLanguages confirms a non-platform (guest) session carries
// the gateway's default supported-languages set, so the web client is ungated.
func TestGuestAuthSeedsDefaultLanguages(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"token":"tok-g","user_id":"u-g","is_guest":true,"display_name":"Guest"}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil, "en", "ru")
op, _ := reg.Lookup(transcode.MsgAuthGuest)
payload, err := op.Handler(context.Background(), transcode.Request{})
if err != nil {
t.Fatalf("handler: %v", err)
}
if got := sessionLanguages(fb.GetRootAsSession(payload, 0)); len(got) != 2 || got[0] != "en" || got[1] != "ru" {
t.Errorf("guest session supported_languages = %v, want [en ru]", got)
}
}
func TestTelegramAuthInvalidInitData(t *testing.T) {
backend, cleanup := fakeBackend(t, func(http.ResponseWriter, *http.Request) {
t.Error("backend must not be called when initData is invalid")
})
defer cleanup()
reg := transcode.NewRegistry(backend, fakeValidator{err: connector.ErrInvalidInitData})
op, _ := reg.Lookup(transcode.MsgAuthTelegram)
_, err := op.Handler(context.Background(), transcode.Request{Payload: telegramLoginPayload("bad")})
if code, ok := transcode.DomainCode(err); !ok || code != "invalid_init_data" {
t.Errorf("DomainCode = (%q, %v), want (invalid_init_data, true)", code, ok)
}
}
// TestTelegramAuthDisabledWithoutConnector confirms a nil validator leaves
// auth.telegram unregistered.
func TestTelegramAuthDisabledWithoutConnector(t *testing.T) {
backend, cleanup := fakeBackend(t, func(http.ResponseWriter, *http.Request) {})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
if _, ok := reg.Lookup(transcode.MsgAuthTelegram); ok {
t.Error("auth.telegram should be unregistered without a connector")
}
}