tgbot: показывать запись матча метабазы (провайдер+id+ссылка)
В карточке подтверждения и уведомлении о готовности бот теперь показывает
запись матча метабазы — провайдер, id и кликабельную ссылку на страницу
записи, — как веб-страница /download/{id} и экран ревью. Так ошибочную
привязку видно и из Telegram.
Билдер URL записи (providerURL/matchURL) вынесен из internal/httpapi в ядро
internal/worker (worker.ProviderURL + метод (*ReviewData).MatchURL()), чтобы
оба транспорта строили ссылку одинаково; httpapi делегирует туда. baseLine
переведён на эффективные provider/id (с учётом ручных правок), URL в href
экранируется escHref (сверх esc закрывает кавычку — иначе изготовленный id
разорвал бы атрибут и Telegram отклонил бы сообщение). При отсутствии матча
карточка ревью показывает «нет матча», уведомление о готовности строку
опускает.
Capability notifications: ADDED «Показ записи матча метабазы» + MODIFIED
требования об экранировании (id матча и URL, контекст href).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||
)
|
||||
|
||||
// Карточка ревью показывает запись матча ссылкой на страницу записи (тот же
|
||||
// билдер URL, что и веб): provider+id внутри <a href>, стрелка ↗, экранировано.
|
||||
func TestReviewCard_ShowsMatchLink(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateReview) // tvdb/269613, сериал
|
||||
text, _ := b.renderCard(rd)
|
||||
|
||||
want := `<a href="https://www.thetvdb.com/dereferrer/series/269613">tvdb 269613 ↗</a>`
|
||||
if !strings.Contains(text, "База: "+want) {
|
||||
t.Errorf("нет ссылки матча в карточке ревью:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
// Запись матча берёт эффективные provider/id (с учётом ручных правок), а не
|
||||
// сырое распознавание — консистентно с веб-страницей загрузки.
|
||||
func TestReviewCard_UsesEffectiveProvider(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateReview)
|
||||
// Сырое распознавание — одно, эффективный выбор (ручная правка) — другое.
|
||||
rd.Recognition.Provider = store.NullString("tvdb")
|
||||
rd.Recognition.ProviderID = store.NullString("269613")
|
||||
rd.Provider, rd.ProviderID = "imdb", "tt0111161"
|
||||
text, _ := b.renderCard(rd)
|
||||
|
||||
if !strings.Contains(text, "imdb tt0111161") {
|
||||
t.Errorf("должен показываться эффективный провайдер:\n%s", text)
|
||||
}
|
||||
if strings.Contains(text, "tvdb") || strings.Contains(text, "269613") {
|
||||
t.Errorf("сырое распознавание не должно просачиваться:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
// Матч без строящегося URL (неизвестный провайдер) — текстом provider id, без
|
||||
// ссылки.
|
||||
func TestReviewCard_MatchWithoutURLAsText(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateReview)
|
||||
rd.Provider, rd.ProviderID = "kinopoisk", "42"
|
||||
text, _ := b.renderCard(rd)
|
||||
|
||||
if !strings.Contains(text, "База: kinopoisk 42") {
|
||||
t.Errorf("матч без URL должен быть текстом:\n%s", text)
|
||||
}
|
||||
if strings.Contains(text, "<a href") {
|
||||
t.Errorf("для несобираемого URL ссылки быть не должно:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
// Нет матча: карточка ревью показывает индикатор «нет матча».
|
||||
func TestReviewCard_NoMatchIndicator(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateReview)
|
||||
rd.Provider, rd.ProviderID = "none", ""
|
||||
text, _ := b.renderCard(rd)
|
||||
|
||||
if !strings.Contains(text, "База: нет матча") {
|
||||
t.Errorf("нет индикатора «нет матча»:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
// Уведомление о готовности содержит запись матча (база + ссылка) — чтобы
|
||||
// ошибочную привязку было видно после раскладки.
|
||||
func TestRenderDone_ShowsMatch(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateDone)
|
||||
text := b.renderDone(rd)
|
||||
|
||||
if !strings.Contains(text, "Готово") {
|
||||
t.Fatalf("не сообщение о готовности:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, `База: <a href="https://www.thetvdb.com/dereferrer/series/269613">tvdb 269613 ↗</a>`) {
|
||||
t.Errorf("нет строки матча в готовности:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
// Без матча уведомление о готовности строку матча опускает (в финальном пинге
|
||||
// «нет матча» — шум).
|
||||
func TestRenderDone_OmitsBaseWithoutMatch(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateDone)
|
||||
rd.Provider, rd.ProviderID = "none", ""
|
||||
text := b.renderDone(rd)
|
||||
|
||||
if strings.Contains(text, "База") {
|
||||
t.Errorf("без матча строки «База» в готовности быть не должно:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
// Кавычка в id (а значит в URL) экранируется в значении href ("), атрибут
|
||||
// остаётся целым — иначе Telegram отклонил бы сообщение (parse error).
|
||||
func TestReviewCard_QuoteInURLEscaped(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateReview)
|
||||
rd.Provider, rd.ProviderID = "imdb", `tt1"onmouseover=x`
|
||||
text, _ := b.renderCard(rd)
|
||||
|
||||
if strings.Contains(text, `="x`) || !strings.Contains(text, """) {
|
||||
t.Errorf("кавычка в href не экранирована:\n%s", text)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user