package telegram import ( "context" "errors" "net/http" "sync" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) func TestName(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"}) if got := a.Name(); got != "telegram" { t.Fatalf("Name() = %q, want telegram", got) } } func TestConfig(t *testing.T) { t.Parallel() a := New(Config{BotToken: "abc123", WebhookURL: "https://example.com/hook"}) got := a.Config() if got["bot_token"] != "abc123" { t.Fatalf("bot_token = %v, want abc123", got["bot_token"]) } if got["webhook_url"] != "https://example.com/hook" { t.Fatalf("webhook_url = %v, want https://example.com/hook", got["webhook_url"]) } // Round-trip through the shared decoder to prove the map is decodable. var round Config if err := adapter.DecodeConfig(got, &round); err != nil { t.Fatalf("DecodeConfig: %v", err) } if round.BotToken != "abc123" || round.WebhookURL != "https://example.com/hook" { t.Fatalf("round trip mismatch: %+v", round) } } func TestCapabilities(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"}) caps := a.Capabilities() if !caps.Proactive { t.Error("Proactive should be true for telegram") } if !caps.Markdown || !caps.Image || !caps.Voice { t.Errorf("expected Markdown/Image/Voice supported, got %+v", caps) } if caps.Reaction || caps.TypingHint { t.Errorf("expected Reaction/TypingHint unsupported, got %+v", caps) } if caps.MaxMessageLength != 4096 { t.Errorf("MaxMessageLength = %d, want 4096", caps.MaxMessageLength) } if caps.RecommendBurstMax <= 0 { t.Errorf("RecommendBurstMax should be positive, got %d", caps.RecommendBurstMax) } } func TestListChannels_Empty(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"}) got, err := a.ListChannels(context.Background()) if err != nil { t.Fatalf("ListChannels: %v", err) } if got == nil || len(got) != 0 { t.Fatalf("expected empty non-nil slice, got %#v", got) } } func TestConnect_MissingToken(t *testing.T) { t.Parallel() a := New(Config{}) err := a.Connect(context.Background()) if !errors.Is(err, ErrMissingToken) { t.Fatalf("err = %v, want ErrMissingToken", err) } if a.IsConnected() { t.Fatal("should not be connected") } } func TestConnect_WebhookIdempotent(t *testing.T) { t.Parallel() var mu sync.Mutex calls := 0 a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if methodOf(r) != "setWebhook" { http.Error(w, "unexpected method", http.StatusNotFound) return } mu.Lock() calls++ mu.Unlock() okResponse(w, map[string]any{"webhook_url": "https://example.com/hook"}) })) a.cfg.WebhookURL = "https://example.com/hook" if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if !a.IsConnected() { t.Fatal("expected connected") } if err := a.Connect(context.Background()); err != nil { t.Fatalf("second Connect should be idempotent: %v", err) } mu.Lock() if calls != 1 { t.Fatalf("expected 1 setWebhook call, got %d", calls) } mu.Unlock() } func TestConnect_PollingMode(t *testing.T) { t.Parallel() a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "no api call expected in poll mode", http.StatusNotFound) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if !a.IsConnected() { t.Fatal("expected connected") } if a.isWebhookMode() { t.Fatal("expected polling mode") } } func TestDisconnect_Idempotent(t *testing.T) { t.Parallel() a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "no api call expected", http.StatusNotFound) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if err := a.Disconnect(context.Background()); err != nil { t.Fatalf("Disconnect: %v", err) } if a.IsConnected() { t.Fatal("expected disconnected") } if err := a.Disconnect(context.Background()); err != nil { t.Fatalf("second Disconnect should be idempotent: %v", err) } } func TestClose_Terminal(t *testing.T) { t.Parallel() a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "no api call expected", http.StatusNotFound) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if err := a.Close(); err != nil { t.Fatalf("Close: %v", err) } if a.IsConnected() { t.Fatal("expected disconnected after close") } if err := a.Connect(context.Background()); !errors.Is(err, adapter.ErrClosed) { t.Fatalf("Connect after close err = %v, want ErrClosed", err) } if err := a.HealthCheck(context.Background()); !errors.Is(err, adapter.ErrClosed) { t.Fatalf("HealthCheck after close err = %v, want ErrClosed", err) } } func TestHealthCheck_OK(t *testing.T) { t.Parallel() a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if methodOf(r) != "getMe" { http.Error(w, "unexpected method", http.StatusNotFound) return } okResponse(w, map[string]any{"id": 123, "username": "cyrene_bot"}) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if err := a.HealthCheck(context.Background()); err != nil { t.Fatalf("HealthCheck: %v", err) } } func TestHealthCheck_NotConnected(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"}) err := a.HealthCheck(context.Background()) if !errors.Is(err, adapter.ErrNotConnected) { t.Fatalf("err = %v, want ErrNotConnected", err) } } func TestHealthCheck_APIError(t *testing.T) { t.Parallel() a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if methodOf(r) != "getMe" { http.Error(w, "unexpected method", http.StatusNotFound) return } failResponse(w, "Unauthorized", 401) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } err := a.HealthCheck(context.Background()) if !errors.Is(err, ErrBadResponse) { t.Fatalf("err = %v, want ErrBadResponse", err) } }