diff --git a/.gitignore b/.gitignore index 769663d..6a2bce8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .yardoc doc - +Gemfile.lock +coverage +Guardfile \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..f2c1cec --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +-c +-fd diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a34a30f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +rvm: + - 1.9.3 + - 2.0.0 + - 2.1.10 + - 2.2.5 + - 2.3.4 + - 2.4.1 +addons: + code_climate: + repo_token: 3004a7854020df72de40cef4c1f7fc1992ca1969b5420f548df7f4ab6a9b881b diff --git a/Gemfile b/Gemfile index 0fc559f..0247523 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,7 @@ source 'https://rubygems.org' -gem 'bencode' +group :test do + gem "json", Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2") ? "< 2" : ">= 2" +end + +gemspec diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 78d3c43..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,10 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - bencode (0.8.0) - -PLATFORMS - ruby - -DEPENDENCIES - bencode diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fa020fc --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 54a1462..0b76a6b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Torckapi — torrent tracker querying API [![Gem Version](https://img.shields.io/gem/v/torckapi.svg)](https://rubygems.org/gems/torckapi) -[![Dependencies Status](https://img.shields.io/gemnasium/krupenik/torckapi.svg)](https://gemnasium.com/krupenik/torckapi) -[![Code Climate](https://img.shields.io/codeclimate/github/krupenik/torckapi.svg)](https://codeclimate.com/github/krupenik/torckapi) +[![Dependency Status](https://gemnasium.com/badges/github.com/1v/torckapi.svg)](https://gemnasium.com/github.com/1v/torckapi) +[![Build Status](https://travis-ci.org/1v/torckapi.svg?branch=master)](https://travis-ci.org/1v/torckapi) +[![Code Climate](https://codeclimate.com/github/1v/torckapi/badges/gpa.svg)](https://codeclimate.com/github/1v/torckapi) +[![Test Coverage](https://codeclimate.com/github/1v/torckapi/badges/coverage.svg)](https://codeclimate.com/github/1v/torckapi/coverage) ## Description @@ -26,14 +28,17 @@ tracker.announce("0123456789ABCDEF0123456789ABCDEF01234567") # @seeders=1, # @peers=[["127.0.0.1", 54078], ["127.0.0.2", 43666]]> -tracker.scrape(["0123456789ABCDEF0123456789ABCDEF01234567", "123456789ABCDEF0123456789ABCDEF012345678"]) +tracker.scrape(["0123456789ABCDEF0123456789ABCDEF01234567", + "123456789ABCDEF0123456789ABCDEF012345678"]) # => #{:seeders=>3, :completed=>0, :leechers=>13}, -# "123456789ABCDEF0123456789ABCDEF012345678"=>{:seeders=>4, :completed=>10, :leechers=>8}}> +# @data={"0123456789ABCDEF0123456789ABCDEF01234567"=>{:seeders=>3, +# :completed=>0, +# :leechers=>13}, +# "123456789ABCDEF0123456789ABCDEF012345678"=>{:seeders=>4, +# :completed=>10, +# :leechers=>8}}> ``` ## TODO - -Add tests. - -Document everything. +* Add tests. +* Document everything. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..61a6f66 --- /dev/null +++ b/Rakefile @@ -0,0 +1,11 @@ +#!/usr/bin/env rake +require 'rake' +require "bundler/gem_tasks" + +require 'rspec/core' +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList['spec/**/*_spec.rb'] +end + +task :default => :spec diff --git a/lib/torckapi.rb b/lib/torckapi.rb index 670d306..f42e489 100644 --- a/lib/torckapi.rb +++ b/lib/torckapi.rb @@ -12,16 +12,17 @@ module Torckapi # @param tracker_url [String] tracker announce url # @param options [Hash] defaults to \\{timeout: 15, tries: 3} # @return [Torckapi::Tracker::Base] tracker interface instance - def self.tracker tracker_url, options={} + def self.tracker(tracker_url, options = {}) url = URI.parse tracker_url case url.scheme when "http" - Torckapi::Tracker::HTTP.new url, options + Torckapi::Tracker::HTTP.new(url, options) when "udp" - Torckapi::Tracker::UDP.new url, options + Torckapi::Tracker::UDP.new(url, options) else - raise InvalidSchemeError, "'#{tracker_url}' cannot be recognized as valid tracker url" + raise Torckapi::Tracker::InvalidSchemeError, \ + "'#{tracker_url}' cannot be recognized as valid tracker url" end end end diff --git a/lib/torckapi/response/announce.rb b/lib/torckapi/response/announce.rb index faeddb2..2158678 100644 --- a/lib/torckapi/response/announce.rb +++ b/lib/torckapi/response/announce.rb @@ -20,7 +20,7 @@ class Announce < Base # @param info_hash [String] 40-char hexadecimal string # @param data [String] UDP response data (omit action and transaction_id) # @return [Torckapi::Response::Announce] response - def self.from_udp info_hash, data + def self.from_udp(info_hash, data) new info_hash, *data[4..11].unpack('L>2'), peers_from_compact(data[12..-1] || '') end @@ -29,21 +29,21 @@ def self.from_udp info_hash, data # @param data [String] HTTP response data (bencoded) # @return [Torckapi::Response::Announce] response # @raise [Torckapi::Tracker::MalformedResponseError] - def self.from_http info_hash, data + def self.from_http(info_hash, data) bdecoded_data = bdecode_and_check data, 'peers' new info_hash, *bdecoded_data.values_at('incomplete', 'complete'), peers_from_compact(bdecoded_data['peers']) end private - def initialize info_hash, leechers, seeders, peers + def initialize(info_hash, leechers, seeders, peers) @info_hash = info_hash @leechers = leechers @seeders = seeders @peers = peers end - def self.peers_from_compact data + def self.peers_from_compact(data) # ipv4 address + tcp/udp port = 6 bytes data.unpack('a6' * (data.length / 6)).map { |i| [IPAddr.ntop(i[0..3]), i[4..5].unpack('S>')[0]] } end diff --git a/lib/torckapi/response/base.rb b/lib/torckapi/response/base.rb index 44a0965..f742a28 100644 --- a/lib/torckapi/response/base.rb +++ b/lib/torckapi/response/base.rb @@ -5,14 +5,14 @@ module Response class Base protected - def self.bdecode_and_check data, key + def self.bdecode_and_check(data, key) begin - bdecoded_data = BEncode.load(data) + bdecoded_data = BEncode.load(data, :ignore_trailing_junk => true) rescue BEncode::DecodeError - raise Torckapi::Tracker::MalformedResponseError + raise Torckapi::Tracker::MalformedResponseError, "Can't decode '%s'" % data end - raise Torckapi::Tracker::MalformedResponseError unless bdecoded_data.is_a? Hash and bdecoded_data.has_key? key + raise Torckapi::Tracker::MalformedResponseError, "bdecoded data: '%s'" % bdecoded_data + " didn't contain key: '%s'" % key unless bdecoded_data.is_a? Hash and bdecoded_data.has_key? key bdecoded_data end diff --git a/lib/torckapi/response/error.rb b/lib/torckapi/response/error.rb index 549e16f..58f1099 100644 --- a/lib/torckapi/response/error.rb +++ b/lib/torckapi/response/error.rb @@ -16,7 +16,7 @@ class Error < Base # @param info_hashes [String, Array] a 40-char hexadecimal string or an array of those # @param data [String] UDP response data (omit action and transaction_id) # @return [Torckapi::Response::Error] response - def self.from_udp info_hashes, data + def self.from_udp(info_hashes, data) new(info_hashes, data || "") end @@ -26,7 +26,7 @@ def info_hash private - def initialize info_hashes, message + def initialize(info_hashes, message) @info_hashes = [*info_hashes] @message = message end diff --git a/lib/torckapi/response/scrape.rb b/lib/torckapi/response/scrape.rb index ab5e70b..9a043dd 100644 --- a/lib/torckapi/response/scrape.rb +++ b/lib/torckapi/response/scrape.rb @@ -12,7 +12,7 @@ class Scrape < Base # @param data [String] UDP response data (omit action and transaction_id) # @return [Torckapi::Response::Scrape] response # @raise [Torckapi::Tracker::MalformedResponseError] - def self.from_udp info_hashes, data + def self.from_udp(info_hashes, data) raise Torckapi::Tracker::MalformedResponseError if data.length != info_hashes.count * 12 new Hash[info_hashes.zip(data.unpack('a12' * info_hashes.count).map { |i| counts_unpacked(i) })] end @@ -21,26 +21,26 @@ def self.from_udp info_hashes, data # @param data [String] HTTP response data (bencoded) # @return [Torckapi::Response::Scrape] response # @raise [Torckapi::Tracker::MalformedResponseError] - def self.from_http data + def self.from_http(data) bdecoded_data = bdecode_and_check data, 'files' new Hash[bdecoded_data['files'].map { |info_hash, counts| [info_hash.unpack('H*').join, counts_translated(counts) ]}] end private - def self.counts_unpacked data + def self.counts_unpacked(data) counts_with_block(data) { |data| data.unpack('L>3').map(&:to_i) } end - def self.counts_translated data + def self.counts_translated(data) counts_with_block(data) { |data| data.values_at('complete', 'downloaded', 'incomplete') } end - def self.counts_with_block data, &block + def self.counts_with_block(data, &block) Hash[[:seeders, :completed, :leechers].zip(yield data)] end - def initialize data + def initialize(data) @data = data end end diff --git a/lib/torckapi/tracker/base.rb b/lib/torckapi/tracker/base.rb index d44bed8..0a711e7 100644 --- a/lib/torckapi/tracker/base.rb +++ b/lib/torckapi/tracker/base.rb @@ -12,7 +12,7 @@ class Base # @raise [Torckapi::InvalidInfohashError] when supplied with invalid info_hash # @raise [Torckapi::Tracker::CommunicationFailedError] when tracker haven't responded at all # @raise [Torckapi::Tracker::MalformedResponseError] when tracker returned junk - def announce info_hash, peer_id=SecureRandom.random_bytes(20) + def announce(info_hash, peer_id = SecureRandom.random_bytes(20)) raise Torckapi::InvalidInfohashError if info_hash !~ /\A[0-9a-f]{40}\z/i end @@ -22,15 +22,15 @@ def announce info_hash, peer_id=SecureRandom.random_bytes(20) # @raise [Torckapi::InvalidInfohashError] when supplied with invalid info_hash # @raise [Torckapi::Tracker::CommunicationFailedError] when tracker haven't responded at all # @raise [Torckapi::Tracker::MalformedResponseError] when tracker returned junk - def scrape info_hashes=[] + def scrape(info_hashes = []) raise Torckapi::InvalidInfohashError if info_hashes.any? { |i| i !~ /\A[0-9a-f]{40}\z/i } end private - def initialize url, options={} + def initialize(url, options = {}) @url = url - @options = {timeout: 15, tries: 7}.merge(options) + @options = {timeout: 15, tries: 2}.merge(options) end end end diff --git a/lib/torckapi/tracker/http.rb b/lib/torckapi/tracker/http.rb index 856376f..355bfac 100644 --- a/lib/torckapi/tracker/http.rb +++ b/lib/torckapi/tracker/http.rb @@ -6,13 +6,13 @@ module Tracker class HTTP < Base # (see Base#announce) - def announce info_hash + def announce(info_hash) super Torckapi::Response::Announce.from_http(info_hash, perform_request(url_for(@url.dup, Announce, info_hash))) end # (see Base#scrape) - def scrape info_hashes=[] + def scrape(info_hashes = []) super Torckapi::Response::Scrape.from_http(perform_request(url_for(@url.dup, Scrape, info_hashes))) end @@ -21,26 +21,42 @@ def scrape info_hashes=[] REQUEST_ACTIONS = [Announce = 1, Scrape = 2].freeze - def initialize url, options={} + def initialize(url, options = {}) super @url.query ||= "" end - def url_for url, action, data + def url_for(url, action, data) url.query += info_hash_params [*data] url.path.gsub!(/announce/, 'scrape') if Scrape == action url end - def perform_request url + def perform_request(url) + + tries = 0 + begin - Net::HTTP.get(url) - rescue Errno::ECONNRESET, Errno::ETIMEDOUT, Timeout::Error - raise CommunicationFailedError + timeout = @options[:timeout] + request_url = if Gem::Version.new(RUBY_VERSION) <= Gem::Version.new('1.9.3') + url.to_s + else + url + end + request = Net::HTTP::Get.new(request_url) + Net::HTTP.start(url.host, url.port, open_timeout: timeout, read_timeout: timeout) do |http| + http.request(request).body + end + rescue Errno::ECONNRESET, Errno::ETIMEDOUT, Timeout::Error, Errno::ECONNREFUSED + if (tries += 1) <= @options[:tries] + retry # backs up to just after the "begin" + else + raise CommunicationFailedError + end end end - def info_hash_params info_hashes + def info_hash_params(info_hashes) info_hashes.map { |i| "info_hash=%s" % URI.encode([i].pack('H*')) }.join('&') end end diff --git a/lib/torckapi/tracker/udp.rb b/lib/torckapi/tracker/udp.rb index 8a0ad2d..887a2c4 100644 --- a/lib/torckapi/tracker/udp.rb +++ b/lib/torckapi/tracker/udp.rb @@ -1,5 +1,4 @@ require 'socket' -require 'timeout' require 'securerandom' require 'torckapi/tracker/base' @@ -7,14 +6,14 @@ module Torckapi module Tracker # Implementation of http://www.bittorrent.org/beps/bep_0015.html class UDP < Base - def announce info_hash, peer_id=SecureRandom.random_bytes(20) + def announce(info_hash, peer_id = SecureRandom.random_bytes(20)) super - perform_request Announce, announce_request_data(info_hash, peer_id), info_hash + perform_request(Announce, announce_request_data(info_hash, peer_id), info_hash) end - def scrape info_hashes=[] + def scrape(info_hashes = []) super - perform_request Scrape, scrape_request_data(info_hashes), info_hashes + perform_request(Scrape, scrape_request_data(info_hashes), info_hashes) end private @@ -25,7 +24,7 @@ def scrape info_hashes=[] RESPONSE_MIN_LENGTHS = [16, 20, 8, 8].freeze RESPONSE_CODES = 0..RESPONSE_CLASSES.length - def initialize url, options={} + def initialize(url, options = {}) super @state = nil @connection_id = nil @@ -40,17 +39,17 @@ def connecting? @state == :connecting end - def perform_request action, data, *args - response = communicate action, data + def perform_request(action, data, *args) + response = communicate(action, data) RESPONSE_CLASSES[response[:code]].from_udp(*args, response[:data]) end - def announce_request_data info_hash, peer_id + def announce_request_data(info_hash, peer_id) [[info_hash].pack('H*'), peer_id, [0, 0, 0, 0, 0, 0, -1, 0].pack('Q>3L>4S>')].join end - def scrape_request_data info_hashes + def scrape_request_data(info_hashes) info_hashes.map { |i| [i].pack('H*') }.join end @@ -58,49 +57,48 @@ def connect return if connected? || connecting? @state, @connection_id = :connecting, [0x041727101980].pack('Q>') - response = communicate Connect + response = communicate(Connect) @state, @connection_id = nil, response[:data] end - def communicate action, data=nil + def communicate(action, data = nil) @socket ||= UDPSocket.new - tries = 0 response = nil - begin - timeout = @options[:timeout] * (2 ** tries) connect transaction_id = SecureRandom.random_bytes(4) packet = [@connection_id, [action].pack('L>'), transaction_id, data].join - - Timeout::timeout(timeout, CommunicationTimeoutError) do - @socket.send(packet, 0, @url.host, @url.port) - response = process_response @socket.recvfrom(65536)[0], transaction_id + @socket.send(packet, 0, @url.host, @url.port) + if IO.select([@socket], nil, nil, @options[:timeout]) + response = process_response(@socket.recvfrom(65536)[0], transaction_id) @communicated_at = Time.now + else + raise CommunicationTimeoutError end + return response rescue CommunicationTimeoutError, LittleEndianResponseError => e - retry if (tries += 1) <= @options[:tries] - end - - raise CommunicationFailedError unless response - - response - end + if (tries += 1) <= @options[:tries] + retry + else + raise CommunicationFailedError + end + end + end - def process_response response, transaction_id - check_transaction_id response, transaction_id - response_code = extract_response_code response - check_response_length response, response_code + def process_response(response, transaction_id) + check_transaction_id(response, transaction_id) + response_code = extract_response_code(response) + check_response_length(response, response_code) {code: response_code, data: response[8..-1]} end - def check_transaction_id response, transaction_id + def check_transaction_id(response, transaction_id) raise TransactionIdMismatchError, response if transaction_id != response[4..7] end - def extract_response_code response + def extract_response_code(response) response_code, response_code_le = [response[0..3]].flat_map { |x| [x.unpack('L>')[0], x.unpack('L<')[0]] } unless RESPONSE_CODES.include?(response_code) @@ -110,7 +108,7 @@ def extract_response_code response response_code end - def check_response_length response, response_code + def check_response_length(response, response_code) raise MalformedResponseError, response if RESPONSE_MIN_LENGTHS[response_code] > response.length end end diff --git a/spec/fixtures/http_announce.txt b/spec/fixtures/http_announce.txt new file mode 100644 index 0000000..2bb9ecf Binary files /dev/null and b/spec/fixtures/http_announce.txt differ diff --git a/spec/fixtures/http_scrape.txt b/spec/fixtures/http_scrape.txt new file mode 100644 index 0000000..a2d3f01 --- /dev/null +++ b/spec/fixtures/http_scrape.txt @@ -0,0 +1 @@ +d5:filesd20:W?/-n2'T1,%d8:completei1893e10:downloadedi10e10:incompletei242ee20:⌮v{mS 6jd8:completei1768e10:downloadedi10e10:incompletei259eeee \ No newline at end of file diff --git a/spec/fixtures/udp_announce.txt b/spec/fixtures/udp_announce.txt new file mode 100644 index 0000000..065cd5c Binary files /dev/null and b/spec/fixtures/udp_announce.txt differ diff --git a/spec/fixtures/udp_scrape.txt b/spec/fixtures/udp_scrape.txt new file mode 100644 index 0000000..c5a087e Binary files /dev/null and b/spec/fixtures/udp_scrape.txt differ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..ff6a839 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,19 @@ +require "simplecov" +SimpleCov.start + +require 'torckapi' + +RSpec.configure do |config| + def fixture(filename) + File.dirname(__FILE__) + '/fixtures/' + filename + end + + def make_response(filename_or_string) + open(fixture(filename_or_string + '.txt'), "r:UTF-8").read + end + + def init_torckapi + @http_tracker = Torckapi.tracker("http://localhost/") + @udp_tracker = Torckapi.tracker("udp://localhost/") + end +end diff --git a/spec/torckapi_spec.rb b/spec/torckapi_spec.rb new file mode 100644 index 0000000..6763220 --- /dev/null +++ b/spec/torckapi_spec.rb @@ -0,0 +1,156 @@ +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') + +describe Torckapi do + + before do + init_torckapi + + http = double + allow(Net::HTTP).to receive(:start).and_yield http + allow(http).to \ + receive(:request).with(an_instance_of(Net::HTTP::Get)) \ + .and_return(Net::HTTPResponse) + + @udp = double + allow(UDPSocket).to receive(:new).and_return @udp + allow(@udp).to receive(:send).and_return @udp + allow(IO).to receive(:select).and_return true + + allow(SecureRandom).to receive(:random_bytes).and_call_original + end + + describe "#tracker" do + it "method should exist" do + expect(Torckapi).to respond_to(:tracker) + end + + it "require at least one argument" do + expect { Torckapi.tracker }.to raise_error(ArgumentError) + expect(@http_tracker).to be_truthy + end + + it "should set default options" do + expect(@http_tracker.instance_variable_get(:@options)).to \ + eql({:timeout=>15, :tries=>2}) + end + + it "should not allow any protocols except HTTP or UDP" do + expect { Torckapi.tracker("https://localhost/") }.to \ + raise_error(Torckapi::Tracker::InvalidSchemeError) + end + end + + describe "#announce" do + it "method should exist" do + expect(@http_tracker).to respond_to(:announce) + expect(@udp_tracker).to respond_to(:announce) + end + + it "should decline not valid hashes" do + expect { + @http_tracker.announce(SecureRandom.hex(19)) + }.to raise_error(Torckapi::InvalidInfohashError) + expect { + @http_tracker.announce(SecureRandom.hex(21)) + }.to raise_error(Torckapi::InvalidInfohashError) + expect { + @udp_tracker.announce(SecureRandom.hex(19)) + }.to raise_error(Torckapi::InvalidInfohashError) + expect { + @udp_tracker.announce(SecureRandom.hex(21)) + }.to raise_error(Torckapi::InvalidInfohashError) + end + + context "should recieve response from" do + it "http" do + test_hash = SecureRandom.hex(20) + # File.write('spec/fixtures/udp_scrape.txt', "") + + allow(Net::HTTPResponse).to receive(:body) + .and_return(make_response("http_announce")) + + tr = @http_tracker.announce(test_hash) + expect(tr.info_hash).to eql(test_hash) + expect(tr.leechers).to eql(23) + expect(tr.seeders).to eql(833) + + end + + it "udp" do + test_hash = SecureRandom.hex(20) + # File.open("spec/fixtures/ceaf...8ec3b83.txt", "w:UTF-8") do |f| + # f.write "" + # end + # File.write('spec/fixtures/ceafca...58ec3b83.txt', "", "w:UTF-8") + + allow(@udp).to receive(:recvfrom).with(65536) + .and_return([make_response("udp_announce") + .force_encoding("ASCII-8BIT")]) + allow(SecureRandom).to receive(:random_bytes).with(4) + .and_return("\x1D\e\xFDp".force_encoding("ASCII-8BIT")) + + tr = @udp_tracker.announce(test_hash) + expect(tr.info_hash).to eql(test_hash) + expect(tr.leechers).to eql(5038) + expect(tr.seeders).to eql(7244) + end + end + end + + describe "#scrape" do + it "method should exist" do + expect(@http_tracker).to respond_to(:scrape) + expect(@udp_tracker).to respond_to(:scrape) + end + + it "should decline not valid hashes" do + expect { + @http_tracker.scrape([SecureRandom.hex(19), SecureRandom.hex(21)]) + }.to raise_error(Torckapi::InvalidInfohashError) + expect { + @udp_tracker.scrape([SecureRandom.hex(19), SecureRandom.hex(21)]) + }.to raise_error(Torckapi::InvalidInfohashError) + end + + context "should recieve response from" do + before do + @test_hash = ["573f1b2f2d11dcc06eaa9b322754312c15bb9b25", + "bce28cae76f27bb36d5380e7dc1090a109bd366a"] + end + + it "http" do + allow(Net::HTTPResponse).to receive(:body) \ + .and_return(make_response("http_scrape")) + + tr = @http_tracker.scrape(@test_hash) + expect(tr).to respond_to(:data) + + tr = tr.data + expect(tr).to have_key(@test_hash[0]) + expect(tr).to have_key(@test_hash[1]) + expect(tr[@test_hash[0]][:seeders]).to eql(1893) + expect(tr[@test_hash[0]][:leechers]).to eql(242) + expect(tr[@test_hash[0]][:completed]).to eql(10) + end + it "udp" do + allow(@udp).to receive(:recvfrom).with(65536) + .and_return([make_response("udp_scrape") + .force_encoding("ASCII-8BIT")]) + allow(SecureRandom).to receive(:random_bytes).with(4) + .and_return("\xAF+\x97\xEA".force_encoding("ASCII-8BIT")) + tr = @udp_tracker.scrape(@test_hash) + + expect(tr).to respond_to(:data) + + tr = tr.data + expect(tr).to have_key(@test_hash[0]) + expect(tr).to have_key(@test_hash[1]) + expect(tr[@test_hash[0]][:seeders]).to eql(26) + expect(tr[@test_hash[0]][:leechers]).to eql(6) + expect(tr[@test_hash[0]][:completed]).to eql(160) + + end + end + end + +end diff --git a/torckapi.gemspec b/torckapi.gemspec index 565178d..fc074a4 100644 --- a/torckapi.gemspec +++ b/torckapi.gemspec @@ -17,8 +17,10 @@ Gem::Specification.new do |gem| gem.licenses = ["MIT"] - gem.add_development_dependency 'rake', '~> 0' - gem.add_development_dependency 'minitest', '~> 0' - - gem.add_runtime_dependency 'bencode', '~> 0' + gem.add_dependency 'bencode', '~> 0' + + gem.add_development_dependency 'rake' + gem.add_development_dependency 'rspec', '~> 3.2' + gem.add_development_dependency 'simplecov' + gem.add_development_dependency 'codeclimate-test-reporter', '~> 1.0' end