3590df28db
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.
101 lines
2.8 KiB
Go
101 lines
2.8 KiB
Go
package bot
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// fakeBotAPI answers getMe (so bot.New succeeds offline) and records the last
|
|
// sendMessage form fields.
|
|
type fakeBotAPI struct {
|
|
chatID string
|
|
text string
|
|
replyMarkup string
|
|
}
|
|
|
|
func (f *fakeBotAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/getMe"):
|
|
io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"test","username":"testbot"}}`)
|
|
case strings.HasSuffix(r.URL.Path, "/sendMessage"):
|
|
f.chatID = r.FormValue("chat_id")
|
|
f.text = r.FormValue("text")
|
|
f.replyMarkup = r.FormValue("reply_markup")
|
|
io.WriteString(w, `{"ok":true,"result":{"message_id":1}}`)
|
|
default:
|
|
io.WriteString(w, `{"ok":true,"result":true}`)
|
|
}
|
|
}
|
|
|
|
func newTestBot(t *testing.T, api http.Handler) *Bot {
|
|
t.Helper()
|
|
srv := httptest.NewServer(api)
|
|
t.Cleanup(srv.Close)
|
|
b, err := New(Config{Token: "123:ABC", APIBaseURL: srv.URL, MiniAppURL: "https://example.com/telegram/"}, zap.NewNop())
|
|
if err != nil {
|
|
t.Fatalf("new bot: %v", err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestNotifyBuildsLaunchButton(t *testing.T) {
|
|
api := &fakeBotAPI{}
|
|
b := newTestBot(t, api)
|
|
if err := b.Notify(context.Background(), 12345, "It's your turn.", "Open game", "g7c9e"); err != nil {
|
|
t.Fatalf("notify: %v", err)
|
|
}
|
|
if api.chatID != "12345" {
|
|
t.Errorf("chat_id = %q, want 12345", api.chatID)
|
|
}
|
|
if api.text != "It's your turn." {
|
|
t.Errorf("text = %q", api.text)
|
|
}
|
|
if !strings.Contains(api.replyMarkup, "web_app") || !strings.Contains(api.replyMarkup, "startapp=g7c9e") {
|
|
t.Errorf("reply_markup = %q, want a web_app button with startapp=g7c9e", api.replyMarkup)
|
|
}
|
|
}
|
|
|
|
func TestSendTextHasNoMarkup(t *testing.T) {
|
|
api := &fakeBotAPI{}
|
|
b := newTestBot(t, api)
|
|
if err := b.SendText(context.Background(), 999, "plain"); err != nil {
|
|
t.Fatalf("send text: %v", err)
|
|
}
|
|
if api.chatID != "999" || api.text != "plain" {
|
|
t.Errorf("chat_id=%q text=%q, want 999/plain", api.chatID, api.text)
|
|
}
|
|
if api.replyMarkup != "" {
|
|
t.Errorf("reply_markup = %q, want empty", api.replyMarkup)
|
|
}
|
|
}
|
|
|
|
func TestStartPayload(t *testing.T) {
|
|
cases := map[string]string{
|
|
"/start g123": "g123",
|
|
"/start": "",
|
|
"/start f99 ": "f99",
|
|
"hello": "",
|
|
}
|
|
for in, want := range cases {
|
|
if got := startPayload(in); got != want {
|
|
t.Errorf("startPayload(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLaunchURL(t *testing.T) {
|
|
b := &Bot{miniAppURL: "https://example.com/telegram/"}
|
|
if got := b.launchURL(""); got != "https://example.com/telegram/" {
|
|
t.Errorf("empty start param = %q, want the base URL", got)
|
|
}
|
|
if got := b.launchURL("g123"); !strings.Contains(got, "startapp=g123") {
|
|
t.Errorf("launchURL = %q, want startapp=g123", got)
|
|
}
|
|
}
|