fluid-telegram/internal/tg/botfather_test.go
tegwick 5ddfee8250 Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.

The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.

The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.

Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172sgCZEEDJcnQmr4SGDvKa

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1361245@bnt-lap001
Assistant-Session: b3b428ef-f3e6-4688-b091-01f71461d66a
2026-09-04 21:39:30 +02:00

92 lines
2.8 KiB
Go

package tg
import (
"testing"
"github.com/gotd/td/tg"
"github.com/tegwick/fluid-telegram/internal/spec"
)
// The token is the one secret that passes through this process's memory. If the
// pattern stops matching, RegisterBot reports "no token found" after a bot has
// already been created -- which is the expensive failure, since retrying makes a
// second bot.
func TestTokenPattern(t *testing.T) {
replies := []struct {
name string
text string
want string
}{
{"typical", "Done! Congratulations on your new bot.\n\nUse this token to access the HTTP API:\n123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw\n\nKeep your token secure.", "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw"},
{"long id", "Token: 7123456789:AAF-abcDEFghiJKLmnoPQRstuVWXyz012345", "7123456789:AAF-abcDEFghiJKLmnoPQRstuVWXyz012345"},
{"underscores and dashes", "8000000001:AA_this-has_all-the_chars012345678", "8000000001:AA_this-has_all-the_chars012345678"},
}
for _, r := range replies {
t.Run(r.name, func(t *testing.T) {
m := tokenRe.FindStringSubmatch(r.text)
if m == nil {
t.Fatalf("no token matched in %q", r.text)
}
if m[1] != r.want {
t.Fatalf("got %q, want %q", m[1], r.want)
}
})
}
}
// It must not match things that merely look like tokens, or a failure reply
// could be mistaken for success and a bogus token written to OpenBao.
func TestTokenPatternRejects(t *testing.T) {
for _, s := range []string{
"Sorry, this username is already taken.",
"12:34",
"Please choose a name for your bot.",
} {
if tokenRe.MatchString(s) {
t.Errorf("matched a token in %q", s)
}
}
}
func TestFirstLineTruncates(t *testing.T) {
if got := firstLine("one\ntwo"); got != "one" {
t.Errorf("got %q", got)
}
long := make([]byte, 200)
for i := range long {
long[i] = 'x'
}
if got := firstLine(string(long)); len(got) > 95 {
t.Errorf("length %d, expected truncation", len(got))
}
}
// Rights reported back must use the spec's vocabulary, so a plan can compare
// them to the spec without translating between two naming schemes -- and must
// report a wider right rather than dropping it, since dropping one would make
// drift invisible exactly where it matters.
func TestRightsNames(t *testing.T) {
got := rightsNames(tg.ChatAdminRights{PostMessages: true})
if len(got) != 1 || got[0] != spec.PostMessages {
t.Fatalf("got %v, want [%s]", got, spec.PostMessages)
}
got = rightsNames(tg.ChatAdminRights{PostMessages: true, DeleteMessages: true})
if len(got) != 2 {
t.Fatalf("a wider right must be reported, got %v", got)
}
var sawDelete bool
for _, r := range got {
if r == "delete_messages" {
sawDelete = true
}
}
if !sawDelete {
t.Errorf("delete_messages was dropped: %v", got)
}
if got := rightsNames(tg.ChatAdminRights{}); len(got) != 0 {
t.Errorf("a bot with no rights should report none, got %v", got)
}
}