From 0ea35fe991b821a10b3d104213f34089e586dd2f Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 5 Jun 2026 16:44:10 +0200 Subject: [PATCH] Stage 16: connector test-env via UseTestEnvironment; pin it in the test contour - bot.New now selects Telegram's test environment with the library's native tgbot.UseTestEnvironment() instead of a token += "/test" hack (functionally identical URL /bot/test/METHOD, but idiomatic) + a bot test asserting the getMe path for both test and prod. - ci.yaml pins TELEGRAM_TEST_ENV=true for the test contour (it IS the test environment) instead of a TEST_TELEGRAM_TEST_ENV variable: removes the confusing double-TEST, telegram-specific, prefixed operator knob and the secret-vs-variable footgun. Prod (Stage 17) leaves it false. - deploy/README.md + PLAN.md updated. --- .gitea/workflows/ci.yaml | 4 +++- PLAN.md | 6 +++++ deploy/README.md | 2 +- platform/telegram/internal/bot/bot.go | 13 ++++------ platform/telegram/internal/bot/bot_test.go | 28 ++++++++++++++++++++++ 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 10207a2..a71229c 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -175,7 +175,9 @@ jobs: TELEGRAM_MINIAPP_URL: ${{ vars.TEST_TELEGRAM_MINIAPP_URL }} TELEGRAM_GAME_CHANNEL_ID_EN: ${{ vars.TEST_TELEGRAM_GAME_CHANNEL_ID_EN }} TELEGRAM_GAME_CHANNEL_ID_RU: ${{ vars.TEST_TELEGRAM_GAME_CHANNEL_ID_RU }} - TELEGRAM_TEST_ENV: ${{ vars.TEST_TELEGRAM_TEST_ENV }} + # The test contour always uses Telegram's test environment — pinned here, + # not an operator variable. Stage 17's prod workflow leaves it false. + TELEGRAM_TEST_ENV: "true" VITE_TELEGRAM_BOT_ID: ${{ vars.TEST_VITE_TELEGRAM_BOT_ID }} VITE_TELEGRAM_LINK: ${{ vars.TEST_VITE_TELEGRAM_LINK }} VITE_GATEWAY_URL: ${{ vars.TEST_VITE_GATEWAY_URL }} diff --git a/PLAN.md b/PLAN.md index 353e789..d59c49e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1089,6 +1089,12 @@ provided cert) at the contour caddy; prod VPN; rollback. on the runner host. CI bootstrap nuance: the first PR introducing `ci.yaml` may first deploy on the post-merge push to `development` (depending on whether Gitea runs head/base workflows for a PR), after which PR-time deploys work. + - **Telegram test environment** (post-deploy fix): the connector now selects Telegram's test env with the + library's native `tgbot.UseTestEnvironment()` (was a `token += "/test"` hack — functionally identical, + verified, but the option is idiomatic and now has a `bot` test asserting the `/bot/test/getMe` + path). The test contour **pins `TELEGRAM_TEST_ENV=true` in `ci.yaml`** (the contour is the test + environment) rather than via a `TEST_`-prefixed variable — removing a confusing double-`TEST` operator + knob and the secret-vs-variable footgun; prod (Stage 17) leaves it `false`. ## Deferred TODOs (cross-stage) diff --git a/deploy/README.md b/deploy/README.md index 3df4c9c..ba4267e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -71,7 +71,7 @@ connector **fails at boot** if both are empty. | `GRAFANA_ADMIN_PASSWORD` | secret | `admin` | Grafana admin password. Low impact (the login form is disabled, access is anonymous-admin behind caddy) but set it anyway. | | `TELEGRAM_GAME_CHANNEL_ID_EN` | variable | _(empty)_ | English game-channel id; empty/`0` disables channel posts. | | `TELEGRAM_GAME_CHANNEL_ID_RU` | variable | _(empty)_ | Russian game-channel id; empty/`0` disables channel posts. | -| `TELEGRAM_TEST_ENV` | variable | `false` | `true` routes the bot through Telegram's test environment. | +| `TELEGRAM_TEST_ENV` | _pinned_ | `false` | `true` routes the bot through Telegram's test environment (`.../bot/test/METHOD`). **The CI test contour pins this to `true` in `ci.yaml`** (the contour is the test environment) — it is not a Gitea variable. Set it in `.env` for a local run; prod (Stage 17) leaves it `false`. | | `TELEGRAM_API_BASE_URL` | variable | _(empty)_ | Override the Bot API host (a mock/self-hosted server); empty = `https://api.telegram.org`. | | `GATEWAY_DEFAULT_SUPPORTED_LANGUAGES` | variable | `en,ru` | Variant-gating set for non-Telegram logins (web/email/guest). | | `VITE_TELEGRAM_BOT_ID` | variable | _(empty)_ | UI build-arg: numeric bot id for the web Login Widget. | diff --git a/platform/telegram/internal/bot/bot.go b/platform/telegram/internal/bot/bot.go index 1f639d3..c014520 100644 --- a/platform/telegram/internal/bot/bot.go +++ b/platform/telegram/internal/bot/bot.go @@ -43,21 +43,18 @@ func New(cfg Config, log *zap.Logger) (*Bot, error) { } t := &Bot{miniAppURL: cfg.MiniAppURL, log: log} - token := cfg.Token - if cfg.TestEnv { - // The Bot API test environment lives under /bot/test/METHOD; the - // client builds /bot/, so suffixing the token with - // "/test" injects the test segment without a custom host. - token += "/test" - } opts := []tgbot.Option{ tgbot.WithDefaultHandler(t.handleStart), tgbot.WithMessageTextHandler("/start", tgbot.MatchTypePrefix, t.handleStart), } + if cfg.TestEnv { + // Route to the Bot API test environment (.../bot/test/METHOD). + opts = append(opts, tgbot.UseTestEnvironment()) + } if cfg.APIBaseURL != "" { opts = append(opts, tgbot.WithServerURL(cfg.APIBaseURL)) } - api, err := tgbot.New(token, opts...) + api, err := tgbot.New(cfg.Token, opts...) if err != nil { return nil, err } diff --git a/platform/telegram/internal/bot/bot_test.go b/platform/telegram/internal/bot/bot_test.go index 4367b19..57b6521 100644 --- a/platform/telegram/internal/bot/bot_test.go +++ b/platform/telegram/internal/bot/bot_test.go @@ -75,6 +75,34 @@ func TestSendTextHasNoMarkup(t *testing.T) { } } +// getMePathFor captures the path bot.New's getMe call hits for the given TestEnv, +// so the test environment routing is covered (a misroute is exactly what makes a +// test-environment token fail with "getMe unauthorized"). +func getMePathFor(t *testing.T, testEnv bool) string { + t.Helper() + var path string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/getMe") { + path = r.URL.Path + } + io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"t","username":"tb"}}`) + })) + t.Cleanup(srv.Close) + if _, err := New(Config{Token: "123:ABC", APIBaseURL: srv.URL, TestEnv: testEnv, MiniAppURL: "https://example.com/"}, zap.NewNop()); err != nil { + t.Fatalf("new bot (testEnv=%v): %v", testEnv, err) + } + return path +} + +func TestTestEnvironmentRoutesGetMe(t *testing.T) { + if got, want := getMePathFor(t, true), "/bot123:ABC/test/getMe"; got != want { + t.Errorf("TestEnv getMe path = %q, want %q", got, want) + } + if got, want := getMePathFor(t, false), "/bot123:ABC/getMe"; got != want { + t.Errorf("prod getMe path = %q, want %q", got, want) + } +} + func TestStartPayload(t *testing.T) { cases := map[string]string{ "/start g123": "g123",