// // TorrentHTTPTrackerTests.swift // BitTorrentTests // // Created by Ben Davis on 27/06/2017. // Copyright © 2017 Ben Davis. All rights reserved. // import XCTest @testable import BitTorrent class TorrentTrackerDelegateSpy: TorrentTrackerDelegate { var receivedResponseCalled = false var receivedResponseParameter: TorrentTrackerResponse? = nil var receivedErrorMessageCalled = false var receivedErrorMessageParameter: String? = nil func torrentTracker(_ sender: Any, receivedResponse response: TorrentTrackerResponse) { receivedResponseCalled = true receivedResponseParameter = response } func torrentTracker(_ sender: Any, receivedErrorMessage errorMessage: String) { receivedErrorMessageCalled = true receivedErrorMessageParameter = errorMessage } } class TorrentHTTPTrackerTests: XCTestCase { var connectionStub: HTTPConnectionStub! var sut: TorrentHTTPTracker! var delegateSpy: TorrentTrackerDelegateSpy! let expectedURLParameters: [String: String] = [ "info_hash": "%07%08%09", "peer_id" : "peerId", "port" : "123", "uploaded" : "1234", "downloaded" : "4321", "left" : "456", "compact" : "1", "event" : "started", "numwant" : "321", ] let basicResponseData = "d8:completei1e10:incompletei2e8:intervali600e5:peers0:e".data(using: .ascii)! override func setUp() { super.setUp() connectionStub = HTTPConnectionStub() let url = URL(string: "http://127.0.0.1:53420/announce")! sut = TorrentHTTPTracker(announceURL: url, connection: connectionStub) delegateSpy = TorrentTrackerDelegateSpy() sut.delegate = delegateSpy } func performAnnounce(withEvent event: TorrentTrackerEvent) { sut.announceClient(with: "peerId", port: 123, event: event, infoHash: Data(bytes: [7,8,9]), numberOfBytesRemaining: 456, numberOfBytesUploaded: 1234, numberOfBytesDownloaded: 4321, numberOfPeersToFetch: 321) } func test_announce() { performAnnounce(withEvent: .started) let request = connectionStub.lastRequest XCTAssertEqual(request.url.absoluteString, "http://127.0.0.1:53420/announce") XCTAssertEqual(request.urlParameters!, expectedURLParameters) } func test_sendStoppedEvent() { performAnnounce(withEvent: .stopped) let request = connectionStub.lastRequest XCTAssertEqual(request.urlParameters!["event"], "stopped") } func test_sendCompletedEvent() { performAnnounce(withEvent: .completed) let request = connectionStub.lastRequest XCTAssertEqual(request.urlParameters!["event"], "completed") } func test_delegateNotifiedOnTrackerResponse() { performAnnounce(withEvent: .started) connectionStub.completeLastRequest(with: HTTPResponse(completed: true, responseData: basicResponseData, statusCode: 200)) XCTAssert(delegateSpy.receivedResponseCalled) } func test_basicResponseParsing() { performAnnounce(withEvent: .started) connectionStub.completeLastRequest(with: HTTPResponse(completed: true, responseData: basicResponseData, statusCode: 200)) let response = delegateSpy.receivedResponseParameter! XCTAssertEqual(response.numberOfPeersComplete, 1) XCTAssertEqual(response.numberOfPeersIncomplete, 2) XCTAssertNil(response.trackerId) XCTAssertEqual(response.interval, 600) XCTAssertEqual(response.minimumInterval, 0) XCTAssertNil(response.warning) } func test_optionalResponseFieldsParsing() { performAnnounce(withEvent: .started) let completeResponse = "d15:warning message7:warning10:tracker id9:trackerId12:min intervali60e8:completei1e10:incompletei2e8:intervali600e5:peers0:e".data(using: .ascii)! connectionStub.completeLastRequest(with: HTTPResponse(completed: true, responseData: completeResponse, statusCode: 200)) let response = delegateSpy.receivedResponseParameter! XCTAssertEqual(response.trackerId, "trackerId".data(using: .ascii)) XCTAssertEqual(response.minimumInterval, 60) XCTAssertEqual(response.warning, "warning") } func test_binaryPeersFormat() { performAnnounce(withEvent: .started) let peersBinary = Data(bytes: [0x7f, 0x00, 0x00, 0x01, 0x3c, 0x17, 0x7f, 0x00, 0x00, 0x01, 0x1a, 0xe1]) let responseData = "d8:completei1e10:incompletei2e8:intervali600e5:peers12:".data(using: .ascii)! + peersBinary + "e".data(using: .ascii)! connectionStub.completeLastRequest(with: HTTPResponse(completed: true, responseData: responseData, statusCode: 200)) let response = delegateSpy.receivedResponseParameter! XCTAssertEqual(response.peers.count, 2) XCTAssertEqual(response.peers.first!.ip, "127.0.0.1") XCTAssertEqual(response.peers.first!.port, 15383) XCTAssertEqual(response.peers.last!.ip, "127.0.0.1") XCTAssertEqual(response.peers.last!.port, 6881) } func test_dictionaryPeersFormat() { performAnnounce(withEvent: .started) let peer1Id = "peerId1-------------" let peer1IP = "127.0.0.1" let peer1Port: UInt16 = 15383 let peer2Id = "peerId2-------------" let peer2IP = "127.0.0.1" let peer2Port: UInt16 = 6881 let peer1 = "d7:peer id20:\(peer1Id)2:ip9:\(peer1IP)4:porti\(peer1Port)ee" let peer2 = "d7:peer id20:\(peer2Id)2:ip9:\(peer2IP)4:porti\(peer2Port)ee" let responseData = "d8:completei1e10:incompletei2e8:intervali600e5:peersl\(peer1)\(peer2)ee".data(using: .ascii)! connectionStub.completeLastRequest(with: HTTPResponse(completed: true, responseData: responseData, statusCode: 200)) let response = delegateSpy.receivedResponseParameter! XCTAssertEqual(response.peers.count, 2) XCTAssertEqual(response.peers.first!.peerId!, peer1Id.data(using: .ascii)) XCTAssertEqual(response.peers.first!.ip, peer1IP) XCTAssertEqual(response.peers.first!.port, peer1Port) XCTAssertEqual(response.peers.last!.peerId!, peer2Id.data(using: .ascii)) XCTAssertEqual(response.peers.last!.ip, peer2IP) XCTAssertEqual(response.peers.last!.port, peer2Port) } func test_failResponseDoesNotCallDelegate() { performAnnounce(withEvent: .started) let responseData = "d14:failure reason45:invalid info_hash (not 20 chars):123length: 3e".data(using: .ascii) connectionStub.completeLastRequest(with: HTTPResponse(completed: true, responseData: responseData, statusCode: 200)) XCTAssertFalse(delegateSpy.receivedResponseCalled) XCTAssert(delegateSpy.receivedErrorMessageCalled) XCTAssertEqual(delegateSpy.receivedErrorMessageParameter, "invalid info_hash (not 20 chars):123length: 3") } func test_invalidTrackerResponse() { performAnnounce(withEvent: .started) connectionStub.completeLastRequest(with: HTTPResponse(completed: true, responseData: Data(), statusCode: 500)) XCTAssertFalse(delegateSpy.receivedResponseCalled) XCTAssertFalse(delegateSpy.receivedErrorMessageCalled) } }