Brings the project to a TDD/BDD-friendly state — apologies for shipping
Phase 1.5 + Phase 2 code-first, that violated feedback_tdd_first_bdd_required.
What's added :
- helpers_test.go : FakeTelegram (httptest server that records sendMessage /
deleteMessage / setWebhook / etc.), miniredis bootstrap, MakeUpdate /
PostWebhook helpers. The same harness simulates 'a user DMing the bot'
end-to-end without hitting Telegram cloud — answer to the user question.
- 43 tests covering : allowlist parsing, telegram type helpers (UserID /
ChatID / Text / messageID), secret_token constant-time compare, Backoff
schedule, Auth (login wrong/right/logout/TTL/nil-receiver), EchoHandler,
HTTPHandler (forward / timeout / non-2xx / empty body), AuthHandler
(start / auth / whoami / logout / replay defense delete), Server (bad
secret 401, unknown bot 404, allowlist drop, gated bot prompt,
full /auth → echo → /logout flow, healthz/readyz).
- All tests pass with -race in 1.6s, no external deps (miniredis +
httptest in-process).
Infra :
- Updated .gitea/workflows/dockerimage.yaml : new 'test' job
(go vet + go test -race) gates the build-and-push-image job. CI now
also runs on pull_request.
- docker-compose.yml : redis + postgres for full local stack.
- Makefile : test-race, compose-up/down targets.
- README updated with test + local-dev sections.
Refs ~/.claude/plans/pour-les-notifications-on-inherited-seal.md § Phase 2.
Adds the async dispatch infrastructure :
- Postgres pool + embedded migration (CREATE TABLE/INDEX IF NOT EXISTS
gateway_jobs). Auto-applied at boot. lib/pq driver (matches webapp
convention).
- queue.go : Enqueue (idempotent on UNIQUE(bot_slug, update_id) — handles
Telegram redelivery), Pop with FOR UPDATE SKIP LOCKED, MarkDone,
MarkFailed with exponential backoff (30s → 2m → 10m → 1h → dead at 5).
- worker.go : goroutine that drains the queue, dispatches via the same
Handler interface as sync, schedules retries on failure, notifies the
user once when a job goes to dead.
- BotConfig gains `async: bool`. Registry refuses bots with async=true
if DATABASE_URL is unset (queue=nil).
- Server : when bot.Async, the webhook ack is immediate ; the update
payload is enqueued for the worker.
When DATABASE_URL is unset (current default), queue/worker stay disabled
and only sync handlers (echo, http, auth) work — no breaking change to
the running cluster.
Refs ~/.claude/plans/pour-les-notifications-on-inherited-seal.md § Phase 2.
Adds an authentication layer in front of the bot handlers :
- Auth handler on the principal bot (@arcodange_factory_bot, slug
factory) parses /start, /auth <code>, /whoami, /logout. On a
successful /auth, the message containing the code is best-effort
deleted from the user's chat (replay defense).
- Redis-backed sessions (key tg-gw:auth:<from.id>, TTL 24h, configurable
via AUTH_SESSION_TTL). Constant-time secret compare via crypto/subtle.
- ALLOWED_USERS env (CSV of Telegram user IDs) — silent-drops anyone
not in the list before the auth gate runs.
- New per-bot field 'requireAuth' (pointer-bool). Default = true (secure
by default). Auto-forced to false for handler=auth (chicken-and-egg).
- Server gates: allowlist first, then requireAuth before handler dispatch.
- Fail-at-startup if a bot is configured with handler=auth or
requireAuth: true while AUTH_SECRET is unset.
Design: factory/docs/adr/20260509-telegram-gateway-auth.md (in factory PR).
User docs: AUTH.md (new), HOWTO_ADD_BOT.md (Cas 2 updated for default
true and gated flow).
New deps: github.com/redis/go-redis/v9.
Refs ~/.claude/plans/pour-les-notifications-on-inherited-seal.md § Phase 1.5.
Aligns the project name with the public URL (tg.arcodange.fr) and the
Arcodange organization conventions. The 'homelab-gateway' name was too
generic.
Touches: chart name + helpers, image registry path, Go module path,
secret/configmap names, deployment mountPath, all docs.