package magnet import ( "net/url" "strings" "testing" ) func TestParse(t *testing.T) { tests := []struct { name string raw string infohash string dn string trackers int }{ { name: "btih hex (из BRIEF)", raw: "magnet:?xt=urn:btih:541ADCFF3B6DD5DBA7088EA83317D9D6FAC331D6&tr=http%3A%2F%2Fbt.t-ru.org%2Fann%3Fmagnet&dn=rutracker-topic-6514485", infohash: "541adcff3b6dd5dba7088ea83317d9d6fac331d6", dn: "rutracker-topic-6514485", trackers: 1, }, { name: "btih base32 (20 нулевых байт)", raw: "magnet:?xt=urn:btih:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", infohash: "0000000000000000000000000000000000000000", }, { name: "btmh v2 sha256", raw: "magnet:?xt=urn:btmh:12200123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", infohash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := Parse(tc.raw) if err != nil { t.Fatalf("Parse: %v", err) } if got.Infohash != tc.infohash { t.Errorf("infohash = %q, want %q", got.Infohash, tc.infohash) } if got.DisplayName != tc.dn { t.Errorf("dn = %q, want %q", got.DisplayName, tc.dn) } if len(got.Trackers) != tc.trackers { t.Errorf("trackers = %d, want %d", len(got.Trackers), tc.trackers) } }) } } // Гибридный magnet несёт оба хеша: Infohash — v1 (приоритет), Infohashes — оба. func TestParseHybridKeepsBothHashes(t *testing.T) { const ( v1 = "541adcff3b6dd5dba7088ea83317d9d6fac331d6" v2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" ) got, err := Parse("magnet:?xt=urn:btih:" + v1 + "&xt=urn:btmh:1220" + v2) if err != nil { t.Fatalf("Parse: %v", err) } if got.Infohash != v1 { t.Errorf("primary = %q, want v1", got.Infohash) } if len(got.Infohashes) != 2 || got.Infohashes[0] != v1 || got.Infohashes[1] != v2 { t.Errorf("infohashes = %v, want [v1 v2]", got.Infohashes) } } func TestParseErrors(t *testing.T) { cases := []string{ "https://example.com/file.torrent", // не magnet "magnet:?dn=no-infohash", // нет xt "magnet:?xt=urn:btih:zzzz", // некорректный hash "", // пусто } for _, raw := range cases { if _, err := Parse(raw); err == nil { t.Errorf("Parse(%q): ожидалась ошибка", raw) } } } func TestParseExtraFields(t *testing.T) { raw := "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6" + "&xl=2200000000&xs=http%3A%2F%2Fmirror.example%2Ft.torrent&kt=dune+scifi" got, err := Parse(raw) if err != nil { t.Fatalf("Parse: %v", err) } if got.ExactLength != 2200000000 { t.Errorf("xl = %d, want 2200000000", got.ExactLength) } if len(got.Sources) != 1 || got.Sources[0] != "http://mirror.example/t.torrent" { t.Errorf("xs = %v", got.Sources) } if len(got.Keywords) != 1 || got.Keywords[0] != "dune scifi" { t.Errorf("kt = %v", got.Keywords) } } func TestParseInvalidXLIgnored(t *testing.T) { for _, raw := range []string{ "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&xl=notanumber", "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&xl=-5", "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6", } { got, err := Parse(raw) if err != nil { t.Fatalf("Parse(%q): %v", raw, err) } if got.ExactLength != 0 { t.Errorf("Parse(%q): xl = %d, want 0", raw, got.ExactLength) } } } func TestInfoContext(t *testing.T) { tests := []struct { name string info Info want []string // подстроки, которые ДОЛЖНЫ быть notWant []string // подстроки, которых быть НЕ должно }{ { name: "содержательный dn", info: Info{DisplayName: "Dune.Part.Two.2024.2160p"}, want: []string{"Dune.Part.Two.2024.2160p"}, notWant: []string{"Размер:", "Трекер:"}, }, { name: "заглушка-dn не берётся как имя", info: Info{DisplayName: "rutracker-topic-6514485", Trackers: []string{"http://bt.t-ru.org/ann?magnet"}}, want: []string{"Трекер: t-ru.org"}, notWant: []string{"rutracker-topic"}, }, { name: "только размер", info: Info{ExactLength: 2200000000}, want: []string{"Размер: ≈ 2.0 GiB"}, }, { name: "домен из xs, служебный поддомен убран", info: Info{Sources: []string{"udp://tracker.example.org:80/announce"}}, want: []string{"Трекер: example.org"}, notWant: []string{"tracker.example.org"}, }, { name: "все поля вместе", info: Info{ DisplayName: "Dune", ExactLength: 1500, Trackers: []string{"http://bt.t-ru.org/ann"}, Keywords: []string{"scifi dune"}, // как отдаёт url.Query() из kt=scifi+dune }, want: []string{"Dune", "Размер:", "Трекер: t-ru.org", "Ключевые слова: scifi, dune"}, }, { name: "пустой Info → пустой контекст", info: Info{}, want: nil, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := tc.info.Context() if len(tc.want) == 0 && got != "" { t.Errorf("Context() = %q, want пусто", got) } for _, w := range tc.want { if !strings.Contains(got, w) { t.Errorf("Context() = %q, want содержит %q", got, w) } } for _, nw := range tc.notWant { if strings.Contains(got, nw) { t.Errorf("Context() = %q, НЕ должно содержать %q", got, nw) } } }) } } // Реальная ссылка с рутрекера: dn несёт полный релиз-заголовок в // процент-кодировке (кириллица + латиница, сезон/серии/год/жанры/качество), // tr — announce-хост bt4.t-ru.org. Проверяем полный путь до Context(). func TestParseRealRutrackerLink(t *testing.T) { const raw = "magnet:?xt=urn:btih:BACA24E18C7382A9E9A44132C8D7DB86C4D319C2" + "&tr=http%3A%2F%2Fbt4.t-ru.org%2Fann%3Fmagnet" + "&dn=%D0%91%D1%83%D1%85%D1%82%D0%B0%20%D0%B2%D0%B4%D0%BE%D0%B2%20%2F%20Widow's%20Bay%20%2F%20%D0%A1%D0%B5%D0%B7%D0%BE%D0%BD%3A%201%20%2F%20%D0%A1%D0%B5%D1%80%D0%B8%D0%B8%3A%201-10%20%D0%B8%D0%B7%2010%20(%D0%A5%D0%B8%D1%80%D0%BE%20%D0%9C%D1%83%D1%80%D0%B0%D0%B9)%20%5B2026%2C%20%D0%A1%D0%A8%D0%90%2C%20%D0%A3%D0%B6%D0%B0%D1%81%D1%8B%2C%20%D0%B4%D1%80%D0%B0%D0%BC%D0%B0%2C%20%D0%BA%D0%BE%D0%BC%D0%B5%D0%B4%D0%B8%D1%8F%2C%20WEB-DL%201080p%5D%207%20x%20MVO" got, err := Parse(raw) if err != nil { t.Fatalf("Parse: %v", err) } if got.Infohash != "baca24e18c7382a9e9a44132c8d7db86c4d319c2" { t.Errorf("infohash = %q", got.Infohash) } // dn раскодирован url.Query() — без ручного urldecode. const wantName = "Бухта вдов / Widow's Bay / Сезон: 1 / Серии: 1-10 из 10 " + "(Хиро Мурай) [2026, США, Ужасы, драма, комедия, WEB-DL 1080p] 7 x MVO" if got.DisplayName != wantName { t.Errorf("dn = %q", got.DisplayName) } ctx := got.Context() if !strings.Contains(ctx, "Widow's Bay") || !strings.Contains(ctx, "2026") { t.Errorf("Context() без релиз-данных: %q", ctx) } if !strings.Contains(ctx, "Трекер: t-ru.org") { t.Errorf("Context() без домена трекера (bt4. должен срезаться): %q", ctx) } if strings.Contains(ctx, "bt4.") { t.Errorf("служебный поддомен не срезан: %q", ctx) } } // Ссылку могли скопировать целиком в процент-кодировке (например вытащили из // другого URL) — Parse должен раскодировать её один раз и разобрать. func TestParseDoubleEncoded(t *testing.T) { const inner = "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&dn=Dune" got, err := Parse(url.QueryEscape(inner)) if err != nil { t.Fatalf("Parse(double-encoded): %v", err) } if got.Infohash != "541adcff3b6dd5dba7088ea83317d9d6fac331d6" { t.Errorf("infohash = %q", got.Infohash) } if got.DisplayName != "Dune" { t.Errorf("dn = %q", got.DisplayName) } } func TestParseNormalisesCase(t *testing.T) { lower := "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6" upper := "magnet:?xt=urn:btih:541ADCFF3B6DD5DBA7088EA83317D9D6FAC331D6" a, err := Parse(lower) if err != nil { t.Fatal(err) } b, err := Parse(upper) if err != nil { t.Fatal(err) } if a.Infohash != b.Infohash { t.Errorf("регистр влияет: %q != %q", a.Infohash, b.Infohash) } }