Ztorrent/internal/tracker/tracker_test.go
2026-03-06 12:52:01 +03:00

311 lines
8.2 KiB
Go

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
}