package tracker import ( "bytes" "context" "encoding/binary" "io" "net/http" "net/url" "strings" "testing" "github.com/veggiedefender/torrent-client/internal/torrentfile" ) func TestParsePeers(t *testing.T) { t.Parallel() raw := []byte{127, 0, 0, 1, 0x1A, 0xE1} peers, err := parsePeers(raw) if err != nil { t.Fatalf("parsePeers returned error: %v", err) } if len(peers) != 1 { t.Fatalf("unexpected peer count: got %d", len(peers)) } if got, want := peers[0].IP.String(), "127.0.0.1"; got != want { t.Fatalf("unexpected peer IP: got %s want %s", got, want) } if got, want := peers[0].Port, uint16(6881); got != want { t.Fatalf("unexpected peer port: got %d want %d", got, want) } } func TestParsePeersRejectsInvalidLength(t *testing.T) { t.Parallel() _, err := parsePeers([]byte{1, 2, 3, 4, 5}) if err == nil { t.Fatal("expected parsePeers to fail for invalid compact peer data") } } func TestGetPeersBuildsAnnounceAndParsesResponse(t *testing.T) { t.Parallel() var infoHash [20]byte copy(infoHash[:], []byte("abcdefghijklmnopqrst")) var peerID [20]byte copy(peerID[:], []byte("-ZT0001-123456789012")) tf := &torrentfile.TorrentFile{ Announce: "http://tracker.test/announce", InfoHash: infoHash, Length: 1000, } var seenRequest *http.Request client := fakeClient{ do: func(req *http.Request) (*http.Response, error) { seenRequest = req payload := []byte("d8:intervali1800e5:peers6:\x7f\x00\x00\x01\x1a\xe1e") return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(payload)), }, nil }, } announceURL, err := url.Parse(tf.Announce) if err != nil { t.Fatalf("failed to parse announce URL: %v", err) } peers, err := getPeersWithClient(context.Background(), client, announceURL, tf, AnnounceOptions{ PeerID: peerID, Port: 6881, Downloaded: 100, }) if err != nil { t.Fatalf("GetPeers returned error: %v", err) } if seenRequest == nil { t.Fatal("expected tracker request to be sent") } q := seenRequest.URL.Query() if got := q.Get("compact"); got != "1" { t.Fatalf("unexpected compact value: got %q", got) } if got := q.Get("port"); got != "6881" { t.Fatalf("unexpected port value: got %q", got) } if got := q.Get("left"); got != "900" { t.Fatalf("unexpected left value: got %q", got) } if got := q.Get("numwant"); got != "50" { t.Fatalf("unexpected numwant value: got %q", got) } if got := []byte(q.Get("info_hash")); !equalBytes(got, infoHash[:]) { t.Fatalf("unexpected info_hash value: got %x want %x", got, infoHash) } if got := []byte(q.Get("peer_id")); !equalBytes(got, peerID[:]) { t.Fatalf("unexpected peer_id value: got %x want %x", got, peerID) } if len(peers) != 1 { t.Fatalf("unexpected peer count: got %d", len(peers)) } if got, want := peers[0].Port, uint16(6881); got != want { t.Fatalf("unexpected peer port: got %d want %d", got, want) } } func TestBuildAnnounceURLEncodesBinaryValues(t *testing.T) { t.Parallel() var infoHash [20]byte copy(infoHash[:], []byte{0x00, 0xFF, 0x2F, 'A', 0x20}) var peerID [20]byte copy(peerID[:], []byte("-ZT0001-\x00\xFFabc")) tf := &torrentfile.TorrentFile{ Announce: "http://tracker.test/announce?token=abc", InfoHash: infoHash, Length: 10, } baseURL, err := url.Parse(tf.Announce) if err != nil { t.Fatalf("failed to parse announce URL: %v", err) } announceURL, err := buildAnnounceURL(baseURL, tf, AnnounceOptions{ PeerID: peerID, Port: 6881, NumWant: 42, Uploaded: -5, Downloaded: -7, }) if err != nil { t.Fatalf("buildAnnounceURL returned error: %v", err) } if !strings.Contains(announceURL, "token=abc") { t.Fatalf("announce URL did not preserve existing query params: %s", announceURL) } if !strings.Contains(announceURL, "info_hash=%00%FF%2FA%20") { t.Fatalf("announce URL did not encode binary info hash correctly: %s", announceURL) } if !strings.Contains(announceURL, "peer_id=-ZT0001-%00%FFabc") { t.Fatalf("announce URL did not encode binary peer ID correctly: %s", announceURL) } if !strings.Contains(announceURL, "uploaded=0") { t.Fatalf("announce URL did not clamp uploaded value: %s", announceURL) } if !strings.Contains(announceURL, "downloaded=0") { t.Fatalf("announce URL did not clamp downloaded value: %s", announceURL) } if !strings.Contains(announceURL, "left=10") { t.Fatalf("announce URL did not compute left bytes correctly: %s", announceURL) } } func TestBuildAnnounceURLRejectsUnsupportedScheme(t *testing.T) { t.Parallel() baseURL, err := url.Parse("udp://tracker.test:6969/announce") if err != nil { t.Fatalf("failed to parse announce URL: %v", err) } _, err = buildAnnounceURL(baseURL, &torrentfile.TorrentFile{ Length: 10, }, AnnounceOptions{}) if err == nil { t.Fatal("expected buildAnnounceURL to reject unsupported tracker scheme") } } func TestGetPeersReturnsTrackerFailure(t *testing.T) { t.Parallel() tf := &torrentfile.TorrentFile{ Announce: "http://tracker.test/announce", Length: 10, } client := fakeClient{ do: func(req *http.Request) (*http.Response, error) { payload := []byte("d14:failure reason11:bad requeste") return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(payload)), }, nil }, } announceURL, err := url.Parse(tf.Announce) if err != nil { t.Fatalf("failed to parse announce URL: %v", err) } _, err = getPeersWithClient(context.Background(), client, announceURL, tf, AnnounceOptions{}) if err == nil { t.Fatal("expected getPeersWithClient to return tracker failure error") } } func TestParseUDPConnectResponse(t *testing.T) { t.Parallel() transactionID := uint32(42) connectionID := uint64(0x0102030405060708) payload := make([]byte, 16) binary.BigEndian.PutUint32(payload[0:4], 0) binary.BigEndian.PutUint32(payload[4:8], transactionID) binary.BigEndian.PutUint64(payload[8:16], connectionID) got, err := parseUDPConnectResponse(payload, transactionID) if err != nil { t.Fatalf("parseUDPConnectResponse returned error: %v", err) } if got != connectionID { t.Fatalf("unexpected connection ID: got %x want %x", got, connectionID) } } func TestParseUDPAnnounceResponse(t *testing.T) { t.Parallel() transactionID := uint32(99) payload := make([]byte, 20+6) binary.BigEndian.PutUint32(payload[0:4], 1) binary.BigEndian.PutUint32(payload[4:8], transactionID) binary.BigEndian.PutUint32(payload[8:12], 1800) binary.BigEndian.PutUint32(payload[12:16], 10) binary.BigEndian.PutUint32(payload[16:20], 20) copy(payload[20:], []byte{127, 0, 0, 1, 0x1A, 0xE1}) peers, err := parseUDPAnnounceResponse(payload, transactionID) if err != nil { t.Fatalf("parseUDPAnnounceResponse returned error: %v", err) } if len(peers) != 1 { t.Fatalf("unexpected peer count: got %d", len(peers)) } if got, want := peers[0].Port, uint16(6881); got != want { t.Fatalf("unexpected peer port: got %d want %d", got, want) } } func TestBuildUDPAnnounceRequest(t *testing.T) { t.Parallel() tf := &torrentfile.TorrentFile{ Length: 100, } copy(tf.InfoHash[:], []byte("abcdefghijklmnopqrst")) var peerID [20]byte copy(peerID[:], []byte("-ZT0001-123456789012")) req, err := buildUDPAnnounceRequest(0x0102030405060708, 42, tf, AnnounceOptions{ PeerID: peerID, Port: 6881, Uploaded: -1, Downloaded: 20, NumWant: 50, }, 0xAABBCCDD) if err != nil { t.Fatalf("buildUDPAnnounceRequest returned error: %v", err) } if len(req) != 98 { t.Fatalf("unexpected announce request length: got %d", len(req)) } if got, want := binary.BigEndian.Uint32(req[8:12]), uint32(1); got != want { t.Fatalf("unexpected action: got %d want %d", got, want) } if got, want := binary.BigEndian.Uint64(req[64:72]), uint64(80); got != want { t.Fatalf("unexpected left bytes: got %d want %d", got, want) } if got, want := binary.BigEndian.Uint64(req[72:80]), uint64(0); got != want { t.Fatalf("unexpected uploaded bytes: got %d want %d", got, want) } } type fakeClient struct { do func(req *http.Request) (*http.Response, error) } func (c fakeClient) Do(req *http.Request) (*http.Response, error) { return c.do(req) } func equalBytes(a, b []byte) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }