Ctrl K
ComunidadGuías…

reverse_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
	}
}