package tg import ( "io" "log/slog" "net/http" "strings" "testing" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // probeClient подменяет клиента бота и запоминает, кого спрашивали. Через него // проверяется стык: скачивание обязано идти клиентом бота, а не общим // `http.DefaultClient` — чистку отказа от адреса с токеном несёт именно клиент // (`internal/adapter/telegram`). Подмена на общий клиент правил гейта не // нарушает, поэтому сторожить стык может только проверка. type probeClient struct { seen []string download func(w *probeResponse) } type probeResponse struct { status int body string } func (c *probeClient) Do(req *http.Request) (*http.Response, error) { c.seen = append(c.seen, req.URL.Path) switch { case strings.Contains(req.URL.Path, "/getMe"): return jsonResponse(`{"ok":true,"result":{"id":1,"is_bot":true,"username":"probe_bot"}}`), nil case strings.Contains(req.URL.Path, "/getFile"): return jsonResponse(`{"ok":true,"result":{"file_id":"x","file_path":"voice/file_1.ogg"}}`), nil } answer := &probeResponse{status: http.StatusOK, body: "аудио"} if c.download != nil { c.download(answer) } return &http.Response{ StatusCode: answer.status, Body: io.NopCloser(strings.NewReader(answer.body)), Header: make(http.Header), }, nil } func jsonResponse(body string) *http.Response { header := make(http.Header) header.Set("Content-Type", "application/json") return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: header, } } func newProbeController(t *testing.T, client *probeClient) *TelegramController { t.Helper() // Клиент подставной, поэтому адрес значения не имеет — важно лишь, что // библиотека соберёт из него разбираемый URL. bot, err := tgbotapi.NewBotAPIWithClient( "7654321:AAHsecretBOTtokenVALUE", "http://telegram.probe/bot%s/%s", client, ) require.NoError(t, err) return &TelegramController{ bot: bot, logger: slog.New(slog.DiscardHandler), } } // Скачивание идёт клиентом бота: иначе отказ пойдёт мимо чистки и унесёт токен. func TestDownloadGoesThroughBotClient(t *testing.T) { client := &probeClient{} controller := newProbeController(t, client) body, name, err := controller.downloadAudioFile(t.Context(), "file-id") require.NoError(t, err) t.Cleanup(func() { if err := body.Close(); err != nil { t.Errorf("не удалось закрыть тело: %v", err) } }) assert.Equal(t, "voice/file_1.ogg", name) require.Len(t, client.seen, 3, "клиент бота видел все обращения: getMe, getFile и скачивание") assert.Contains(t, client.seen[2], "voice/file_1.ogg", "скачивание ушло мимо клиента бота") } // Отказ выдачи файла — это не запись: тело такого ответа не должно доехать до // хранилища и умереть на `ffprobe`, уведя диагностику к чужой причине. func TestDownloadRejectsNonOKStatus(t *testing.T) { client := &probeClient{download: func(w *probeResponse) { w.status = http.StatusUnauthorized w.body = `{"ok":false,"error_code":401,"description":"Unauthorized"}` }} controller := newProbeController(t, client) body, _, err := controller.downloadAudioFile(t.Context(), "file-id") require.Error(t, err) assert.Nil(t, body, "тело отказа наружу не отдают") assert.Contains(t, err.Error(), "401", "код ответа назван — по нему видно, что отказал Telegram") }