Files

265 lines
8.1 KiB
Go

package tgbot
import (
"context"
"io"
"log/slog"
"strings"
"testing"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"git.vakhrushev.me/av/jellybit/internal/ingest"
"git.vakhrushev.me/av/jellybit/internal/layout"
"git.vakhrushev.me/av/jellybit/internal/recognize"
"git.vakhrushev.me/av/jellybit/internal/store"
"git.vakhrushev.me/av/jellybit/internal/worker"
)
// fakeAPI записывает исходящие Chattable; обновления не нужны (хендлеры зовём
// напрямую).
type fakeAPI struct {
sent []sentMsg
edits []sentMsg
answers []string
}
type sentMsg struct {
chatID int64
text string
hasKB bool
}
func (f *fakeAPI) Send(c tgbotapi.Chattable) (tgbotapi.Message, error) {
switch m := c.(type) {
case tgbotapi.MessageConfig:
f.sent = append(f.sent, sentMsg{m.ChatID, m.Text, m.ReplyMarkup != nil})
case tgbotapi.EditMessageTextConfig:
f.edits = append(f.edits, sentMsg{m.ChatID, m.Text, m.ReplyMarkup != nil})
}
return tgbotapi.Message{MessageID: 1}, nil
}
func (f *fakeAPI) Request(c tgbotapi.Chattable) (*tgbotapi.APIResponse, error) {
if cb, ok := c.(tgbotapi.CallbackConfig); ok {
f.answers = append(f.answers, cb.Text)
}
return &tgbotapi.APIResponse{Ok: true}, nil
}
func (f *fakeAPI) GetUpdatesChan(tgbotapi.UpdateConfig) tgbotapi.UpdatesChannel { return nil }
func (f *fakeAPI) StopReceivingUpdates() {}
type fakeIngestor struct {
lastReq ingest.Request
res ingest.Result
}
func (f *fakeIngestor) Ingest(_ context.Context, req ingest.Request) (ingest.Result, error) {
f.lastReq = req
return f.res, nil
}
type fakeReviewer struct {
data *worker.ReviewData
applied []int64
refined map[int64]string
typed map[int64]string
deferred []int64
canceled []int64
}
func (f *fakeReviewer) ReviewData(context.Context, int64) (*worker.ReviewData, error) {
return f.data, nil
}
func (f *fakeReviewer) Apply(_ context.Context, id int64) error {
f.applied = append(f.applied, id)
return nil
}
func (f *fakeReviewer) Refine(_ context.Context, id int64, hint string) error {
if f.refined == nil {
f.refined = map[int64]string{}
}
f.refined[id] = hint
return nil
}
func (f *fakeReviewer) SetType(_ context.Context, id int64, t string) error {
if f.typed == nil {
f.typed = map[int64]string{}
}
f.typed[id] = t
return nil
}
func (f *fakeReviewer) Defer(_ context.Context, id int64) error {
f.deferred = append(f.deferred, id)
return nil
}
func (f *fakeReviewer) Cancel(_ context.Context, id int64) error {
f.canceled = append(f.canceled, id)
return nil
}
func reviewData(state store.State) *worker.ReviewData {
s, e := 2, 1
return &worker.ReviewData{
Download: store.Download{ID: 5, State: state, Context: "Фарго, второй сезон", SourceRef: "magnet:?x"},
Recognition: &store.Recognition{
Provider: store.NullString("tvdb"), ProviderID: store.NullString("269613"),
Reasons: `["неполный пак"]`,
},
Plan: recognize.Plan{
Type: recognize.MediaSeries, Title: "Фарго", Year: 2015,
Files: []recognize.PlanFile{{Src: "e1.mkv", Role: recognize.RoleEpisode, Season: &s, Episode: &e}},
},
Preview: []layout.Link{
{Src: "e1.mkv", Dst: "/srv/media/series/Фарго (2015)/Season 02/Фарго (2015) S02E01.mkv"},
},
}
}
func newTestBot(t *testing.T, allowed []int64) (*Bot, *fakeAPI, *fakeIngestor, *fakeReviewer) {
t.Helper()
api := &fakeAPI{}
ing := &fakeIngestor{res: ingest.Result{DownloadID: 5, State: store.StateDownloading}}
rev := &fakeReviewer{data: reviewData(store.StateReview)}
b := New(api, ing, rev, Config{AllowedUserIDs: allowed, WebBaseURL: "http://host:8080"},
slog.New(slog.NewTextHandler(io.Discard, nil)))
return b, api, ing, rev
}
func msgFrom(userID int64, text string) *tgbotapi.Message {
return &tgbotapi.Message{
MessageID: 1, From: &tgbotapi.User{ID: userID}, Chat: &tgbotapi.Chat{ID: userID}, Text: text,
}
}
func TestBot_IngestFromMagnet(t *testing.T) {
b, api, ing, _ := newTestBot(t, []int64{7})
b.handleMessage(context.Background(), msgFrom(7, "крутой сериал\nmagnet:?xt=urn:btih:ABC"))
if !strings.HasPrefix(ing.lastReq.Source, "magnet:?xt=urn:btih:ABC") {
t.Errorf("source = %q", ing.lastReq.Source)
}
if ing.lastReq.Context != "крутой сериал" {
t.Errorf("context = %q", ing.lastReq.Context)
}
if len(api.sent) != 1 || !strings.Contains(api.sent[0].text, "Принято #5") {
t.Errorf("sent = %+v", api.sent)
}
}
func TestBot_DeniesUnknownUser(t *testing.T) {
b, api, ing, _ := newTestBot(t, []int64{7})
b.handleMessage(context.Background(), msgFrom(999, "magnet:?xt=urn:btih:ABC"))
if len(ing.lastReq.Source) != 0 {
t.Error("ingest не должен вызываться для чужого пользователя")
}
if len(api.sent) != 1 || !strings.Contains(api.sent[0].text, "Доступ запрещён") {
t.Errorf("sent = %+v", api.sent)
}
}
func TestBot_NoMagnet(t *testing.T) {
b, api, _, _ := newTestBot(t, []int64{7})
b.handleMessage(context.Background(), msgFrom(7, "привет"))
if len(api.sent) != 1 || !strings.Contains(api.sent[0].text, "Не вижу magnet") {
t.Errorf("sent = %+v", api.sent)
}
}
func TestBot_RefineViaReply(t *testing.T) {
b, _, _, rev := newTestBot(t, []int64{7})
// Кнопка «Уточнить» поставила ожидание подсказки для чата 7.
b.setPending(7, 5)
b.handleMessage(context.Background(), msgFrom(7, "это второй сезон"))
if rev.refined[5] != "это второй сезон" {
t.Errorf("refine = %v", rev.refined)
}
}
func cbFrom(userID int64, data string) *tgbotapi.CallbackQuery {
return &tgbotapi.CallbackQuery{
ID: "cb", From: &tgbotapi.User{ID: userID}, Data: data,
Message: &tgbotapi.Message{MessageID: 99, Chat: &tgbotapi.Chat{ID: userID}},
}
}
func TestBot_CallbackApply(t *testing.T) {
b, api, _, rev := newTestBot(t, []int64{7})
b.handleCallback(context.Background(), cbFrom(7, "apply:5"))
if len(rev.applied) != 1 || rev.applied[0] != 5 {
t.Errorf("applied = %v", rev.applied)
}
if len(api.answers) != 1 {
t.Errorf("answers = %v", api.answers)
}
if len(api.edits) != 1 { // карточка обновлена на месте
t.Errorf("edits = %v", api.edits)
}
}
func TestBot_CallbackType(t *testing.T) {
b, _, _, rev := newTestBot(t, []int64{7})
b.handleCallback(context.Background(), cbFrom(7, "type:5:movie"))
if rev.typed[5] != "movie" {
t.Errorf("typed = %v", rev.typed)
}
}
func TestBot_CallbackRefineSetsPending(t *testing.T) {
b, api, _, _ := newTestBot(t, []int64{7})
b.handleCallback(context.Background(), cbFrom(7, "refine:5"))
if id, ok := b.takePending(7); !ok || id != 5 {
t.Errorf("pending = %d,%v", id, ok)
}
if len(api.sent) != 1 || !strings.Contains(api.sent[0].text, "подсказкой") {
t.Errorf("sent = %+v", api.sent)
}
}
func TestBot_CallbackDeniesUnknown(t *testing.T) {
b, _, _, rev := newTestBot(t, []int64{7})
b.handleCallback(context.Background(), cbFrom(999, "apply:5"))
if len(rev.applied) != 0 {
t.Error("чужой колбэк не должен исполняться")
}
}
func TestBot_NotifyReview(t *testing.T) {
b, api, _, _ := newTestBot(t, []int64{7, 8})
b.Notify(context.Background(), 5, worker.EventReview)
if len(api.sent) != 2 { // обоим доверенным
t.Fatalf("sent to %d chats, want 2", len(api.sent))
}
if !strings.Contains(api.sent[0].text, "Нужно подтверждение #5") {
t.Errorf("card text = %q", api.sent[0].text)
}
if !api.sent[0].hasKB {
t.Error("карточка ревью без клавиатуры")
}
}
func TestBot_NotifyDone(t *testing.T) {
b, api, _, rev := newTestBot(t, []int64{7})
rev.data = reviewData(store.StateDone)
b.Notify(context.Background(), 5, worker.EventDone)
if len(api.sent) != 1 || !strings.Contains(api.sent[0].text, "Готово") {
t.Errorf("sent = %+v", api.sent)
}
}
func TestParseCallback(t *testing.T) {
a, id, v := parseCallback("type:5:series")
if a != "type" || id != 5 || v != "series" {
t.Errorf("got %q %d %q", a, id, v)
}
a, id, v = parseCallback("apply:9")
if a != "apply" || id != 9 || v != "" {
t.Errorf("got %q %d %q", a, id, v)
}
}