From 79104ef3d7ac1be7c4d3230af4b0f18e8228c54e Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 7 Oct 2026 15:08:08 +0700 Subject: [PATCH] feat(config): resolve the bot username from BOT_USERNAME or getMe The default sticker pack name and the lol User-Agent now follow whichever bot runs the code, so a bot account change needs no code edit. The default pack becomes miti99_by_. --- .env.example | 9 +++- cmd/server/bot_username_test.go | 49 +++++++++++++++++++ cmd/server/main.go | 32 +++++++++++- compose.yml | 3 +- docs/deploy-coolify-selfhosted.md | 3 +- docs/sticker-packs.md | 7 ++- internal/modules/lol/api_client.go | 20 ++++++-- internal/modules/lol/api_client_test.go | 9 ++++ internal/modules/lol/lol.go | 2 +- internal/modules/module.go | 4 ++ internal/modules/registry.go | 10 ++-- .../modules/sticker/addsticker_command.go | 10 ++-- .../sticker/addsticker_command_test.go | 38 ++++++++++++-- internal/modules/sticker/sticker.go | 10 ++-- internal/modules/sticker/sticker_pack.go | 27 ++++++---- internal/modules/sticker/sticker_pack_test.go | 6 +-- 16 files changed, 198 insertions(+), 41 deletions(-) create mode 100644 cmd/server/bot_username_test.go diff --git a/.env.example b/.env.example index e879f13..3bacad8 100644 --- a/.env.example +++ b/.env.example @@ -21,12 +21,17 @@ OWNER_ID= # Comma-separated admin Telegram user ids (renamed from ADMIN_USER_IDS). ADMIN_IDS= +# Bot's Telegram username, without "@". Unset = asked from Telegram (getMe) +# once at startup. Used for the default sticker pack name and the lol +# User-Agent. Set it to skip that call or to pin the value. +BOT_USERNAME= + # Sticker set /addsticker writes to. Every user contributes to this one pack. # MUST end in _by_: Telegram requires that suffix on sets a # bot creates and refuses to edit sets it did not create, so the suffix is what # proves the pack is manageable. Created automatically, owned by OWNER_ID, on -# the first /addsticker if it does not exist yet. Unset = the default below. -STICKER_PACK_NAME=miti99_by_miti99bot +# the first /addsticker if it does not exist yet. Unset = miti99_by_. +STICKER_PACK_NAME= # SOURCE_COMMIT (commit SHA) is read at startup for the deploynotify owner DM. # Do NOT set it here. Coolify provides it at runtime. Keep "Include Source diff --git a/cmd/server/bot_username_test.go b/cmd/server/bot_username_test.go new file mode 100644 index 0000000..fa61bf7 --- /dev/null +++ b/cmd/server/bot_username_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "testing" + + "github.com/tiennm99/miti99bot/internal/testutil" +) + +func TestResolveBotUsername(t *testing.T) { + t.Run("configured value wins without calling getMe", func(t *testing.T) { + rb := testutil.NewRecordingBot(t) + if got := resolveBotUsername(context.Background(), rb.Bot, "envbot"); got != "envbot" { + t.Errorf("got %q, want envbot", got) + } + for _, c := range rb.Sent() { + if c.Method == "getMe" { + t.Error("getMe called although BOT_USERNAME was set") + } + } + }) + + t.Run("falls back to getMe", func(t *testing.T) { + rb := testutil.NewRecordingBot(t) + rb.StubMethod("getMe", `{"id":1,"is_bot":true,"username":"apibot"}`) + if got := resolveBotUsername(context.Background(), rb.Bot, ""); got != "apibot" { + t.Errorf("got %q, want apibot", got) + } + }) + + // RecordingBot answers an unstubbed method with a bare `true`, which cannot + // decode into a User: the failure must yield "" rather than stop startup. + t.Run("getMe failure yields empty", func(t *testing.T) { + rb := testutil.NewRecordingBot(t) + if got := resolveBotUsername(context.Background(), rb.Bot, ""); got != "" { + t.Errorf("got %q, want empty", got) + } + }) +} + +func TestLoadConfig_BotUsername(t *testing.T) { + t.Setenv("PORT", "8080") + for in, want := range map[string]string{"": "", "examplebot": "examplebot", " @examplebot ": "examplebot"} { + t.Setenv("BOT_USERNAME", in) + if got := loadConfig().BotUsername; got != want { + t.Errorf("BOT_USERNAME=%q: got %q, want %q", in, got, want) + } + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go index bbb6775..eb5e2d9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -17,6 +17,8 @@ import ( "syscall" "time" + "github.com/go-telegram/bot" + "github.com/tiennm99/miti99bot/internal/cron" "github.com/tiennm99/miti99bot/internal/deploynotify" "github.com/tiennm99/miti99bot/internal/log" @@ -164,8 +166,10 @@ func main() { log.Fatal("telegram bot init failed", "err", err) } + botUsername := resolveBotUsername(rootCtx, b, cfg.BotUsername) reg, err := modules.Build(cfg.Modules, factories(), provider, modules.BuildOptions{ - Bot: b, + Bot: b, + BotUsername: botUsername, }) if err != nil { log.Fatal("module registry build failed", "err", err) @@ -355,11 +359,36 @@ func buildProvider(ctx context.Context, cfg config) (storage.Provider, func(), e } } +// botUsernameTimeout bounds the startup getMe. It is best-effort, so a slow +// Telegram must not hold up the rest of startup for long. +const botUsernameTimeout = 10 * time.Second + +// resolveBotUsername returns the bot's username: BOT_USERNAME when set, +// otherwise one getMe call. A failed getMe is logged and yields "" rather than +// stopping startup; modules that need the username then learn it lazily or +// do without. +func resolveBotUsername(ctx context.Context, b *bot.Bot, configured string) string { + if configured != "" { + log.Info("bot username", "username", configured, "source", "BOT_USERNAME") + return configured + } + ctx, cancel := context.WithTimeout(ctx, botUsernameTimeout) + defer cancel() + me, err := b.GetMe(ctx) + if err != nil || me == nil || me.Username == "" { + log.Warn("getMe failed; bot username unknown until a module asks again", "err", err) + return "" + } + log.Info("bot username", "username", me.Username, "source", "getMe") + return me.Username +} + // config is the process configuration read from the environment by loadConfig. type config struct { Port string TelegramBotToken string SourceCommit string // Coolify-injected commit SHA (runtime env) for deploynotify + BotUsername string // optional; empty = ask Telegram via getMe at startup Modules []string BotOwnerID int64 AdminUserIDs map[int64]bool @@ -392,6 +421,7 @@ func loadConfig() config { Port: port, TelegramBotToken: envMap["TELEGRAM_BOT_TOKEN"], SourceCommit: envMap["SOURCE_COMMIT"], + BotUsername: strings.TrimPrefix(strings.TrimSpace(envMap["BOT_USERNAME"]), "@"), Modules: splitCSV(envMap["MODULES"]), BotOwnerID: parseInt64(envMap["OWNER_ID"]), AdminUserIDs: parseInt64Set(envMap["ADMIN_IDS"]), diff --git a/compose.yml b/compose.yml index 26c5e4f..43b6890 100644 --- a/compose.yml +++ b/compose.yml @@ -23,7 +23,8 @@ services: # ADMIN_IDS: ${ADMIN_IDS:-} # CSV of admin Telegram user ids; empty = none # LOL_PANDASCORE_TOKEN: ${LOL_PANDASCORE_TOKEN:-} # PandaScore token — SECRET; empty = /lol* fetches fail # GOLD_VNAPP_API_KEY: ${GOLD_VNAPP_API_KEY:-} # VNAppMob key — SECRET; empty = fetched and cached in Mongo - # STICKER_PACK_NAME: ${STICKER_PACK_NAME:-miti99_by_miti99bot} # /addsticker set + # BOT_USERNAME: ${BOT_USERNAME:-} # bot username without "@"; empty = getMe at startup + # STICKER_PACK_NAME: ${STICKER_PACK_NAME:-} # /addsticker set; empty = miti99_by_ # LOG_LEVEL: ${LOG_LEVEL:-info} # debug|info|warn|error # --- Fixed by this stack (not Coolify settings) --- diff --git a/docs/deploy-coolify-selfhosted.md b/docs/deploy-coolify-selfhosted.md index bc5aae9..8611800 100644 --- a/docs/deploy-coolify-selfhosted.md +++ b/docs/deploy-coolify-selfhosted.md @@ -34,7 +34,8 @@ Copy [`.env.example`](../.env.example) → `.env` (gitignored) and fill in. | `MODULES` | optional | CSV; empty = all modules, including any added later | | `OWNER_ID` | optional | Telegram user id for owner-only commands, the deploy DM, and the `/addsticker` pack owner. Unset = owner-only commands are denied and `/addsticker` refuses | | `ADMIN_IDS` | optional | CSV of Telegram user ids for admin-only commands | -| `STICKER_PACK_NAME` | optional | set `/addsticker` writes to; default `miti99_by_miti99bot`. See [sticker packs](sticker-packs.md) | +| `BOT_USERNAME` | optional | the bot's Telegram username, without `@`; unset = asked from Telegram (`getMe`) once at startup. Used for the default sticker pack name and the lol User-Agent | +| `STICKER_PACK_NAME` | optional | set `/addsticker` writes to; default `miti99_by_`. See [sticker packs](sticker-packs.md) | | `LOL_PANDASCORE_TOKEN` | optional | PandaScore API token for the lol module (free tier) — secret, never logged; without it every `/lol*` fetch fails (stale cache may still serve briefly) | | `RENDERER_URL` | leave unset | base URL of the animation renderer; fixed by `compose.yml` to the bundled renderer (`http://renderer:3000`), so a Coolify value is ignored | | `LOG_LEVEL` | optional | `debug`, `info` (default), `warn`, or `error`; logs are JSON on stdout | diff --git a/docs/sticker-packs.md b/docs/sticker-packs.md index 82ffc16..d800783 100644 --- a/docs/sticker-packs.md +++ b/docs/sticker-packs.md @@ -24,7 +24,7 @@ Single-shot: one message replying to the media to add. No conversation state. | Env | Default | Meaning | |---|---|---| -| `STICKER_PACK_NAME` | `miti99_by_miti99bot` | The Telegram set to write to | +| `STICKER_PACK_NAME` | `miti99_by_` | The Telegram set to write to | | `OWNER_ID` | — | Must be the account that **owns** that set | `OWNER_ID` is reused rather than given a sticker-specific twin because @@ -36,6 +36,9 @@ The caller's identity is used nowhere. That is what makes the command stateless: no records, no keys, no per-user locks, and no ownership checks. It is also why `/addsticker` needs no per-user storage. +The bot's username comes from `BOT_USERNAME` when set, otherwise from one +`getMe` call at startup (retried on the first `/addsticker` if that failed). + `STICKER_PACK_NAME` **must end in `_by_`** — Telegram requires that suffix on every set a bot creates, and refuses to let a bot edit any set it did not create. A name without the suffix therefore cannot be this @@ -49,7 +52,7 @@ before any download or API call: **The pack creates itself on first use.** If the set does not exist, the first `/addsticker` creates it — owned by `OWNER_ID`, titled with the slug half of the -name (`miti99_by_miti99bot` → *miti99*), and seeded with the sticker that +name (`miti99_by_examplebot` → *miti99*), and seeded with the sticker that triggered it, because Telegram cannot create an empty set. The reply says *"Created the shared pack with this sticker."* diff --git a/internal/modules/lol/api_client.go b/internal/modules/lol/api_client.go index b96f5e2..707db1c 100644 --- a/internal/modules/lol/api_client.go +++ b/internal/modules/lol/api_client.go @@ -49,7 +49,9 @@ const ( // credential itself. tokenEnv = "LOL_PANDASCORE_TOKEN" // #nosec G101 - userAgent = "miti99bot/0.1 (https://t.me/miti99bot)" + // userAgentProduct leads the User-Agent; Client.userAgent appends the + // running bot's t.me link when its username is known. + userAgentProduct = "miti99bot/0.1" // pageSize is PandaScore's per_page maximum; 100 covers a dense day in // one request and a full week in a couple. pageSize = 100 @@ -247,8 +249,18 @@ type CacheStore = storage.DocStore[cacheRecord] // default HTTP client; tests inject a custom HTTP client (typically pointing // at httptest.Server). type Client struct { - HTTP *http.Client - URL string // override for tests; empty falls back to apiURL + HTTP *http.Client + URL string // override for tests; empty falls back to apiURL + BotUsername string // running bot's username for the User-Agent; may be empty +} + +// userAgent identifies the caller to PandaScore, linking the running bot when +// its username is known. +func (c *Client) userAgent() string { + if c.BotUsername == "" { + return userAgentProduct + } + return userAgentProduct + " (https://t.me/" + c.BotUsername + ")" } // httpClient returns the client to use, or a sensible default. @@ -296,7 +308,7 @@ func (c *Client) fetchEventsPage(ctx context.Context, from, to time.Time, page i // stay safe to log. req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", userAgent) + req.Header.Set("User-Agent", c.userAgent()) resp, err := c.httpClient().Do(req) if err != nil { diff --git a/internal/modules/lol/api_client_test.go b/internal/modules/lol/api_client_test.go index 74c8ff2..1fcaf17 100644 --- a/internal/modules/lol/api_client_test.go +++ b/internal/modules/lol/api_client_test.go @@ -422,3 +422,12 @@ func TestTruncate(t *testing.T) { t.Errorf("truncate = %q, want 'a lon...'", got) } } + +func TestClientUserAgent(t *testing.T) { + if got := (&Client{}).userAgent(); got != "miti99bot/0.1" { + t.Errorf("no username: got %q", got) + } + if got := (&Client{BotUsername: "examplebot"}).userAgent(); got != "miti99bot/0.1 (https://t.me/examplebot)" { + t.Errorf("with username: got %q", got) + } +} diff --git a/internal/modules/lol/lol.go b/internal/modules/lol/lol.go index de06ff3..8c47e14 100644 --- a/internal/modules/lol/lol.go +++ b/internal/modules/lol/lol.go @@ -19,7 +19,7 @@ func New(deps modules.Deps) modules.Module { subscribers: storage.Typed[subscription.Doc](deps.Store), pushDate: storage.Typed[subscription.DayDoc](deps.Store), cache: storage.Typed[cacheRecord](deps.Store), - client: &Client{}, + client: &Client{BotUsername: deps.BotUsername}, } return modules.Module{ Commands: []modules.Command{ diff --git a/internal/modules/module.go b/internal/modules/module.go index e05fee1..843ec8f 100644 --- a/internal/modules/module.go +++ b/internal/modules/module.go @@ -117,6 +117,10 @@ type Deps struct { Store storage.Collection // the module's own collection; build typed views with storage.Typed[T] Registry *Registry // populated by Build; safe to capture but read-only at module use Bot *bot.Bot // nil-safe: only crons that fan-out (lol daily push) need it + // BotUsername is the bot's Telegram username without "@", from BOT_USERNAME + // or getMe at startup. Empty when neither was available; consumers must + // cope (sticker resolves it lazily, lol omits it from its User-Agent). + BotUsername string } // Factory constructs a Module from its Deps. Deps are passed directly (instead diff --git a/internal/modules/registry.go b/internal/modules/registry.go index b80c0af..6cceac1 100644 --- a/internal/modules/registry.go +++ b/internal/modules/registry.go @@ -159,7 +159,8 @@ func (r *Registry) Crons() []Cron { // Factory's Deps. Adding new optional deps here keeps Build's signature // stable as the dep list grows. type BuildOptions struct { - Bot *bot.Bot + Bot *bot.Bot + BotUsername string } // Build constructs a Registry from the requested module names. The Provider @@ -220,9 +221,10 @@ func Build(enabled []string, factories map[string]Factory, provider storage.Prov } moduleDeps := Deps{ - Store: provider.Collection(name), - Registry: reg, - Bot: opts.Bot, + Store: provider.Collection(name), + Registry: reg, + Bot: opts.Bot, + BotUsername: opts.BotUsername, } mod := factory(moduleDeps) // A factory that hardcodes its own Name is a bug: the registry key is diff --git a/internal/modules/sticker/addsticker_command.go b/internal/modules/sticker/addsticker_command.go index 039d14f..2f9b4b2 100644 --- a/internal/modules/sticker/addsticker_command.go +++ b/internal/modules/sticker/addsticker_command.go @@ -53,9 +53,10 @@ type stickerSource struct { // no ownership to check. That is what keeps the command stateless. // // The resolver is built here and captured by the handler so the bot's username -// is fetched at most once per process rather than once per invocation. -func addStickerCommand() modules.Command { - resolver := &botUsernameResolver{} +// is fetched at most once per process rather than once per invocation. It +// starts from botUsername, which is empty when startup could not learn it. +func addStickerCommand(botUsername string) modules.Command { + resolver := &botUsernameResolver{username: botUsername} return modules.Command{ Name: "addsticker", Visibility: modules.VisibilityPublic, @@ -93,6 +94,9 @@ func handleAddSticker(ctx context.Context, b *bot.Bot, update *models.Update, re if err != nil { return replyErr(ctx, b, msg, "util_addsticker_username", err) } + if pack.Name == "" { + pack.Name = defaultPackName(username) + } title, err := packTitle(pack.Name, username) if err != nil { return replyMisconfigured(ctx, b, msg, "util_addsticker_pack_name", err, packNotBotOwnedRefusal) diff --git a/internal/modules/sticker/addsticker_command_test.go b/internal/modules/sticker/addsticker_command_test.go index f37e483..95ab8d8 100644 --- a/internal/modules/sticker/addsticker_command_test.go +++ b/internal/modules/sticker/addsticker_command_test.go @@ -94,8 +94,10 @@ func TestAddSticker_NonOwnerWritesToConfiguredPack(t *testing.T) { } } -func TestAddSticker_DefaultsToMiti99Pack(t *testing.T) { - rb := installAddSticker(t, "", "miti99bot") +// The default pack follows the running bot: its name ends in the username +// getMe reports, so the same build works under any bot token. +func TestAddSticker_DefaultPackFollowsBotUsername(t *testing.T) { + rb := installAddSticker(t, "", "otherbot") rb.Bot.ProcessUpdate(context.Background(), stickerReply(999, "", "src", "")) @@ -103,8 +105,36 @@ func TestAddSticker_DefaultsToMiti99Pack(t *testing.T) { if !ok { t.Fatalf("no addStickerToSet call; got %+v", rb.Sent()) } - if got := call.Form["name"]; got != "miti99_by_miti99bot" { - t.Errorf("name = %q, want the default pack", got) + if got := call.Form["name"]; got != "miti99_by_otherbot" { + t.Errorf("name = %q, want the default pack for @otherbot", got) + } +} + +// A username known at startup (BOT_USERNAME or the startup getMe) is used as +// is: the command must not spend a getMe call re-learning it. +func TestAddSticker_SeededUsernameSkipsGetMe(t *testing.T) { + t.Setenv("OWNER_ID", "999") + t.Setenv("STICKER_PACK_NAME", "") + rb := testutil.NewRecordingBot(t) + reg, err := modules.Build([]string{"sticker"}, + map[string]modules.Factory{"sticker": sticker.New}, + storage.NewMemoryProvider(), modules.BuildOptions{BotUsername: "seededbot"}) + if err != nil { + t.Fatalf("Build: %v", err) + } + modules.Install(rb.Bot, reg, modules.Auth{}) + + rb.Bot.ProcessUpdate(context.Background(), stickerReply(999, "", "src", "")) + + if _, ok := callTo(rb, "getMe"); ok { + t.Error("getMe called although the username was seeded") + } + call, ok := callTo(rb, "addStickerToSet") + if !ok { + t.Fatalf("no addStickerToSet call; got %+v", rb.Sent()) + } + if got := call.Form["name"]; got != "miti99_by_seededbot" { + t.Errorf("name = %q, want the default pack for @seededbot", got) } } diff --git a/internal/modules/sticker/sticker.go b/internal/modules/sticker/sticker.go index 8a119a3..429a31f 100644 --- a/internal/modules/sticker/sticker.go +++ b/internal/modules/sticker/sticker.go @@ -15,13 +15,13 @@ import ( // same handle to Build and to any startup task, matching lol/coin/stock. const CollectionName = "sticker" -// New is the module Factory. It takes no Deps: the command reads its pack from -// the environment and keeps no state, so the collection this module is handed -// goes unused. -func New(_ modules.Deps) modules.Module { +// New is the module Factory. Only Deps.BotUsername is used: the command reads +// its pack from the environment and keeps no state, so the collection this +// module is handed goes unused. +func New(deps modules.Deps) modules.Module { return modules.Module{ Commands: []modules.Command{ - addStickerCommand(), + addStickerCommand(deps.BotUsername), }, } } diff --git a/internal/modules/sticker/sticker_pack.go b/internal/modules/sticker/sticker_pack.go index f366231..cf3ada8 100644 --- a/internal/modules/sticker/sticker_pack.go +++ b/internal/modules/sticker/sticker_pack.go @@ -23,8 +23,10 @@ const ( // not exist yet is created by the first successful /addsticker. stickerPackNameEnv = "STICKER_PACK_NAME" - // defaultStickerPackName is the shared pack used when the env is unset. - defaultStickerPackName = "miti99_by_miti99bot" + // defaultStickerPackSlug names the shared pack used when the env is unset. + // The full name is this slug plus "_by_", so the default + // follows whichever bot runs the code. + defaultStickerPackSlug = "miti99" // stickerPackOwnerEnv reuses the bot-wide owner setting rather than // introducing a second variable: AddStickerToSet needs the *set owner's* @@ -74,7 +76,7 @@ const ( // stickerPack is the resolved target of /addsticker. type stickerPack struct { - Name string // Telegram set name, e.g. "miti99_by_miti99bot" + Name string // Telegram set name, e.g. "miti99_by_examplebot"; empty until defaulted OwnerID int64 // the account the set belongs to; AddStickerToSet demands it } @@ -82,16 +84,15 @@ type stickerPack struct { // Internal, not user-facing: nothing the caller does fixes a misconfiguration. var errNoPackOwner = errors.New("util: sticker pack owner ID unset") -// loadStickerPack resolves the shared pack from the environment. +// loadStickerPack resolves the shared pack from the environment. Name is left +// empty when STICKER_PACK_NAME is unset: the default depends on the bot's +// username, which the caller fills in with defaultPackName once resolved. // // Read per invocation rather than captured at startup, matching how gold and // lol read their credentials: it keeps the command testable with t.Setenv and // costs nothing next to the API calls that follow. func loadStickerPack() (stickerPack, error) { name := strings.TrimSpace(os.Getenv(stickerPackNameEnv)) - if name == "" { - name = defaultStickerPackName - } ownerID, err := strconv.ParseInt(strings.TrimSpace(os.Getenv(stickerPackOwnerEnv)), 10, 64) if err != nil || ownerID == 0 { return stickerPack{}, errNoPackOwner @@ -99,6 +100,11 @@ func loadStickerPack() (stickerPack, error) { return stickerPack{Name: name, OwnerID: ownerID}, nil } +// defaultPackName is the shared pack's name for a bot with this username. +func defaultPackName(botUsername string) string { + return defaultStickerPackSlug + setNameSuffix + botUsername +} + // stickerShareLink is the public URL of a sticker set. func stickerShareLink(setName string) string { return "https://t.me/addstickers/" + setName @@ -150,9 +156,10 @@ func packTitle(name, botUsername string) (string, error) { // botUsernameResolver caches the bot's username. // -// The bot starts with bot.WithSkipGetMe(), so nothing populates a username -// until this asks. Failures are never cached: a transient GetMe error must not -// disable /addsticker for the process's lifetime. +// main seeds it from BOT_USERNAME or a startup getMe. The bot starts with +// bot.WithSkipGetMe(), so when neither produced a username nothing populates +// one until this asks. Failures are never cached: a transient GetMe error must +// not disable /addsticker for the process's lifetime. type botUsernameResolver struct { mu sync.Mutex username string diff --git a/internal/modules/sticker/sticker_pack_test.go b/internal/modules/sticker/sticker_pack_test.go index 9227f4e..f806acf 100644 --- a/internal/modules/sticker/sticker_pack_test.go +++ b/internal/modules/sticker/sticker_pack_test.go @@ -106,15 +106,15 @@ func TestPackTitle_AtNameLengthCap(t *testing.T) { } func TestLoadStickerPack(t *testing.T) { - t.Run("defaults the name and requires an owner", func(t *testing.T) { + t.Run("leaves the name to the default and requires an owner", func(t *testing.T) { t.Setenv("OWNER_ID", "42") t.Setenv("STICKER_PACK_NAME", "") pack, err := loadStickerPack() if err != nil { t.Fatalf("loadStickerPack: %v", err) } - if pack.Name != defaultStickerPackName { - t.Errorf("name = %q, want %q", pack.Name, defaultStickerPackName) + if pack.Name != "" { + t.Errorf("name = %q, want empty until the username fills the default", pack.Name) } if pack.OwnerID != 42 { t.Errorf("ownerID = %d, want 42", pack.OwnerID)