reverse_test.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"image"
"image/png"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/go-telegram/bot/models"
"github.com/max-messenger/max-bot-api-client-go/v2/model"
)
func TestMaxTextOnlyAttachmentsDoNotWaitForMediaSlot(t *testing.T) {
for _, kind := range []model.AttachmentType{"", model.AttachShare, model.AttachSticker} {
t.Run(string(kind), func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
db := testStore(t)
src := &maxSource{channel: maxChat{id: -42}, to: []*models.ChatFullInfo{{ID: -100}}}
message := maxMessage{Body: maxMessageBody{Mid: "original", Text: "text"}, URL: "https://max.ru/channel/post"}
if kind != "" {
message.Body.Attachments = []maxMedia{{Attachment: model.Attachment{Type: kind}}}
}
var sent int
r := &relay{mediaSlot: make(chan struct{}, 1), tgAPI: telegramAPI{
baseURL: "https://telegram.example", token: "token", http: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
defer func() { _ = req.Body.Close() }()
var body struct {
Text string `json:"text"`
}
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, err
}
if !strings.HasPrefix(body.Text, "text") {
t.Errorf("message text lost: %q", body.Text)
}
sent++
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":{"message_id":11}}`)), Header: make(http.Header), Request: req}, nil
})},
}}
r.mediaSlot <- struct{}{}
book := &deliveryBook{store: db, source: channelKey{maxPlatform, -42}, post: "message:original"}
r.forwardMax(ctx, src, message, book)
if sent != 1 || book.err != nil || len(r.mediaSlot) != 1 {
t.Fatalf("text waited for another post's media: sent=%d, err=%v, occupied=%d", sent, book.err, len(r.mediaSlot))
}
})
}
}
func TestMaxWebhookPersistsAndRelaysAlbumOnce(t *testing.T) {
ctx := t.Context()
db := testStore(t)
src := &maxSource{channel: maxChat{id: -42, title: "MAX"}, to: []*models.ChatFullInfo{{ID: -100, Title: "Telegram"}}}
key := channelKey{maxPlatform, src.channel.id}
hook := maxWebhook("test-secret", db, map[int64]*maxSource{-42: src})
var pngBytes bytes.Buffer
if err := png.Encode(&pngBytes, image.NewGray(image.Rect(0, 0, 1, 1))); err != nil {
t.Fatal(err)
}
photo := pngBytes.Bytes()
video := []byte("\x00\x00\x00\x18ftypmp42\x00\x00\x00\x00mp42isom")
cdn := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/image" {
_, _ = w.Write(photo)
} else {
_, _ = w.Write(video)
}
}))
defer cdn.Close()
payload := fmt.Sprintf(`{
"update_type":"message_created",
"message":{"recipient":{"chat_type":"channel","chat_id":-42},
"body":{"mid":"mid.1","text":"🙂 test","markup":[{"type":"strong","from":3,"length":4}],
"attachments":[{"type":"image","payload":{"url":%q}},{"type":"video","payload":{"url":%q}}]}}
}`, cdn.URL+"/image", cdn.URL+"/video")
for i, secret := range []string{"wrong", "test-secret", "test-secret"} {
req := httptest.NewRequestWithContext(ctx, http.MethodPost, "/perepost/max", strings.NewReader(payload))
req.Header.Set("X-Max-Bot-Api-Secret", secret)
w := httptest.NewRecorder()
hook.ServeHTTP(w, req)
want := http.StatusOK
if i == 0 {
want = http.StatusUnauthorized
}
if w.Code != want {
t.Fatalf("webhook status %d, want %d", w.Code, want)
}
}
post, err := db.next(ctx, key, time.Now())
if err != nil || len(post.payloads) != 1 {
t.Fatalf("webhook not stored exactly once: %+v, %v", post, err)
}
var calls atomic.Int32
telegram := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
calls.Add(1)
if req.URL.Path != "/bottoken/sendMediaGroup" {
t.Errorf("method %s", req.URL.Path)
}
if err := req.ParseMultipartForm(1 << 20); err != nil {
t.Errorf("invalid multipart: %v", err)
return
}
defer func() { _ = req.MultipartForm.RemoveAll() }()
for i, want := range [][]byte{photo, video} {
file, _, err := req.FormFile(fileField(i))
if err != nil {
t.Error(err)
continue
}
contents, err := io.ReadAll(file)
_ = file.Close()
if err != nil || !bytes.Equal(contents, want) {
t.Errorf("file %d: %q, %v", i, contents, err)
}
}
var album []struct {
Type string `json:"type"`
Caption string `json:"caption"`
Entities []models.MessageEntity `json:"caption_entities"`
}
if err := json.Unmarshal([]byte(req.FormValue("media")), &album); err != nil || len(album) != 2 {
t.Errorf("album: %+v, %v", album, err)
} else if album[0].Type != "photo" || album[1].Type != "video" || album[0].Caption != "🙂 test" ||
len(album[0].Entities) != 1 || album[0].Entities[0].Offset != 3 || album[0].Entities[0].Type != models.MessageEntityTypeBold {
t.Errorf("album contents: %+v", album)
}
_, _ = io.WriteString(w, `{"ok":true,"result":[{"message_id":11},{"message_id":12}]}`)
}))
defer telegram.Close()
dir := t.TempDir()
r := &relay{tgAPI: telegramAPI{baseURL: telegram.URL, token: "token", http: telegram.Client()}, maxMediaHTTP: cdn.Client(),
mediaDir: dir, maxFileLimit: 1024, maxPostLimit: 2048, mediaSlot: make(chan struct{}, 1)}
var m maxMessage
if err := json.Unmarshal(post.payloads[0], &m); err != nil {
t.Fatal(err)
}
for range 2 {
book := &deliveryBook{store: db, source: key, post: post.key}
r.forwardMax(ctx, src, m, book)
if book.err != nil {
t.Fatal(book.err)
}
}
if calls.Load() != 1 {
t.Fatalf("published %d times", calls.Load())
}
if entries, err := os.ReadDir(dir); err != nil || len(entries) != 0 {
t.Fatalf("temporary files remain: %v, %v", entries, err)
}
if err := db.complete(ctx, key, post); err != nil {
t.Fatal(err)
}
if p, err := db.next(ctx, key, time.Now()); err != nil || p.row != 0 {
t.Fatalf("duplicate webhook in queue: %+v, %v", p, err)
}
var count int
if err := db.QueryRowContext(ctx, `SELECT count(*) FROM published WHERE channel = 'telegram:-100' AND mid IN ('11', '12')`).Scan(&count); err != nil || count != 2 {
t.Fatalf("missing album output IDs: %d, %v", count, err)
}
}
func TestTelegramAcknowledgesOnlyAfterPersisting(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
db := testStore(t)
key := channelKey{telegramPlatform, -100}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var payload struct {
Offset int64 `json:"offset"`
}
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
t.Error(err)
cancel()
return
}
switch payload.Offset {
case 0:
_, _ = io.WriteString(w, `{"ok":true,"result":[{"update_id":42,"channel_post":{"message_id":8,"chat":{"id":-100,"type":"channel"},"text":"example"}}]}`)
case 43:
p, err := db.next(req.Context(), key, time.Now())
if err != nil || p.key != "message:8" {
t.Errorf("acknowledging before persisting: %+v, %v", p, err)
}
cancel()
default:
t.Errorf("unexpected offset %d", payload.Offset)
cancel()
}
}))
defer server.Close()
api := telegramAPI{baseURL: server.URL, token: "token", http: server.Client()}
var lastPoll atomic.Int64
if err := api.poll(ctx, db, map[int64]*source{-100: {}}, []string{"channel_post"}, &lastPoll); err != nil {
t.Fatal(err)
}
}