telegram: сервис поднимается без бота и работает одним входом
- Клиент бота собирается один раз и достаётся отправителю и транспорту; разрез прошёл по «ответил ли Telegram»: ответ «такого бота нет» роняет старт, недоступность даёт подъём без Telegram (ADR-2026-08-13). Ожидание при сборке ограничено сроком — иначе молчащий Telegram вешал подъём. - Недоставленный ответ не роняет шаг: пишется с job_id и считается метрикой, уровень по причине — WARN для неподнятого входа, ERROR для неназванного адресата. Заведены transcriber_intake_up и transcriber_undelivered_reply_count. - Закрыта утечка токена в журнал: отказ разбора адреса рождается раньше обращения к клиенту, то есть мимо чистки на его границе.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||
)
|
||||
|
||||
// AbsentMessageSender подставляется вместо отправителя Telegram, когда токен
|
||||
// бота не задан и клиента заводить не из чего. Он ничего не отправляет и на
|
||||
// всякий ответ отдаёт `contract.ErrDeliveryChannelDown`.
|
||||
//
|
||||
// Заглушка, а не пустой отправитель: необязательная зависимость, доехавшая до
|
||||
// ядра нулём, роняет процесс на первой же задаче из Telegram, а проверка на
|
||||
// месте употребления завела бы в ядре знание о том, как собран сервис.
|
||||
//
|
||||
// Молчит он намеренно. Записать недоставку заглушке нечем: контракт отправки
|
||||
// несёт текст, чат и сообщение для ответа, а идентификатора задачи в нём нет.
|
||||
// Пишет поэтому шаг конвейера, который задачу знает.
|
||||
type AbsentMessageSender struct{}
|
||||
|
||||
func NewAbsentMessageSender() *AbsentMessageSender {
|
||||
return &AbsentMessageSender{}
|
||||
}
|
||||
|
||||
func (s *AbsentMessageSender) Send(_ string, _ int64, _ *int) error {
|
||||
return contract.ErrDeliveryChannelDown
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||
)
|
||||
|
||||
// Заглушка отдаёт «канал не поднят» и молчит: записать недоставку ей нечем —
|
||||
// идентификатора задачи контракт отправки не несёт, и пишет её шаг конвейера.
|
||||
func TestAbsentSenderReportsChannelDown(t *testing.T) {
|
||||
sender := NewAbsentMessageSender()
|
||||
|
||||
err := sender.Send("расшифровка записи", 100, nil)
|
||||
|
||||
require.ErrorIs(t, err, contract.ErrDeliveryChannelDown)
|
||||
}
|
||||
|
||||
// Непустой годный токен по-прежнему даёт настоящего отправителя: прежний путь
|
||||
// сохранён, и меняется только то, что клиента теперь отдают готовым.
|
||||
func TestSenderIsBuiltFromLiveBot(t *testing.T) {
|
||||
bot, _ := newProbeBot(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
if _, err := w.Write([]byte(getMeResponse)); err != nil {
|
||||
t.Errorf("подставной Telegram не смог ответить: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
sender := NewTelegramMessageSender(bot, slog.New(slog.DiscardHandler))
|
||||
|
||||
require.NotNil(t, sender)
|
||||
assert.Same(t, bot, sender.bot, "отправитель говорит с тем же клиентом, что и транспорт")
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
@@ -43,9 +44,35 @@ func newBot(token, endpoint string, logger *slog.Logger) (*tgbotapi.BotAPI, erro
|
||||
return nil, fmt.Errorf("failed to set telegram logger: %w", err)
|
||||
}
|
||||
|
||||
return tgbotapi.NewBotAPIWithClient(token, endpoint, &safeClient{inner: &http.Client{}})
|
||||
// Сборка ходит за `getMe` и стоит на пути старта — раньше HTTP-сервера,
|
||||
// панели и воркеров. Без срока ожидания молчащий Telegram (соединение
|
||||
// принято, ответа нет) вешал бы весь подъём бессрочно: порт не слушается,
|
||||
// проба здоровья не отвечает, а в журнале ни строки.
|
||||
probe := &safeClient{inner: &http.Client{Timeout: ProbeTimeout}}
|
||||
|
||||
// Отказ конструктора чистится здесь, а не клиентом: адрес собирается
|
||||
// строкой с токеном внутри, и `http.NewRequest` падает на его разборе
|
||||
// **до** обращения к клиенту — то есть мимо `safeClient`. Токен с
|
||||
// управляющим символом или неверной `%`-последовательностью иначе уезжает
|
||||
// в журнал целиком: перенос строки в конце значения ловится так же.
|
||||
bot, err := tgbotapi.NewBotAPIWithClient(token, endpoint, probe)
|
||||
if err != nil {
|
||||
return nil, WithoutURL(err)
|
||||
}
|
||||
|
||||
// Дальше живёт длинный опрос, и срок ему не нужен: он ждёт обновлений
|
||||
// столько, сколько задано настройкой, и клиент со сроком рвал бы его.
|
||||
bot.Client = &safeClient{inner: &http.Client{}}
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// ProbeTimeout — сколько ждём Telegram при сборке клиента. Число выбрано
|
||||
// решением, а не замером: одно обращение за `getMe` укладывается в доли
|
||||
// секунды, а десять секунд — потолок, после которого Telegram считается
|
||||
// недоступным и сервис поднимается без него.
|
||||
const ProbeTimeout = 10 * time.Second
|
||||
|
||||
// safeClient — клиент, чей отказ не несёт адреса. Библиотека объявляет
|
||||
// зависимость интерфейсом `HTTPClient` и возвращает наш отказ вызывающему
|
||||
// нетронутым, поэтому чистка отсюда доходит до каждого вызова Bot API.
|
||||
|
||||
@@ -63,6 +63,27 @@ func TestBotAPIFailureDoesNotCarryToken(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// Токен, ломающий разбор адреса, — второй путь отказа конструктора, и до
|
||||
// недавнего он был открыт: `http.NewRequest` падает раньше обращения к клиенту,
|
||||
// то есть мимо чистки на его границе. Так выглядит перенос строки, приехавший
|
||||
// с секретом из шаблона выкладки, и невычищенная `%`-последовательность.
|
||||
func TestBotConstructionFailureOnUnparsableTokenDoesNotCarryToken(t *testing.T) {
|
||||
broken := map[string]string{
|
||||
"перенос строки": probeToken + "\n",
|
||||
"негодная escape-пара": "7654321:AAH%zzSECRETtokenVALUE",
|
||||
}
|
||||
|
||||
for name, token := range broken {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := newBot(token, tgbotapi.APIEndpoint, slog.New(slog.DiscardHandler))
|
||||
|
||||
require.Error(t, err)
|
||||
assert.NotContains(t, err.Error(), token, "токен уехал в отказ: %v", err)
|
||||
assert.NotContains(t, err.Error(), "api.telegram.org", "адрес остался в отказе: %v", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Отказ конструктора несёт тот же путь: `NewBotAPIWithClient` ходит за `getMe`,
|
||||
// и контейнер, стартующий раньше сети, печатал бы токен в первую же секунду.
|
||||
func TestBotConstructionFailureDoesNotCarryToken(t *testing.T) {
|
||||
@@ -92,10 +113,21 @@ func TestLibraryLoggerRedactsToken(t *testing.T) {
|
||||
}
|
||||
|
||||
// Пустой токен — законный исход подъёма без Telegram, и узнаётся он по смыслу.
|
||||
// Обратное тоже нормируется: отказ негодного токена не должен читаться как
|
||||
// отказ от входа, иначе сборка при старте подставит заглушку там, где нужен
|
||||
// отказ, и молча потеряет бота.
|
||||
func TestEmptyTokenIsRecognizedByValue(t *testing.T) {
|
||||
_, err := NewBot("", slog.New(slog.DiscardHandler))
|
||||
|
||||
require.ErrorIs(t, err, ErrEmptyToken)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
server.Close()
|
||||
|
||||
_, err = newBot(probeToken, server.URL+"/bot%s/%s", slog.New(slog.DiscardHandler))
|
||||
|
||||
require.Error(t, err)
|
||||
require.NotErrorIs(t, err, ErrEmptyToken)
|
||||
}
|
||||
|
||||
// WithoutURL снимает адрес, но не причину: `errors.Is` по цепочке продолжает
|
||||
|
||||
@@ -15,18 +15,15 @@ type TelegramMessageSender struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewTelegramMessageSender(botToken string, logger *slog.Logger) (*TelegramMessageSender, error) {
|
||||
// Клиент заводится единой точкой: её отказ не несёт токена, а отказ
|
||||
// конструктора несёт — `NewBotAPI` зовёт `getMe`.
|
||||
bot, err := NewBot(botToken, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NewTelegramMessageSender принимает готового клиента, а не токен. Клиента
|
||||
// заводит сборка при старте — одного на отправителя и на транспорт бота: пока
|
||||
// его строили здесь и там порознь, два пути одного старта разошлись в том,
|
||||
// терпеть ли негодный токен, и согласовывать их приходилось руками.
|
||||
func NewTelegramMessageSender(bot *tgbotapi.BotAPI, logger *slog.Logger) *TelegramMessageSender {
|
||||
return &TelegramMessageSender{
|
||||
bot: bot,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TelegramMessageSender) Send(text string, chatId int64, replyToMessageId *int) error {
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
package contract
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrDeliveryChannelDown — канал, которым отвечают отправителю, не поднят.
|
||||
// Отдаётся отправителем-заглушкой, которого получает ядро, когда вход не
|
||||
// настроен.
|
||||
//
|
||||
// Значение сентинельное, а не тип: соседям по ряду есть что нести — состояние,
|
||||
// идентификатор задачи, — а этому нечего. Заглушка не знает ни задачи, ни чата,
|
||||
// и запись о недоставке делает шаг, у которого задача под рукой.
|
||||
var ErrDeliveryChannelDown = errors.New("delivery channel is down")
|
||||
|
||||
type JobNotFoundError struct {
|
||||
State string
|
||||
|
||||
@@ -328,9 +328,3 @@ func (c *TelegramController) isAudioDocument(document *tgbotapi.Document) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type EmptyBotTokenError struct{}
|
||||
|
||||
func (e *EmptyBotTokenError) Error() string {
|
||||
return "telegram bot token is empty"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,28 @@ var (
|
||||
[]string{"source_format", "target_format", "error"},
|
||||
)
|
||||
|
||||
// Поднят ли вход приёма. Единственный канал наблюдения, автоматизированный
|
||||
// у владельца: потерянный вход иначе виден только строкой журнала при
|
||||
// старте, а проба здоровья отвечает «ok» и без него.
|
||||
IntakeUpGauge = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "transcriber_intake_up",
|
||||
Help: "Whether an intake channel is up (1) or not (0)",
|
||||
},
|
||||
[]string{"channel"},
|
||||
)
|
||||
|
||||
// Ответы, которые не удалось доставить отправителю. Работа при этом
|
||||
// сделана, шаг отказа не объявляет, и без счётчика недоставка видна только
|
||||
// в журнале — до его ротации.
|
||||
UndeliveredReplyCounter = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "transcriber_undelivered_reply_count",
|
||||
Help: "Count of replies that could not be delivered to the sender",
|
||||
},
|
||||
[]string{"reason"},
|
||||
)
|
||||
|
||||
// Размер файла после конвертации (в байтах)
|
||||
OutputFileSizeHistogram = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
|
||||
@@ -551,19 +551,43 @@ func (s *TranscribeService) failJob(job *entity.TranscribeJob, holder string, jo
|
||||
return s.send(job, errorMessage)
|
||||
}
|
||||
|
||||
// send отвечает отправителю там, откуда пришла запись, и отказ отправки
|
||||
// поднимает вверх: он принадлежит шагу.
|
||||
// send отвечает отправителю там, откуда пришла запись. Отказ отправки поднимает
|
||||
// вверх: он принадлежит шагу.
|
||||
//
|
||||
// Кроме недоставки — её шаг записывает и завершается без отказа. Ответ уходит
|
||||
// после того, как достигнутое состояние сохранено: работа к этой минуте
|
||||
// сделана, и объявленный отказ засчитался бы воркеру сбоем и лёг бы владельцу
|
||||
// записью отказа. Повтор делу не помогает — ни бот, ни адресат от ожидания не
|
||||
// появятся, — поэтому причина недоставки живёт в журнале, а не в состоянии
|
||||
// задачи.
|
||||
//
|
||||
// Служебные поля завершённой задачи отказ бы при этом не переписал: переход в
|
||||
// терминальное состояние снимает захват, и повторная запись натыкается на
|
||||
// «захват потерян». Довод держится на счётчике и журнале, а не на этом.
|
||||
func (s *TranscribeService) send(job *entity.TranscribeJob, text string) error {
|
||||
if job.Source != entity.SourceTelegram {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Адресата у задачи нет: отвечать некуда, и повторять нечего. Уровень здесь
|
||||
// выше, чем у неподнятого канала, и это не педантизм: пустой чат у задачи
|
||||
// из Telegram — симптом порчи записи, а самый коварный её источник назван
|
||||
// инвариантом «колонки очереди правятся в четырёх местах». Утони этот
|
||||
// сигнал в одном ряду со штатным «бот не настроен» — и обнуление колонки
|
||||
// заметит только отправитель, переставший получать ответы.
|
||||
if job.TgChatId == nil {
|
||||
s.logger.Error("Telegram chat not specified", "job_id", job.Id)
|
||||
return fmt.Errorf("tg chat id not specified, job id: %s", job.Id)
|
||||
s.undelivered(job, slog.LevelError, "chat is not specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.tgSender.Send(text, *job.TgChatId, job.TgReplyMessageId); err != nil {
|
||||
// Канал не поднят: сервис работает без этого входа, и это объявленный
|
||||
// режим, а не поломка.
|
||||
if errors.Is(err, contract.ErrDeliveryChannelDown) {
|
||||
s.undelivered(job, slog.LevelWarn, "delivery channel is down")
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Error("Failed to sent message to client", "job_id", job.Id)
|
||||
return fmt.Errorf("failed to sent message to client, job id: %s, err: %w", job.Id, err)
|
||||
}
|
||||
@@ -571,6 +595,31 @@ func (s *TranscribeService) send(job *entity.TranscribeJob, text string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// undelivered записывает недоставленный ответ и считает его в метрику. Уровень
|
||||
// приходит от причины: объявленный режим — «может стать проблемой», порча
|
||||
// записи — событие для разбора.
|
||||
//
|
||||
// Идентификатор задачи обязателен, иначе владелец видит, что ответ не ушёл, но
|
||||
// не может найти, чей; текста ответа в записи нет — он содержимое чужой записи.
|
||||
//
|
||||
// Счётчик нужен потому, что журнал контейнера живёт до ротации, а вопрос «кому
|
||||
// не ответили за последние сутки» задают позже.
|
||||
func (s *TranscribeService) undelivered(job *entity.TranscribeJob, level slog.Level, reason string) {
|
||||
metrics.UndeliveredReplyCounter.WithLabelValues(reason).Inc()
|
||||
|
||||
// Уровень выбирается ветвлением, а не передачей контекста: контекст здесь
|
||||
// брать неоткуда — ответ идёт после сохранения состояния, — а выдуманный
|
||||
// `context.Background()` соврал бы про отмену и цеплялся бы правилами.
|
||||
switch level {
|
||||
case slog.LevelError:
|
||||
s.logger.Error(undeliveredMessage, "job_id", job.Id, "reason", reason)
|
||||
default:
|
||||
s.logger.Warn(undeliveredMessage, "job_id", job.Id, "reason", reason)
|
||||
}
|
||||
}
|
||||
|
||||
const undeliveredMessage = "Reply was not delivered"
|
||||
|
||||
// notify отвечает отправителю там, где поднимать отказ некуда: задача уже
|
||||
// доведена до конца, и отказ отправки остаётся записью в журнале владельца.
|
||||
func (s *TranscribeService) notify(job *entity.TranscribeJob, text string) {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
)
|
||||
|
||||
// Ответ отправителю уходит после того, как достигнутое состояние сохранено.
|
||||
// Значит, недоставка не может быть отказом шага: объявленный отказ засчитался
|
||||
// бы воркеру сбоем, лёг бы владельцу записью отказа и переписал бы служебные
|
||||
// поля завершённой задачи. Причин недоставки две, исход у них общий.
|
||||
|
||||
// downSender изображает неподнятый канал доставки: так ведёт себя заглушка,
|
||||
// которую ядро получает вместо отправителя Telegram.
|
||||
type downSender struct {
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *downSender) Send(string, int64, *int) error {
|
||||
s.calls++
|
||||
return contract.ErrDeliveryChannelDown
|
||||
}
|
||||
|
||||
// journalEnv пересобирает сервис с названным отправителем и своим журналом:
|
||||
// утверждения судят и состояние задачи, и то, что увидел владелец.
|
||||
func journalEnv(
|
||||
t *testing.T,
|
||||
env *pipelineEnv,
|
||||
rec contract.AudioRecognizer,
|
||||
sender contract.TelegramMessageSender,
|
||||
) (*TranscribeService, *bytes.Buffer) {
|
||||
t.Helper()
|
||||
|
||||
journal := &bytes.Buffer{}
|
||||
svc := NewTranscribeService(
|
||||
env.jobRepo,
|
||||
env.fileRepo,
|
||||
&okMetaViewer{},
|
||||
&failingConverter{},
|
||||
rec,
|
||||
sender,
|
||||
slog.New(slog.NewTextHandler(journal, &slog.HandlerOptions{Level: slog.LevelDebug})),
|
||||
)
|
||||
|
||||
return svc, journal
|
||||
}
|
||||
|
||||
// Канал не поднят: задача доводится до конца, шаг отказа не объявляет, а
|
||||
// владелец узнаёт о недоставке из журнала.
|
||||
func TestUndeliveredOnDownChannelKeepsJobDone(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
|
||||
rec := &scriptedRecognizer{result: entity.NewInProgressResult()}
|
||||
job := transcribingJob(t, env, rec)
|
||||
|
||||
rec.result = entity.NewCompletedResult()
|
||||
rec.text = "расшифровка записи"
|
||||
sender := &downSender{}
|
||||
svc, journal := journalEnv(t, env, rec, sender)
|
||||
|
||||
// Шаг завершается без отказа — именно это воркер считает в свой счётчик.
|
||||
require.NoError(t, svc.FindAndRunTranscribeCheckJob(t.Context()))
|
||||
|
||||
assert.Equal(t, 1, sender.calls, "ответ до отправителя доехал")
|
||||
|
||||
after, err := env.jobRepo.GetByID(job.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, entity.StateDone, after.State, "задача осталась в достигнутом состоянии")
|
||||
require.NotNil(t, after.TranscriptionText)
|
||||
assert.Equal(t, "расшифровка записи", *after.TranscriptionText, "расшифровка сохранена")
|
||||
assert.Nil(t, after.ErrorText, "отказ задаче не приписан")
|
||||
|
||||
written := journal.String()
|
||||
assert.Contains(t, written, "Reply was not delivered", "недоставка названа")
|
||||
assert.Contains(t, written, job.Id, "запись несёт идентификатор задачи")
|
||||
assert.Contains(t, written, "level=WARN", "объявленный режим — «может стать проблемой»")
|
||||
assert.NotContains(t, written, "расшифровка записи", "текста расшифровки в журнале нет")
|
||||
}
|
||||
|
||||
// Адресат у задачи не назван: исход тот же. Прежде эта ветка объявляла отказ
|
||||
// шага на уже завершённой работе.
|
||||
func TestUndeliveredWithoutChatKeepsJobDone(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
|
||||
rec := &scriptedRecognizer{result: entity.NewInProgressResult()}
|
||||
job := transcribingJob(t, env, rec)
|
||||
|
||||
// Задача из Telegram, у которой чат не назван: такую отдаёт правка в панели.
|
||||
// Колонка чистится мимо захвата — иначе setup унёс бы задачу у шага.
|
||||
record, err := env.app.FindRecordById(migrations.JobsCollection, job.Id)
|
||||
require.NoError(t, err)
|
||||
record.Set("tg_chat_id", nil)
|
||||
require.NoError(t, env.app.Save(record))
|
||||
|
||||
rec.result = entity.NewCompletedResult()
|
||||
rec.text = "расшифровка записи"
|
||||
sender := &downSender{}
|
||||
svc, journal := journalEnv(t, env, rec, sender)
|
||||
|
||||
require.NoError(t, svc.FindAndRunTranscribeCheckJob(t.Context()))
|
||||
|
||||
assert.Equal(t, 0, sender.calls, "до отправителя дело не дошло: адресата нет")
|
||||
|
||||
after, err := env.jobRepo.GetByID(job.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, entity.StateDone, after.State)
|
||||
assert.Nil(t, after.ErrorText, "отказ задаче не приписан")
|
||||
|
||||
written := journal.String()
|
||||
assert.Contains(t, written, "Reply was not delivered")
|
||||
assert.Contains(t, written, job.Id)
|
||||
assert.Contains(t, written, "chat is not specified", "причина названа")
|
||||
assert.Contains(t, written, "level=ERROR",
|
||||
"порча записи громче штатного «бот не настроен»: иначе сигнал утонет")
|
||||
}
|
||||
|
||||
// Запись, принятая по HTTP, до отправителя не доходит вовсе: недоставки нет, и
|
||||
// записи о ней в журнале быть не должно — иначе журнал владельца заполнят
|
||||
// строки о задачах основного входа.
|
||||
func TestApiJobDoesNotReachSenderAndLogsNothing(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
|
||||
|
||||
job, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "voice.ogg")
|
||||
require.NoError(t, err)
|
||||
|
||||
sender := &downSender{}
|
||||
svc, journal := journalEnv(t, env, &scriptedRecognizer{}, sender)
|
||||
|
||||
require.NoError(t, svc.send(job, "расшифровка записи"))
|
||||
|
||||
assert.Equal(t, 0, sender.calls, "отправителя не звали")
|
||||
assert.NotContains(t, journal.String(), "Reply was not delivered", "недоставки не было")
|
||||
}
|
||||
Reference in New Issue
Block a user