package bt import ( "encoding/base64" "encoding/json" gohttp "net/http" "net/url" "os" "path/filepath" "reflect" "testing" "github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/test" "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/protocol/bt" ) func TestFetcher_Resolve_Torrent(t *testing.T) { doResolve(t, buildFetcher(t)) } func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) { fetcher := buildFetcher(t) buf, err := os.ReadFile("./testdata/ubuntu-22.04-live-server-amd64.iso.torrent") if err != nil { t.Fatal(err) } // convert to data uri dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf) err = fetcher.Resolve(&base.Request{ URL: dataUri, }) if err != nil { panic(err) } want := &base.Resource{ Size: 1466714112, Range: true, Files: []*base.FileInfo{ { Name: "ubuntu-22.04-live-server-amd64.iso", Size: 1466714112, }, }, Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed", } if !reflect.DeepEqual(want, fetcher.Meta().Res) { t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want) } } func TestFetcher_Config(t *testing.T) { doResolve(t, buildConfigFetcher(t, nil)) } func TestFetcher_ResolveWithProxy(t *testing.T) { usr, pwd := "admin", "123" proxyListener := test.StartSocks5Server(usr, pwd) defer proxyListener.Close() doResolve(t, buildConfigFetcher(t, &base.DownloaderProxyConfig{ Enable: true, System: false, Scheme: "socks5", Host: proxyListener.Addr().String(), Usr: usr, Pwd: pwd, })) } func doResolve(t *testing.T, fetcher fetcher.Fetcher) { t.Run("Resolve Single File", func(t *testing.T) { err := fetcher.Resolve(&base.Request{ URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent", Extra: bt.ReqExtra{ Trackers: []string{ "udp://tracker.birkenwald.de:6969/announce", "udp://tracker.bitsearch.to:1337/announce", }, }, }) if err != nil { panic(err) } want := &base.Resource{ Size: 1466714112, Range: true, Files: []*base.FileInfo{ { Name: "ubuntu-22.04-live-server-amd64.iso", Size: 1466714112, }, }, Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed", } if !reflect.DeepEqual(want, fetcher.Meta().Res) { t.Errorf("Resolve Single File Resolve() got = %v, want %v", fetcher.Meta().Res, want) } }) t.Run("Resolve Multi Files", func(t *testing.T) { err := fetcher.Resolve(&base.Request{ URL: "./testdata/test.torrent", Extra: bt.ReqExtra{ Trackers: []string{ "udp://tracker.birkenwald.de:6969/announce", "udp://tracker.bitsearch.to:1337/announce", }, }, }) if err != nil { panic(err) } want := &base.Resource{ Name: "test", Size: 107484864, Range: true, Files: []*base.FileInfo{ { Name: "c.txt", Path: "path", Size: 98501754, }, { Name: "b.txt", Size: 8904996, }, { Name: "a.txt", Size: 78114, }, }, Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb", } if !reflect.DeepEqual(want, fetcher.Meta().Res) { t.Errorf("Resolve Multi Files Resolve() got = %v, want %v", fetcher.Meta().Res, want) } }) t.Run("Resolve Unclean Torrent", func(t *testing.T) { err := fetcher.Resolve(&base.Request{ URL: "./testdata/test.unclean.torrent", }) if err != nil { t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err) } }) t.Run("Resolve file scheme Torrent", func(t *testing.T) { file, _ := filepath.Abs("./testdata/test.unclean.torrent") uri := "file:///" + file err := fetcher.Resolve(&base.Request{ URL: uri, }) if err != nil { t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err) } }) } func TestFetcherManager_ParseName(t *testing.T) { type args struct { u string } tests := []struct { name string args args want string }{ { name: "broken url", args: args{ u: "magnet://!@#%github.com", }, want: "", }, { name: "dn", args: args{ u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed&dn=ubuntu-22.04-live-server-amd64.iso", }, want: "ubuntu-22.04-live-server-amd64.iso", }, { name: "no dn", args: args{ u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed", }, want: "8a55cfbd5ca5d11507364765936c4f9e55b253ed", }, { name: "non standard magnet", args: args{ u: "magnet:?xxt=abcd", }, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fm := &FetcherManager{} if got := fm.ParseName(tt.args.u); got != tt.want { t.Errorf("ParseName() = %v, want %v", got, tt.want) } }) } } func buildFetcher(t testing.TB) fetcher.Fetcher { fb := new(FetcherManager) fetcher := fb.Build() newController := controller.NewController() newController.GetConfig = func(v any) { json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v) } fetcher.Setup(newController) t.Cleanup(func() { fetcher.Close(); closeClient() }) return fetcher } func buildConfigFetcher(t testing.TB, proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher { fetcher := new(FetcherManager).Build() newController := controller.NewController() mockCfg := config{ CustomTrackers: []string{ "udp://tracker.birkenwald.de:6969/announce", "udp://tracker.bitsearch.to:1337/announce", }} newController.GetConfig = func(v any) { json.Unmarshal([]byte(test.ToJson(mockCfg)), v) } newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) { return proxyConfig.ToHandler() } fetcher.Setup(newController) t.Cleanup(func() { fetcher.Close(); closeClient() }) return fetcher } // TestFetcher_Patch tests the Patch functionality for BT fetcher. // It tests modifying selected files after Resolve (without downloading). func TestFetcher_Patch(t *testing.T) { f := buildFetcher(t) // Resolve a multi-file torrent err := f.Resolve(&base.Request{ URL: "./testdata/test.torrent", }) if err != nil { t.Fatalf("Resolve failed: %v", err) } // Resolution describes files without choosing download options. meta := f.Meta() if len(meta.Res.Files) != 3 { t.Fatalf("Expected 3 files, got %d", len(meta.Res.Files)) } if meta.Opts != nil { t.Fatal("Resolve should not assign download options") } // Total size of all files: 107484864 totalSize := int64(107484864) if meta.Res.Size != totalSize { t.Fatalf("Expected total size %d, got %d", totalSize, meta.Res.Size) } t.Run("Patch with valid indices", func(t *testing.T) { // Select only file 0 and 2 (c.txt and a.txt) err := f.Patch(nil, &base.Options{ SelectFiles: []int{0, 2}, }) if err != nil { t.Fatalf("Patch failed: %v", err) } meta := f.Meta() if !reflect.DeepEqual(meta.Opts.SelectFiles, []int{0, 2}) { t.Errorf("Expected SelectFiles [0, 2], got %v", meta.Opts.SelectFiles) } // Size should be recalculated: c.txt (98501754) + a.txt (78114) = 98579868 expectedSize := int64(98501754 + 78114) if meta.Res.Size != expectedSize { t.Errorf("Expected size %d, got %d", expectedSize, meta.Res.Size) } }) t.Run("Patch with invalid indices are silently ignored", func(t *testing.T) { // Mix of valid (0, 1) and invalid (-1, 5, 100) indices err := f.Patch(nil, &base.Options{ SelectFiles: []int{-1, 0, 5, 1, 100}, }) if err != nil { t.Fatalf("Patch should not return error for invalid indices: %v", err) } meta := f.Meta() // Only valid indices 0 and 1 should remain if !reflect.DeepEqual(meta.Opts.SelectFiles, []int{0, 1}) { t.Errorf("Expected SelectFiles [0, 1], got %v", meta.Opts.SelectFiles) } // Size should be: c.txt (98501754) + b.txt (8904996) = 107406750 expectedSize := int64(98501754 + 8904996) if meta.Res.Size != expectedSize { t.Errorf("Expected size %d, got %d", expectedSize, meta.Res.Size) } }) t.Run("Patch with all invalid indices results in empty selection", func(t *testing.T) { err := f.Patch(nil, &base.Options{ SelectFiles: []int{-5, 10, 999}, }) if err != nil { t.Fatalf("Patch should not return error: %v", err) } meta := f.Meta() if len(meta.Opts.SelectFiles) != 0 { t.Errorf("Expected empty SelectFiles, got %v", meta.Opts.SelectFiles) } // Note: CalcSize with empty selectFiles calculates total size of all files // This is by design - empty selection in CalcSize means "all files" // But SelectFiles being empty means no files are selected for download if meta.Res.Size != totalSize { t.Errorf("Expected size %d (CalcSize with empty slice = all files), got %d", totalSize, meta.Res.Size) } }) t.Run("Patch with nil opts does nothing", func(t *testing.T) { // First set a known state f.Patch(nil, &base.Options{SelectFiles: []int{1}}) prevSelectFiles := f.Meta().Opts.SelectFiles // Patch with nil opts err := f.Patch(nil, nil) if err != nil { t.Fatalf("Patch with nil opts should not fail: %v", err) } // Should remain unchanged if !reflect.DeepEqual(f.Meta().Opts.SelectFiles, prevSelectFiles) { t.Errorf("SelectFiles should remain unchanged after nil opts Patch") } }) t.Run("Patch progress array is resized", func(t *testing.T) { btFetcher := f.(*Fetcher) // Initialize progress array btFetcher.data.Progress = make(fetcher.Progress, 3) err := f.Patch(nil, &base.Options{ SelectFiles: []int{0, 2}, }) if err != nil { t.Fatalf("Patch failed: %v", err) } if len(btFetcher.data.Progress) != 2 { t.Errorf("Expected Progress length 2, got %d", len(btFetcher.data.Progress)) } }) } func TestFetcher_PeerDiscoveryConfig(t *testing.T) { tests := []struct { name string stored any wantDisableDHT bool wantDisablePEX bool }{ {"default config", new(FetcherManager).DefaultConfig(), false, false}, {"stored config without the fields", map[string]any{"listenPort": 0, "seedRatio": 1.0}, false, false}, {"both disabled", map[string]any{"disableDht": true, "disablePex": true}, true, true}, {"only DHT disabled", map[string]any{"disableDht": true}, true, false}, {"only PEX disabled", map[string]any{"disablePex": true}, false, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := buildFetcherWithConfig(tt.stored).(*Fetcher) if f.config.DisableDHT != tt.wantDisableDHT || f.config.DisablePEX != tt.wantDisablePEX { t.Errorf("config got DisableDHT = %v, DisablePEX = %v, want %v, %v", f.config.DisableDHT, f.config.DisablePEX, tt.wantDisableDHT, tt.wantDisablePEX) } }) } } func TestFetcher_PeerDiscoveryDisabledOnClient(t *testing.T) { if err := closeClient(); err != nil { t.Fatal(err) } f := buildFetcherWithConfig(map[string]any{"disableDht": true, "disablePex": true}) if err := f.Resolve(&base.Request{URL: "./testdata/test.torrent"}); err != nil { t.Fatal(err) } if !cfg.NoDHT || !cfg.DisablePEX { t.Errorf("client config got NoDHT = %v, DisablePEX = %v, want true", cfg.NoDHT, cfg.DisablePEX) } if n := len(client.DhtServers()); n != 0 { t.Errorf("client got %d DHT servers, want 0", n) } } func buildFetcherWithConfig(stored any) fetcher.Fetcher { fetcher := new(FetcherManager).Build() newController := controller.NewController() newController.GetConfig = func(v any) { json.Unmarshal([]byte(test.ToJson(stored)), v) } fetcher.Setup(newController) return fetcher }