Files
scrabble-game/platform/telegram/internal/initdata/validator_test.go
T
Ilia Denisov cf66ed7e26
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 12s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Stage 9: Telegram integration (connector side-service, Mini App, out-of-app push)
New platform/telegram connector (own container, bot token only there):
- go-telegram/bot long-poll loop: /start deep-links + Mini App launch button.
- gRPC API pkg/proto/telegram/v1 (Telegram service): ValidateInitData, Notify
  (renders a localized message + deep-link button), SendToUser/SendToGameChannel
  (admin, wired in Stage 10). Generic methods are platform-agnostic (external_id).
- Bot API base override for Telegram's test environment; Dockerfile + compose
  (VPN sidecar, no public ingress); README.

Gateway:
- initData validation relocated from the gateway into the connector; the gateway
  calls ValidateInitData over gRPC (GATEWAY_CONNECTOR_ADDR), drops the bot token,
  and deletes internal/auth.
- Out-of-app push: runPushPump routes events whose recipient has no live in-app
  stream to connector.Notify, gated by /internal/push-target + the in-app-only
  flag (race-free de-dup); HasSubscribers added to the push hub.

Backend:
- Migration 00007 accounts.notifications_in_app_only (default true) + jetgen.
- ProvisionTelegram seeds a new account's language/display name from the launch
  fields; IdentityExternalID reverse lookup; /internal/push-target handler.

UI:
- Telegram Mini App launch: detect initData, apply themeParams, authTelegram,
  route the deep-link start_param (g/i/f); /telegram/ guard redirects outside
  Telegram. Vite relative base + telegram-web-app.js. In-app-only profile toggle;
  share-to-Telegram link for a friend code. Vitest + Playwright coverage.

Wire/docs/CI: fbs Profile/UpdateProfileRequest gain notifications_in_app_only
(Go + TS); go.work uses ./platform/telegram; go-unit.yaml covers it; PLAN,
ARCHITECTURE, FUNCTIONAL (+ru), UI_DESIGN, READMEs updated.
2026-06-04 07:10:21 +02:00

86 lines
2.5 KiB
Go

package initdata
import (
"encoding/hex"
"errors"
"net/url"
"sort"
"strconv"
"strings"
"testing"
"time"
)
const testToken = "123456:TESTTOKEN"
// signInitData builds a validly signed initData query string for the given token
// and decoded fields, mirroring Telegram's data-check algorithm.
func signInitData(token string, fields map[string]string) string {
keys := make([]string, 0, len(fields))
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
lines := make([]string, 0, len(keys))
for _, k := range keys {
lines = append(lines, k+"="+fields[k])
}
secret := hmacSHA256([]byte("WebAppData"), []byte(token))
mac := hmacSHA256(secret, []byte(strings.Join(lines, "\n")))
v := url.Values{}
for k, val := range fields {
v.Set(k, val)
}
v.Set("hash", hex.EncodeToString(mac))
return v.Encode()
}
func freshFields() map[string]string {
return map[string]string{
"auth_date": strconv.FormatInt(time.Now().Unix(), 10),
"user": `{"id":42,"username":"neo","first_name":"Thomas","language_code":"ru"}`,
}
}
func TestValidateOK(t *testing.T) {
initData := signInitData(testToken, freshFields())
u, err := NewHMACValidator(testToken).Validate(initData)
if err != nil {
t.Fatalf("validate: %v", err)
}
if u.ExternalID != "42" || u.Username != "neo" || u.FirstName != "Thomas" || u.LanguageCode != "ru" {
t.Errorf("user = %+v, want {42 neo Thomas ru}", u)
}
}
func TestValidateRejects(t *testing.T) {
valid := signInitData(testToken, freshFields())
t.Run("tampered hash", func(t *testing.T) {
tampered := strings.Replace(valid, "hash=", "hash=00", 1)
if _, err := NewHMACValidator(testToken).Validate(tampered); !errors.Is(err, ErrInvalidInitData) {
t.Errorf("err = %v, want ErrInvalidInitData", err)
}
})
t.Run("wrong token", func(t *testing.T) {
if _, err := NewHMACValidator("other:TOKEN").Validate(valid); !errors.Is(err, ErrInvalidInitData) {
t.Errorf("err = %v, want ErrInvalidInitData", err)
}
})
t.Run("missing hash", func(t *testing.T) {
if _, err := NewHMACValidator(testToken).Validate("user=%7B%7D&auth_date=1"); !errors.Is(err, ErrInvalidInitData) {
t.Errorf("err = %v, want ErrInvalidInitData", err)
}
})
t.Run("stale auth_date", func(t *testing.T) {
stale := signInitData(testToken, map[string]string{
"auth_date": strconv.FormatInt(time.Now().Add(-48*time.Hour).Unix(), 10),
"user": `{"id":42}`,
})
if _, err := NewHMACValidator(testToken).Validate(stale); !errors.Is(err, ErrInvalidInitData) {
t.Errorf("err = %v, want ErrInvalidInitData", err)
}
})
}