package telegram import ( "context" "errors" "net/http" "sync" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) func TestSendMessage_TextConstruction(t *testing.T) { t.Parallel() var mu sync.Mutex var body map[string]any a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if methodOf(r) != "sendMessage" { http.Error(w, "unexpected method: "+methodOf(r), http.StatusNotFound) return } mu.Lock() body = decodeBody(r) mu.Unlock() okResponse(w, map[string]any{"message_id": 100}) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } resp := adapter.UnifiedResponse{ ReplyTo: "42", Messages: []adapter.UnifiedMessage{ {ChannelID: "200", ChannelType: adapter.ChannelDirect, Content: "hello", ContentType: adapter.ContentText}, }, } if err := a.SendMessage(context.Background(), resp); err != nil { t.Fatalf("SendMessage: %v", err) } mu.Lock() defer mu.Unlock() if body["chat_id"] != "200" { t.Errorf("chat_id = %v, want 200", body["chat_id"]) } if body["text"] != "hello" { t.Errorf("text = %v, want hello", body["text"]) } if body["parse_mode"] != "Markdown" { t.Errorf("parse_mode = %v, want Markdown", body["parse_mode"]) } if body["reply_to_message_id"] != float64(42) { t.Errorf("reply_to_message_id = %v, want 42", body["reply_to_message_id"]) } } func TestSendMessage_ImageByFileID(t *testing.T) { t.Parallel() var mu sync.Mutex var body map[string]any var method string a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m := methodOf(r) if m != "sendPhoto" { http.Error(w, "unexpected method: "+m, http.StatusNotFound) return } mu.Lock() method = m body = decodeBody(r) mu.Unlock() okResponse(w, map[string]any{"message_id": 101}) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } resp := adapter.UnifiedResponse{ Messages: []adapter.UnifiedMessage{ { ChannelID: "200", ChannelType: adapter.ChannelDirect, Content: "a caption", ContentType: adapter.ContentImage, Media: []adapter.Media{{Kind: adapter.MediaImage, URL: "file-abc"}}, }, }, } if err := a.SendMessage(context.Background(), resp); err != nil { t.Fatalf("SendMessage: %v", err) } mu.Lock() defer mu.Unlock() if method != "sendPhoto" { t.Errorf("method = %q, want sendPhoto", method) } if body["chat_id"] != "200" || body["photo"] != "file-abc" { t.Errorf("body = %v, want chat_id 200 and photo file-abc", body) } if body["caption"] != "a caption" || body["parse_mode"] != "Markdown" { t.Errorf("caption/parse_mode = %v/%v", body["caption"], body["parse_mode"]) } } func TestSendMessage_ImageNoFileIDFallsBackToText(t *testing.T) { t.Parallel() var mu sync.Mutex var method string a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m := methodOf(r) mu.Lock() method = m mu.Unlock() if m != "sendMessage" { http.Error(w, "unexpected method: "+m, http.StatusNotFound) return } okResponse(w, map[string]any{"message_id": 102}) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } resp := adapter.UnifiedResponse{ Messages: []adapter.UnifiedMessage{ {ChannelID: "200", ContentType: adapter.ContentImage, Content: "no file_id so text"}, }, } if err := a.SendMessage(context.Background(), resp); err != nil { t.Fatalf("SendMessage: %v", err) } mu.Lock() defer mu.Unlock() if method != "sendMessage" { t.Errorf("method = %q, want fallback sendMessage", method) } } func TestSendMessage_VoiceAndDocument(t *testing.T) { t.Parallel() for _, tc := range []struct { name string ct adapter.ContentType media adapter.Media wantMethod string wantField string }{ {name: "voice", ct: adapter.ContentVoice, media: adapter.Media{Kind: adapter.MediaVoice, URL: "voice-1"}, wantMethod: "sendVoice", wantField: "voice"}, {name: "document", ct: adapter.ContentFile, media: adapter.Media{Kind: adapter.MediaFile, URL: "doc-1", Name: "x.pdf"}, wantMethod: "sendDocument", wantField: "document"}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() var mu sync.Mutex var body map[string]any var method string a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m := methodOf(r) mu.Lock() method = m body = decodeBody(r) mu.Unlock() if m != tc.wantMethod { http.Error(w, "unexpected method: "+m, http.StatusNotFound) return } okResponse(w, map[string]any{"message_id": 103}) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } resp := adapter.UnifiedResponse{ Messages: []adapter.UnifiedMessage{ {ChannelID: "200", ChannelType: adapter.ChannelDirect, ContentType: tc.ct, Media: []adapter.Media{tc.media}}, }, } if err := a.SendMessage(context.Background(), resp); err != nil { t.Fatalf("SendMessage: %v", err) } mu.Lock() defer mu.Unlock() if method != tc.wantMethod { t.Errorf("method = %q, want %q", method, tc.wantMethod) } field := tc.media.URL if body[tc.wantField] != field { t.Errorf("%s = %v, want %v", tc.wantField, body[tc.wantField], field) } }) } } func TestSendProactive(t *testing.T) { t.Parallel() var mu sync.Mutex var body map[string]any a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if methodOf(r) != "sendMessage" { http.Error(w, "unexpected method", http.StatusNotFound) return } mu.Lock() body = decodeBody(r) mu.Unlock() okResponse(w, map[string]any{"message_id": 104}) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } msg := adapter.UnifiedMessage{ ChannelID: "300", ChannelType: adapter.ChannelDirect, Content: "reminder", ContentType: adapter.ContentText, } if err := a.SendProactive(context.Background(), msg); err != nil { t.Fatalf("SendProactive: %v", err) } mu.Lock() defer mu.Unlock() if body["chat_id"] != "300" || body["text"] != "reminder" { t.Errorf("body = %v, want chat_id 300 text reminder", body) } if _, ok := body["reply_to_message_id"]; ok { t.Errorf("proactive message should not carry reply_to_message_id, got %v", body) } } func TestSendMessage_NotConnected(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"}) err := a.SendMessage(context.Background(), adapter.UnifiedResponse{}) if !errors.Is(err, adapter.ErrNotConnected) { t.Fatalf("err = %v, want ErrNotConnected", err) } if err := a.SendProactive(context.Background(), adapter.UnifiedMessage{}); !errors.Is(err, adapter.ErrNotConnected) { t.Fatalf("SendProactive err = %v, want ErrNotConnected", err) } } func TestSendMessage_NoTargetChat(t *testing.T) { t.Parallel() a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "unexpected method", http.StatusNotFound) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } err := a.SendMessage(context.Background(), adapter.UnifiedResponse{ Messages: []adapter.UnifiedMessage{{ContentType: adapter.ContentText, Content: "hi"}}, }) if !errors.Is(err, ErrNoChatTarget) { t.Fatalf("err = %v, want ErrNoChatTarget", err) } } func TestSendMessage_APIError(t *testing.T) { t.Parallel() a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if methodOf(r) != "sendMessage" { http.Error(w, "unexpected method", http.StatusNotFound) return } failResponse(w, "Bad Request: chat not found", 400) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } err := a.SendMessage(context.Background(), adapter.UnifiedResponse{ Messages: []adapter.UnifiedMessage{{ChannelID: "999", ContentType: adapter.ContentText, Content: "hi"}}, }) if !errors.Is(err, ErrBadResponse) { t.Fatalf("err = %v, want ErrBadResponse", err) } } func TestProcessUpdate_DeliversToHandler(t *testing.T) { t.Parallel() a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "unexpected method", http.StatusNotFound) })) if err := a.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } var mu sync.Mutex var got *adapter.UnifiedMessage a.SetMessageHandler(func(ctx context.Context, m adapter.UnifiedMessage) { mu.Lock() got = &m mu.Unlock() }) raw := []byte(`{"update_id":1,"message":{"message_id":5,"from":{"id":1,"first_name":"Ada"},"chat":{"id":10,"type":"private"},"date":1700000000,"text":"webhook hi"}}`) n, err := a.ProcessUpdate(context.Background(), raw) if err != nil { t.Fatalf("ProcessUpdate: %v", err) } if n != 1 { t.Errorf("delivered %d messages, want 1", n) } mu.Lock() defer mu.Unlock() if got == nil { t.Fatal("handler was not called") } if got.Content != "webhook hi" || got.Platform != "telegram" { t.Errorf("message = %+v", got) } } func TestProcessUpdate_NonMessageIgnored(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"}) raw := []byte(`{"update_id":2,"callback_query":{"id":"cq","from":{"id":1},"data":"btn"}}`) n, err := a.ProcessUpdate(context.Background(), raw) if err != nil { t.Fatalf("ProcessUpdate: %v", err) } if n != 0 { t.Errorf("delivered %d messages, want 0 for callback query", n) } } func TestProcessUpdate_InvalidJSON(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"}) if _, err := a.ProcessUpdate(context.Background(), []byte("{not json")); err == nil { t.Fatal("expected error decoding invalid JSON") } }