65c194264c
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m0s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Mirror the Telegram Mini App wrapper for VK: the SPA loads at a new /vk/ entry, authenticates from VK's signed launch parameters, and provisions a 'vk' platform identity — the minimum to run the game in VK test mode. - Gateway verifies the launch signature in-process (internal/vkauth: HMAC-SHA256 over the sorted vk_* params under GATEWAY_VK_APP_SECRET, base64url) — a pure offline check, no side-service. New auth.vk op (gated on the secret), backendclient.VKAuth, /vk/ SPA mount. - Backend: KindVK + ProvisionVK/vkSeed, /sessions/vk handler, identity kind widened to include 'vk' (migration 00005, expand-contract). - UI: src/lib/vk.ts (VK Bridge, lazy-imported), bootVK + the /vk/ boot dispatch, encodeVKLogin + authVK across transport/client/mock. VK omits the name from the signed params, so the client reads it via VKWebAppGetUserInfo as an unsigned display seed. - Deploy: /vk in the edge Caddyfile, GATEWAY_VK_APP_SECRET wired through compose + .env.example + CI (TEST_) + prod-deploy (PROD_). - Admin console: surface the VK user id (link to the VK profile) next to the Telegram id on the user card. - Docs: ARCHITECTURE §12/§13, FUNCTIONAL (+ _ru), gateway README; VK integration reference under .claude/. Signature algorithm verified against dev.vk.com plus independent Node/Python references and a %2C edge-case vector.
130 lines
5.2 KiB
Go
130 lines
5.2 KiB
Go
package account
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// TestTelegramSeed covers the pure mapping from Telegram launch fields to the
|
|
// create-time account seed: supported-language detection (bare and region-tagged),
|
|
// the real-name → @username (verbatim) → placeholder display-name precedence, and
|
|
// the sanitization of the real name (emoji, digits, punctuation stripped to the
|
|
// editable format). The username, when used, is kept verbatim.
|
|
func TestTelegramSeed(t *testing.T) {
|
|
cases := map[string]struct {
|
|
languageCode, username, firstName string
|
|
wantLang, wantName string
|
|
}{
|
|
"ru bare": {"ru", "user", "Иван", "ru", "Иван"},
|
|
"en region-tagged": {"en-US", "user", "John", "en", "John"},
|
|
"ru region-tagged": {"ru-RU", "", "Пётр", "ru", "Пётр"},
|
|
"unknown language": {"fr", "frodo", "Frodo", "", "Frodo"},
|
|
"empty language": {"", "neo", "Neo", "", "Neo"},
|
|
"first name wins": {"en", "handle", "Real Name", "en", "Real Name"},
|
|
"username fallback": {"en", "handle", "", "en", "handle"},
|
|
"trimmed": {" RU ", " ", " Anna ", "ru", "Anna"},
|
|
"emoji stripped": {"en", "user", "🎮Kaya🎮", "en", "Kaya"},
|
|
"punct to space": {"en", "user", "John❤Doe", "en", "John Doe"},
|
|
"digits dropped": {"ru", "user", "Маша123", "ru", "Маша"},
|
|
"garbage to username": {"en", "good", "123!@#", "en", "good"},
|
|
"username verbatim": {"en", "co_ol99", "🎮🎮", "en", "co_ol99"},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := telegramSeed(tc.languageCode, tc.username, tc.firstName)
|
|
if got.preferredLanguage != tc.wantLang {
|
|
t.Errorf("preferredLanguage = %q, want %q", got.preferredLanguage, tc.wantLang)
|
|
}
|
|
if got.displayName != tc.wantName {
|
|
t.Errorf("displayName = %q, want %q", got.displayName, tc.wantName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTelegramSeedPlaceholder checks that a name with no usable letters falls back to
|
|
// a generated placeholder in the seeded language ("Player-NNNNN" / "Игрок-NNNNN").
|
|
func TestTelegramSeedPlaceholder(t *testing.T) {
|
|
cases := map[string]struct {
|
|
languageCode, username, firstName string
|
|
wantRe string
|
|
}{
|
|
"en empty": {"en", "", "", `^Player-\d{5}$`},
|
|
"ru empty": {"ru", "", "", `^Игрок-\d{5}$`},
|
|
"default en": {"fr", "", "", `^Player-\d{5}$`},
|
|
"name garbage, no username": {"ru", "", "!!!", `^Игрок-\d{5}$`},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := telegramSeed(tc.languageCode, tc.username, tc.firstName).displayName
|
|
if !regexp.MustCompile(tc.wantRe).MatchString(got) {
|
|
t.Errorf("displayName = %q, want match %s", got, tc.wantRe)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTelegramSeedTruncatesLongName checks an over-long Telegram name is capped to
|
|
// maxDisplayName runes (counted in runes, not bytes).
|
|
func TestTelegramSeedTruncatesLongName(t *testing.T) {
|
|
long := strings.Repeat("я", maxDisplayName+5)
|
|
got := telegramSeed("ru", "", long)
|
|
if n := utf8.RuneCountInString(got.displayName); n != maxDisplayName {
|
|
t.Errorf("display name rune count = %d, want %d", n, maxDisplayName)
|
|
}
|
|
}
|
|
|
|
// TestVKSeed covers the pure mapping from VK launch fields to the create-time account
|
|
// seed: supported-language detection from vk_language (bare and region-tagged) and the
|
|
// display name sanitized from the client-supplied name. Unlike Telegram there is no
|
|
// @username fallback — VK provides only the name.
|
|
func TestVKSeed(t *testing.T) {
|
|
cases := map[string]struct {
|
|
languageCode, displayName string
|
|
wantLang, wantName string
|
|
}{
|
|
"ru bare": {"ru", "Иван", "ru", "Иван"},
|
|
"en region-tagged": {"en-US", "John", "en", "John"},
|
|
"full name kept": {"ru", "Иван Петров", "ru", "Иван Петров"},
|
|
"unknown language": {"uk", "Тарас", "", "Тарас"},
|
|
"empty language": {"", "Neo", "", "Neo"},
|
|
"trimmed": {" RU ", " Anna ", "ru", "Anna"},
|
|
"emoji stripped": {"en", "🎮Kaya🎮", "en", "Kaya"},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := vkSeed(tc.languageCode, tc.displayName)
|
|
if got.preferredLanguage != tc.wantLang {
|
|
t.Errorf("preferredLanguage = %q, want %q", got.preferredLanguage, tc.wantLang)
|
|
}
|
|
if got.displayName != tc.wantName {
|
|
t.Errorf("displayName = %q, want %q", got.displayName, tc.wantName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVKSeedPlaceholder checks a VK name with no usable letters falls back to a
|
|
// generated placeholder in the seeded language ("Player-NNNNN" / "Игрок-NNNNN").
|
|
func TestVKSeedPlaceholder(t *testing.T) {
|
|
cases := map[string]struct {
|
|
languageCode, displayName string
|
|
wantRe string
|
|
}{
|
|
"en empty": {"en", "", `^Player-\d{5}$`},
|
|
"ru empty": {"ru", "", `^Игрок-\d{5}$`},
|
|
"default en": {"uk", "", `^Player-\d{5}$`},
|
|
"name garbage": {"ru", "123!@#", `^Игрок-\d{5}$`},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := vkSeed(tc.languageCode, tc.displayName).displayName
|
|
if !regexp.MustCompile(tc.wantRe).MatchString(got) {
|
|
t.Errorf("displayName = %q, want match %s", got, tc.wantRe)
|
|
}
|
|
})
|
|
}
|
|
}
|