265 lines
8.1 KiB
Go
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)
|
|
}
|
|
}
|