package main import ( "context" "net/http" "strings" "testing" "time" ) // e2eHarness wires a full Server (Registry + Auth + Allowlist + FakeTelegram) // for integration tests covering the webhook → handler dispatch path. type e2eHarness struct { ft *FakeTelegram auth *Auth server *Server } func newE2EHarness(t *testing.T, allowedUsers string) *e2eHarness { t.Helper() ft := NewFakeTelegram(t) m := StartMiniRedis(t) auth := NewTestAuth(t, m, testAuthCode, time.Hour) bots := map[string]BotConfig{ "factory": { Handler: "auth", Token: "factory-tok", Secret: "factory-secret", }, "echobot": { Handler: "echo", Token: "echo-tok", Secret: "echo-secret", }, "publicbot": { Handler: "echo", RequireAuth: boolPtr(false), Token: "pub-tok", Secret: "pub-secret", }, } cfg := &Config{Bots: bots} reg, err := NewRegistry(cfg, ft.Client(), auth, nil) if err != nil { t.Fatalf("NewRegistry: %v", err) } allow := NewAllowlist(allowedUsers) srv := NewServer(reg, auth, allow, ft.Client(), nil) return &e2eHarness{ft: ft, auth: auth, server: srv} } func (h *e2eHarness) routes() http.Handler { return h.server.Routes() } // authenticate does the /auth dance for the given user. func (h *e2eHarness) authenticate(t *testing.T, userID int64) { t.Helper() ok, err := h.auth.Login(context.Background(), userID, testAuthCode) if err != nil || !ok { t.Fatalf("authenticate user %d: ok=%v err=%v", userID, ok, err) } } func TestServer_BadSecretToken_401(t *testing.T) { h := newE2EHarness(t, "") upd := MakeUpdate(1, 7497777082, 7497777082, 1, "hi") status, _ := PostWebhook(t, h.routes(), "echobot", "WRONG", upd) if status != 401 { t.Fatalf("status = %d, want 401", status) } } func TestServer_UnknownBot_404(t *testing.T) { h := newE2EHarness(t, "") upd := MakeUpdate(1, 7497777082, 7497777082, 1, "hi") status, _ := PostWebhook(t, h.routes(), "ghost", "anything", upd) if status != 404 { t.Fatalf("status = %d, want 404", status) } } func TestServer_AllowlistDrop_SilentNoReply(t *testing.T) { h := newE2EHarness(t, "7497777082") // only this user is allowed upd := MakeUpdate(1, 999, 999, 1, "hi") // stranger status, _ := PostWebhook(t, h.routes(), "echobot", "echo-secret", upd) if status != 200 { t.Fatalf("status = %d, want 200", status) } if len(h.ft.Sent()) != 0 { t.Fatalf("stranger should be silent-dropped, got %d sends", len(h.ft.Sent())) } } func TestServer_GatedBot_NotAuthed_PromptsAuth(t *testing.T) { h := newE2EHarness(t, "") upd := MakeUpdate(1, 100, 100, 1, "hi") status, _ := PostWebhook(t, h.routes(), "echobot", "echo-secret", upd) if status != 200 { t.Fatalf("status = %d, want 200", status) } last := h.ft.LastSentTextTo(100) if !strings.Contains(last, "🔒") || !strings.Contains(last, "@arcodange_factory_bot") { t.Fatalf("should prompt /auth, got %q", last) } } func TestServer_GatedBot_Authed_HandlerRuns(t *testing.T) { h := newE2EHarness(t, "") h.authenticate(t, 100) upd := MakeUpdate(1, 100, 100, 1, "ping") status, _ := PostWebhook(t, h.routes(), "echobot", "echo-secret", upd) if status != 200 { t.Fatalf("status = %d, want 200", status) } if got := h.ft.LastSentTextTo(100); got != "ping" { t.Fatalf("authed user should get echo, got %q", got) } } func TestServer_PublicBot_NoAuthNeeded(t *testing.T) { h := newE2EHarness(t, "") upd := MakeUpdate(1, 100, 100, 1, "hi public") status, _ := PostWebhook(t, h.routes(), "publicbot", "pub-secret", upd) if status != 200 { t.Fatalf("status = %d, want 200", status) } if got := h.ft.LastSentTextTo(100); got != "hi public" { t.Fatalf("public bot must echo without auth, got %q", got) } } func TestServer_FactoryAuth_FullFlow(t *testing.T) { h := newE2EHarness(t, "") // Step 1 : /auth wrong → "Mauvais code" wrong := MakeUpdate(1, 100, 100, 11, "/auth wrong") _, _ = PostWebhook(t, h.routes(), "factory", "factory-secret", wrong) if got := h.ft.LastSentTextTo(100); !strings.Contains(got, "Mauvais") { t.Fatalf("step 1 wrong code reply : %q", got) } authed, _ := h.auth.IsAuthed(context.Background(), 100) if authed { t.Fatal("session should not exist after wrong code") } // Step 2 : /auth right → "Authentifié" + deleteMessage right := MakeUpdate(2, 100, 100, 22, "/auth "+testAuthCode) _, _ = PostWebhook(t, h.routes(), "factory", "factory-secret", right) if got := h.ft.LastSentTextTo(100); !strings.Contains(got, "Authentifié") { t.Fatalf("step 2 right code reply : %q", got) } deleted := h.ft.Deleted() if len(deleted) != 1 || deleted[0].MessageID != 22 { t.Fatalf("right code should deleteMessage 22, got %+v", deleted) } authed, _ = h.auth.IsAuthed(context.Background(), 100) if !authed { t.Fatal("session must exist after right code") } // Step 3 : the gated bot now responds normally ping := MakeUpdate(3, 100, 100, 33, "ping after auth") _, _ = PostWebhook(t, h.routes(), "echobot", "echo-secret", ping) if got := h.ft.LastSentTextTo(100); got != "ping after auth" { t.Fatalf("step 3 echo reply : %q", got) } // Step 4 : /logout closes the session logout := MakeUpdate(4, 100, 100, 44, "/logout") _, _ = PostWebhook(t, h.routes(), "factory", "factory-secret", logout) authed, _ = h.auth.IsAuthed(context.Background(), 100) if authed { t.Fatal("logout should drop the session") } // Step 5 : the gated bot is gated again again := MakeUpdate(5, 100, 100, 55, "after logout") _, _ = PostWebhook(t, h.routes(), "echobot", "echo-secret", again) if got := h.ft.LastSentTextTo(100); !strings.Contains(got, "🔒") { t.Fatalf("step 5 should be gated again, got %q", got) } } func TestServer_HealthAndReady(t *testing.T) { h := newE2EHarness(t, "") for _, path := range []string{"/healthz", "/readyz"} { req := httpRequest("GET", path, "") rr := httpRecorder() h.routes().ServeHTTP(rr, req) if rr.Code != 200 || rr.Body.String() != "OK" { t.Errorf("%s = %d %q", path, rr.Code, rr.Body.String()) } } }