package worker import ( "bytes" "context" "crypto/sha1" "encoding/hex" "testing" "github.com/anacrolix/torrent/bencode" "github.com/anacrolix/torrent/metainfo" "git.vakhrushev.me/av/jellybit/internal/store" ) // buildTorrent — валидные байты .torrent и их v1-инфохэш. func buildTorrent(t *testing.T, name string) (data []byte, infohash string) { t.Helper() info := metainfo.Info{Name: name, Length: 2048, PieceLength: 1024, Pieces: make([]byte, 40)} infoBytes, err := bencode.Marshal(info) if err != nil { t.Fatalf("marshal info: %v", err) } sum := sha1.Sum(infoBytes) mi := metainfo.MetaInfo{InfoBytes: infoBytes, Announce: "http://t/ann"} var buf bytes.Buffer if err := mi.Write(&buf); err != nil { t.Fatalf("write metainfo: %v", err) } return buf.Bytes(), hex.EncodeToString(sum[:]) } // catchedTorrentStore — пойманная torrent-загрузка с сохранёнными байтами. func catchedTorrentStore(id string, data []byte, infohash string) *fakeStore { return &fakeStore{ downloads: map[string]*store.Download{ id: { ID: id, State: store.StateCatched, SourceType: store.SourceTorrent, SourceRef: "Some.Release.Name", // человекочитаемый референс, не URL Infohashes: hashesOf(id, infohash), CreatedAt: nowStr, }, }, torrents: map[string][]byte{id: data}, } } // Пойманная torrent-загрузка добавляется в qBittorrent ФАЙЛОМ (Torrents), не URL. func TestProcessCatchedTorrentAddsAsFile(t *testing.T) { data, ih := buildTorrent(t, "Dune.mkv") st := catchedTorrentStore("1", data, ih) qb := &fakeQbt{} w := newTestWorker(st, qb) w.SetNamer(&fakeNamer{name: "Дюна (2024)"}) w.processCatched(context.Background()) if len(qb.added) != 1 { t.Fatalf("qbt.Add calls = %d, want 1", len(qb.added)) } add := qb.added[0] if len(add.URLs) != 0 { t.Errorf("torrent не должен добавляться по URLs: %v", add.URLs) } if len(add.Torrents) != 1 || !bytes.Equal(add.Torrents[0], data) { t.Errorf("байты .torrent не переданы в Add.Torrents") } if add.Rename != "Дюна (2024)" { t.Errorf("rename = %q", add.Rename) } if st.downloads["1"].State != store.StateDownloading { t.Errorf("state = %q, want downloading", st.downloads["1"].State) } } // Retry torrent-загрузки без живой раздачи добавляет её ФАЙЛОМ (регрессия // BLOCKER 1: раньше add гейтился на magnet и torrent активировался без раздачи). func TestRetryTorrentAddsAsFile(t *testing.T) { data, ih := buildTorrent(t, "Fargo.mkv") st := catchedTorrentStore("1", data, ih) st.downloads["1"].State = store.StateFailed qb := &fakeQbt{} // раздачи в qBittorrent нет → должен добавить заново w := newTestWorker(st, qb) if err := w.Retry(context.Background(), "1"); err != nil { t.Fatalf("Retry: %v", err) } if len(qb.added) != 1 { t.Fatalf("ожидалось повторное добавление, got %d", len(qb.added)) } if len(qb.added[0].Torrents) != 1 || len(qb.added[0].URLs) != 0 { t.Errorf("retry torrent должен добавляться файлом, add = %+v", qb.added[0]) } if st.downloads["1"].State != store.StateDownloading { t.Errorf("state = %q, want downloading", st.downloads["1"].State) } } // Retry torrent-загрузки без сохранённых байтов — откат активации (не оставляем // «качающуюся» задачу без раздачи). func TestRetryTorrentMissingBytesRollsBack(t *testing.T) { _, ih := buildTorrent(t, "X.mkv") st := catchedTorrentStore("1", nil, ih) // байтов нет delete(st.torrents, "1") st.downloads["1"].State = store.StateFailed qb := &fakeQbt{} w := newTestWorker(st, qb) if err := w.Retry(context.Background(), "1"); err == nil { t.Fatal("ожидалась ошибка (нет байтов torrent)") } if len(qb.added) != 0 { t.Errorf("add не должен вызываться без байтов") } if st.downloads["1"].State != store.StateFailed { t.Errorf("state = %q, want failed (откат активации)", st.downloads["1"].State) } }