main
  1require 'test_helper'
  2
  3class ClientTest < Minitest::Test
  4  attr_reader :subject
  5
  6  def setup
  7    @subject = Net::Hippie::Client.new
  8  end
  9
 10  def test_get
 11    VCR.use_cassette('get_breaches') do
 12      uri = URI.parse('https://haveibeenpwned.com/api/breaches')
 13      response = subject.get(uri)
 14      refute_nil response
 15      assert_equal(283, JSON.parse(response.body).count)
 16    end
 17  end
 18
 19  def test_multiple_gets_to_pypi
 20    VCR.use_cassette('multiple-gets-to-pypi') do
 21      %w{
 22        https://pypi.org/pypi/django/1.11.3/json
 23        https://pypi.org/pypi/Django/1.11.3/json
 24        https://pypi.org/pypi/docutils/0.13.1/json
 25        https://pypi.org/pypi/pytz/2019.2/json
 26        https://pypi.org/pypi/requests/2.5.3/json
 27      }.each do |url|
 28        subject = Net::Hippie::Client.new(follow_redirects: 3)
 29        response = subject.get(url)
 30        refute_nil response
 31        assert_equal Net::HTTPOK, response.class
 32        assert JSON.parse(response.body)
 33      end
 34    end
 35  end
 36
 37  def test_does_not_follow_redirect
 38    VCR.use_cassette('does_not_follow_redirect') do
 39      subject = Net::Hippie::Client.new(follow_redirects: 0)
 40      response = subject.get('https://pypi.org/pypi/django/1.11.3/json')
 41      refute_nil response
 42      assert_kind_of Net::HTTPRedirection, response
 43      assert response['location']
 44    end
 45  end
 46
 47  def test_does_follow_redirects
 48    VCR.use_cassette('does_follow_redirects') do
 49      subject = Net::Hippie::Client.new(follow_redirects: 10)
 50      response = subject.get('https://pypi.org/pypi/django/1.11.3/json')
 51      refute_nil response
 52      assert_kind_of Net::HTTPOK, response
 53      assert JSON.parse(response.body)
 54    end
 55  end
 56
 57  def test_follow_redirects_with_relative_paths
 58    VCR.use_cassette('follow_redirects_with_relative_paths') do
 59      subject = Net::Hippie::Client.new(follow_redirects: 10)
 60      response = subject.get("http://go.microsoft.com/fwlink/?LinkId=329770")
 61      refute_nil response
 62      assert_kind_of Net::HTTPOK, response
 63      assert response.body
 64    end
 65  end
 66
 67  def test_get_with_redirects
 68    url = 'https://www.example.org/'
 69    n = 10
 70    WebMock
 71      .stub_request(:get, url)
 72      .to_return(status: 301, headers: { 'Location' => url })
 73      .times(n)
 74      .then
 75      .to_return(status: 200, body: { success: true }.to_json)
 76
 77    subject = Net::Hippie::Client.new(follow_redirects: n)
 78    response = subject.get(url)
 79    refute_nil response
 80    assert_equal Net::HTTPOK, response.class
 81    assert JSON.parse(response.body)['success']
 82  end
 83
 84  def test_get_root_path
 85    VCR.use_cassette('get_root') do
 86      uri = URI.parse('https://www.mokhan.ca')
 87      response = subject.get(uri, headers: {})
 88      refute_nil response
 89      assert_equal response.code, "200"
 90      assert response.body.include?("<!DOCTYPE html>")
 91    end
 92  end
 93
 94  def test_get_with_retry
 95    uri = URI.parse('https://www.example.org/api/scim/v2/schemas')
 96    WebMock.stub_request(:get, uri.to_s)
 97      .to_timeout.then
 98      .to_timeout.then
 99      .to_timeout.then
100      .to_return(status: 200, body: { 'success' => 'true' }.to_json)
101    response = subject.with_retry(retries: 3) do |client|
102      client.get(uri)
103    end
104    refute_nil response
105    assert_equal Net::HTTPOK, response.class
106    assert_equal JSON.parse(response.body)['success'], 'true'
107  end
108
109  def test_exceeds_retries
110    uri = URI.parse('https://www.example.org/api/scim/v2/schemas')
111    WebMock.stub_request(:get, uri.to_s)
112      .to_timeout.then
113      .to_return(status: 200, body: { 'success' => 'true' }.to_json)
114
115    assert_raises Net::OpenTimeout do
116      subject.with_retry(retries: 0) do |client|
117        client.get(uri)
118      end
119    end
120  end
121
122  def test_get_with_string_uri
123    VCR.use_cassette('get_breaches') do
124      response = subject.get('https://haveibeenpwned.com/api/breaches')
125      refute_nil response
126      assert_equal(283, JSON.parse(response.body).count)
127    end
128  end
129
130  def test_get_with_generic_uri
131    VCR.use_cassette('get_breaches') do
132      uri = URI::Generic.build(host: 'haveibeenpwned.com', scheme: 'https', path: '/api/breaches', port: 443)
133      response = subject.get(uri)
134      refute_nil response
135      assert_equal(283, JSON.parse(response.body).count)
136    end
137  end
138
139  def test_get_with_block_syntax
140    VCR.use_cassette('get_breaches') do
141      uri = URI.parse('https://haveibeenpwned.com/api/breaches')
142      subject.get(uri) do |_request, response|
143        @response = response
144      end
145      refute_nil @response
146      assert_equal(283, JSON.parse(@response.body).count)
147    end
148  end
149
150  def test_get_with_headers
151    headers = { 'Accept' => 'application/vnd.haveibeenpwned.v2+json' }
152    WebMock.stub_request(:get, 'https://haveibeenpwned.com/api/breaches')
153      .with(headers: headers)
154      .to_return(status: 201, body: {}.to_json)
155
156    uri = URI.parse('https://haveibeenpwned.com/api/breaches')
157
158    response = subject.get(uri, headers: headers)
159    refute_nil response
160    assert_equal response.class, Net::HTTPCreated
161  end
162
163  def test_post_with_basic_auth_headers
164    VCR.use_cassette('post_basic_auth') do
165      uri = URI.parse('http://localhost:3000/oauth/tokens')
166      client_id = "79a1c787-5cac-4cc5-b00e-374f5a909ef8"
167      client_password = "za9NeRkm9YbqKDa6GreUmo6V"
168      authorization_code = "NGooUnxYq5f5DHvJqyzDhSft"
169      headers = { 'Authorization' => Net::Hippie.basic_auth(client_id, client_password) }
170      body = { grant_type: 'authorization_code', code: authorization_code }
171      response = subject.post(uri, headers: headers, body: body)
172      refute_nil response
173      json = JSON.parse(response.body, symbolize_names: true)
174      assert_equal('Bearer', json[:token_type])
175      assert(json[:token_type])
176    end
177  end
178
179  def test_get_with_bearer_auth_headers
180    VCR.use_cassette('get_bearer_auth') do
181      uri = URI.parse('http://localhost:3000/oauth/me')
182      token = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1NDE4NzEyMDksImlhdCI6MTU0MTg2NzYwOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwL21ldGFkYXRhIiwibmJmIjoxNTQxODY3NjA5LCJhdWQiOiI3OWExYzc4Ny01Y2FjLTRjYzUtYjAwZS0zNzRmNWE5MDllZjgiLCJqdGkiOiIzZmZmODNkYi1mMmM3LTRjMDUtYWY4Zi02NWM5ODU1ODUyZjciLCJzdWIiOiIwNDJhZjE5Ny1hOTQxLTRkNTctYjc4Ny00M2IxYmNjZDUwNzAiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIn0.HcwFAgg54RqKEONvHRigAavISuUUmkOA3gz0pkV6UEABCUHGucJCNehnvjpiwe4ZpCF_J6Uen6rfLFQOz7oYe416Du3_lQ3IS3Vc6hTpsT0XZ0bY-BVV_D9-thYFIrT7mNnNoxEs8AhOTBaAjgBammO_097MCwMjTGzAnxm1cmYfBad4yZPJ8HxDqeL769Urc6vz3Ku_M9yUzzfgb6jkwfFlvxmqHOPYWxlDY9uTR2uNr-ZYL5e6J6ZE8rgLkNRqy-jla03z2nFxEuxlSjYbBe60Vcwc4IyKS4QzbKXFXB1v9bKBvJxIUjQPQ7dICQeT9xSXQwDhnBtGUVcGM4njSH0-0rbxE470bGVslmYsChosX0PvRqlo4TMuVr7R5iuwWawZrIB-Dx3kkvhFYhn0jWrEEJkd96nLD-dmg2Tzqa40AE2WqmKtM5jM0LNO9E21l-hPQXAleoKspFIjT6Yd2Om4bJi-0eJB6sNqDuP55rvd5WSjp-ktrrtRirt-9aldCB_0eWP9oFCMJ_Xboq5w0P1W5MXlBv5p6eEdgRjohQyT-dkOvYsZiT9-Y5ggbBMhgtHm1CXZnutN0RE_skZk9PvxV_nUfTji3CHIaUhoJmeI11Tw2tdtOEt58RqkFgK7CZYylG7JqQS6eKpTLB2MrrZtCHY3rrrDaL64l6jYYQI"
183      headers = { 'Authorization' => Net::Hippie.bearer_auth(token) }
184      response = subject.get(uri, headers: headers)
185
186      refute_nil response
187      json = JSON.parse(response.body, symbolize_names: true)
188      assert(json[:sub])
189      assert(json[:exp])
190      assert(json[:iss])
191      assert(json[:nbf])
192    end
193  end
194
195  def test_get_with_body
196    uri = URI.parse('https://haveibeenpwned.com/api/breaches')
197    body = { 'hello' => 'world' }
198    WebMock.stub_request(:get, uri.to_s)
199      .with(body: body.to_json)
200      .to_return(status: 201, body: {}.to_json)
201
202    response = subject.get(uri, body: body)
203
204    refute_nil response
205    assert_equal response.class, Net::HTTPCreated
206  end
207
208  def test_post
209    VCR.use_cassette('post_breaches') do
210      uri = URI.parse('https://haveibeenpwned.com/api/breaches')
211      response = subject.post(uri)
212      refute_nil response
213      assert_equal 'Congratulations!', JSON.parse(response.body)['Message']
214    end
215  end
216
217  def test_post_with_block_syntax
218    VCR.use_cassette('post_breaches') do
219      uri = URI.parse('https://haveibeenpwned.com/api/breaches')
220      subject.post(uri) do |_request, response|
221        @response = response
222      end
223      refute_nil @response
224      assert_equal 'Congratulations!', JSON.parse(@response.body)['Message']
225    end
226  end
227
228  def test_put
229    VCR.use_cassette('put_breaches') do
230      uri = URI.parse('https://haveibeenpwned.com/api/breaches')
231      body = { command: 'echo hello' }
232      response = subject.put(uri, body: body)
233      refute_nil response
234      assert_equal 'Congratulations!', JSON.parse(response.body)['Message']
235    end
236  end
237
238  def test_put_with_block_syntax
239    VCR.use_cassette('put_breaches') do
240      uri = URI.parse('https://haveibeenpwned.com/api/breaches')
241      body = { command: 'echo hello' }
242      subject.put(uri, body: body) do |_request, response|
243        @response = response
244      end
245      refute_nil @response
246      assert_equal 'Congratulations!', JSON.parse(@response.body)['Message']
247    end
248  end
249
250  def test_delete
251    uri = URI.parse('https://haveibeenpwned.com/api/breaches')
252    VCR.use_cassette('delete_breaches') do
253      response = subject.delete(uri)
254      refute_nil response
255      assert_equal 'Congratulations!', JSON.parse(response.body)['Message']
256    end
257  end
258
259  def test_client_tls
260    private_key = OpenSSL::PKey::RSA.new(2048)
261    certificate = OpenSSL::X509::Certificate.new
262    certificate.not_after = certificate.not_before = Time.now
263    certificate.public_key = private_key.public_key
264    certificate.sign(private_key, OpenSSL::Digest::SHA256.new)
265
266    subject = Net::Hippie::Client.new(certificate: certificate.to_pem, key: private_key.export)
267    uri = URI.parse('https://haveibeenpwned.com/api/breaches')
268
269    @called = false
270    VCR.use_cassette('get_breaches') do
271      subject.get(uri) do |_request, response|
272        @called = true
273        refute_nil response
274        assert_equal '000webhost', JSON.parse(response.body)[0]['Title']
275      end
276    end
277    assert(@called)
278  end
279
280  def test_client_tls_with_passphrase
281    private_key = OpenSSL::PKey::RSA.new(2048)
282    passphrase = SecureRandom.hex(16)
283    certificate = OpenSSL::X509::Certificate.new
284    certificate.not_after = certificate.not_before = Time.now
285    certificate.public_key = private_key.public_key
286    certificate.sign(private_key, OpenSSL::Digest::SHA256.new)
287
288    subject = Net::Hippie::Client.new(
289      certificate: certificate.to_pem,
290      key: private_key.export(OpenSSL::Cipher.new('AES-256-CBC'), passphrase),
291      passphrase: passphrase
292    )
293    uri = URI.parse('https://haveibeenpwned.com/api/breaches')
294
295    @called = false
296    VCR.use_cassette('get_breaches') do
297      subject.get(uri) do |_request, response|
298        @called = true
299        refute_nil response
300        assert_equal '000webhost', JSON.parse(response.body)[0]['Title']
301      end
302    end
303    assert(@called)
304  end
305
306  def test_logger
307    VCR.turned_off do
308      WebMock.allow_net_connect!
309      StringIO.open do |io|
310        subject = Net::Hippie::Client.new(logger: Logger.new(io, level: :debug))
311        response = subject.get('https://www.example.org/')
312
313        refute_nil response
314        assert_kind_of Net::HTTPOK, response
315        io.rewind
316        assert_match %r{^(opening connection to www.example.org:443)}, io.read
317      end
318    end
319  end
320
321  def test_post_with_streaming
322    chunks = ["chunk1", "chunk2", "chunk3"]
323    response_body = chunks.join
324
325    WebMock.stub_request(:post, "https://example.org/stream")
326      .to_return(status: 200, body: response_body)
327
328    uri = URI.parse('https://example.org/stream')
329    received_chunks = []
330
331    subject.post(uri, body: { test: true }) do |response|
332      response.read_body do |chunk|
333        received_chunks << chunk
334      end
335    end
336
337    assert_equal chunks.join, received_chunks.join
338  end
339
340  def test_get_with_streaming
341    chunks = ["data1\n", "data2\n", "data3\n"]
342    response_body = chunks.join
343
344    WebMock.stub_request(:get, "https://example.org/stream")
345      .to_return(status: 200, body: response_body)
346
347    uri = URI.parse('https://example.org/stream')
348    received_data = []
349
350    subject.get(uri) do |response|
351      response.read_body do |chunk|
352        received_data << chunk
353      end
354    end
355
356    assert_equal chunks.join, received_data.join
357  end
358end