restify 2.0.2 → 3.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/README.md +31 -5
  4. data/lib/restify/adapter/base.rb +11 -2
  5. data/lib/restify/adapter/ethon.rb +462 -0
  6. data/lib/restify/adapter/loop_lock.rb +124 -0
  7. data/lib/restify/adapter/telemetry.rb +6 -4
  8. data/lib/restify/adapter/typhoeus.rb +48 -23
  9. data/lib/restify/cache.rb +1 -1
  10. data/lib/restify/context.rb +5 -5
  11. data/lib/restify/error.rb +13 -0
  12. data/lib/restify/global.rb +9 -30
  13. data/lib/restify/logging.rb +10 -4
  14. data/lib/restify/processors/base/parsing.rb +13 -1
  15. data/lib/restify/promise.rb +20 -2
  16. data/lib/restify/registry.rb +2 -2
  17. data/lib/restify/relation.rb +9 -17
  18. data/lib/restify/request.rb +2 -2
  19. data/lib/restify/resource.rb +20 -2
  20. data/lib/restify/response.rb +41 -5
  21. data/lib/restify/timeout.rb +11 -5
  22. data/lib/restify/version.rb +3 -3
  23. data/lib/restify/webmock.rb +126 -0
  24. data/lib/restify.rb +5 -1
  25. data/spec/restify/adapter/base_spec.rb +33 -0
  26. data/spec/restify/adapter/ethon_spec.rb +263 -0
  27. data/spec/restify/adapter/loop_lock_spec.rb +144 -0
  28. data/spec/restify/adapter/telemetry_spec.rb +61 -0
  29. data/spec/restify/adapter/typhoeus_spec.rb +26 -0
  30. data/spec/restify/cache_spec.rb +1 -3
  31. data/spec/restify/context_spec.rb +57 -2
  32. data/spec/restify/features/opentelemetry_spec.rb +1 -1
  33. data/spec/restify/features/protocols_spec.rb +28 -0
  34. data/spec/restify/features/redirects_spec.rb +45 -0
  35. data/spec/restify/features/request_errors_spec.rb +1 -1
  36. data/spec/restify/features/response_bodies_spec.rb +45 -0
  37. data/spec/restify/features/response_encoding_spec.rb +82 -0
  38. data/spec/restify/features/response_errors_spec.rb +22 -1
  39. data/spec/restify/features/webmock_spec.rb +92 -1
  40. data/spec/restify/global_spec.rb +118 -1
  41. data/spec/restify/link_spec.rb +13 -3
  42. data/spec/restify/processors/json_spec.rb +36 -0
  43. data/spec/restify/processors/msgpack_spec.rb +36 -0
  44. data/spec/restify/promise_spec.rb +82 -0
  45. data/spec/restify/relation_spec.rb +22 -7
  46. data/spec/restify/request_spec.rb +26 -0
  47. data/spec/restify/resource_spec.rb +6 -0
  48. data/spec/restify/timeout_spec.rb +11 -0
  49. data/spec/restify_spec.rb +1 -1
  50. data/spec/spec_helper.rb +10 -7
  51. data/spec/support/stub_server.rb +4 -4
  52. metadata +27 -105
  53. data/vendor/bundle/ruby/3.4.0/bundler/gems/my-rubocop-3bcd2110ca87/LICENSE +0 -21
  54. data/vendor/bundle/ruby/3.4.0/bundler/gems/my-rubocop-3bcd2110ca87/README.md +0 -23
  55. data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/CHANGELOG.md +0 -255
  56. data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/MIT-LICENSE +0 -20
  57. data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/README.rdoc +0 -40
  58. data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/CHANGELOG.md +0 -301
  59. data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/LICENSE.txt +0 -202
  60. data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/README.md +0 -121
  61. data/vendor/bundle/ruby/3.4.0/gems/base64-0.2.0/LICENSE.txt +0 -22
  62. data/vendor/bundle/ruby/3.4.0/gems/base64-0.2.0/README.md +0 -48
  63. data/vendor/bundle/ruby/3.4.0/gems/benchmark-0.4.0/README.md +0 -138
  64. data/vendor/bundle/ruby/3.4.0/gems/bigdecimal-3.1.9/LICENSE +0 -56
  65. data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/CHANGELOG.md +0 -603
  66. data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/LICENSE.txt +0 -21
  67. data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/README.md +0 -407
  68. data/vendor/bundle/ruby/3.4.0/gems/connection_pool-2.5.3/LICENSE +0 -20
  69. data/vendor/bundle/ruby/3.4.0/gems/connection_pool-2.5.3/README.md +0 -167
  70. data/vendor/bundle/ruby/3.4.0/gems/drb-2.2.1/LICENSE.txt +0 -22
  71. data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/CHANGELOG.md +0 -375
  72. data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/LICENSE +0 -20
  73. data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/README.md +0 -118
  74. data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/CHANGELOG.md +0 -473
  75. data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/LICENSE +0 -24
  76. data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/LICENSE.SPECS +0 -22
  77. data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/README.md +0 -137
  78. data/vendor/bundle/ruby/3.4.0/gems/hitimes-3.1.0/LICENSE.txt +0 -16
  79. data/vendor/bundle/ruby/3.4.0/gems/hitimes-3.1.0/README.md +0 -187
  80. data/vendor/bundle/ruby/3.4.0/gems/i18n-1.14.7/MIT-LICENSE +0 -20
  81. data/vendor/bundle/ruby/3.4.0/gems/i18n-1.14.7/README.md +0 -127
  82. data/vendor/bundle/ruby/3.4.0/gems/little-plugger-1.1.4/README.rdoc +0 -53
  83. data/vendor/bundle/ruby/3.4.0/gems/logger-1.7.0/README.md +0 -104
  84. data/vendor/bundle/ruby/3.4.0/gems/logging-2.4.0/LICENSE +0 -22
  85. data/vendor/bundle/ruby/3.4.0/gems/logging-2.4.0/README.md +0 -140
  86. data/vendor/bundle/ruby/3.4.0/gems/minitest-5.25.5/README.rdoc +0 -842
  87. data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/ChangeLog +0 -368
  88. data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/LICENSE +0 -177
  89. data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/README.md +0 -302
  90. data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/CHANGELOG.md +0 -275
  91. data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/LICENSE.md +0 -20
  92. data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/README.md +0 -121
  93. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/CHANGELOG.md +0 -193
  94. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/LICENSE +0 -201
  95. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/README.md +0 -68
  96. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/CHANGELOG.md +0 -98
  97. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/LICENSE +0 -201
  98. data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/README.md +0 -62
  99. data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/CHANGELOG.md +0 -498
  100. data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/LICENSE.txt +0 -22
  101. data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/README.md +0 -222
  102. data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/CHANGELOG.md +0 -1146
  103. data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/MIT-LICENSE +0 -20
  104. data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/README.md +0 -355
  105. data/vendor/bundle/ruby/3.4.0/gems/rake-13.2.1/MIT-LICENSE +0 -21
  106. data/vendor/bundle/ruby/3.4.0/gems/rake-13.2.1/README.rdoc +0 -155
  107. data/vendor/bundle/ruby/3.4.0/gems/rake-release-1.3.0/LICENSE +0 -21
  108. data/vendor/bundle/ruby/3.4.0/gems/rake-release-1.3.0/README.md +0 -107
  109. data/vendor/bundle/ruby/3.4.0/gems/securerandom-0.4.1/README.md +0 -72
  110. data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/CHANGELOG.md +0 -410
  111. data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/LICENSE +0 -22
  112. data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/README.md +0 -588
  113. data/vendor/bundle/ruby/3.4.0/gems/tzinfo-2.0.6/LICENSE +0 -19
  114. data/vendor/bundle/ruby/3.4.0/gems/tzinfo-2.0.6/README.md +0 -406
  115. data/vendor/bundle/ruby/3.4.0/gems/uri-1.0.3/README.md +0 -55
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify::Adapter::LoopLock do
6
+ subject(:lock) { described_class.new { interrupts << Thread.current } }
7
+
8
+ let(:interrupts) { Queue.new }
9
+ let(:threads) { [] }
10
+
11
+ after { threads.each(&:kill).each(&:join) }
12
+
13
+ # Run the block in another thread, and wait until it has returned.
14
+ # The thread is kept alive until the example finished.
15
+ def other(&)
16
+ done = Concurrent::Event.new
17
+ threads << Thread.new do
18
+ yield
19
+ done.set
20
+ sleep
21
+ end
22
+ expect(done.wait(1)).to be true
23
+ end
24
+
25
+ def elapsed
26
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
+ yield
28
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
29
+ end
30
+
31
+ describe '#acquire' do
32
+ it 'acquires a free loop' do
33
+ expect(lock.acquire(1)).to be true
34
+ expect(lock).to be_owned
35
+ end
36
+
37
+ it 'does not acquire the loop when done' do
38
+ expect(lock.acquire(1) { true }).to be false
39
+ expect(lock).not_to be_owned
40
+ end
41
+
42
+ it 'times out while another thread holds the loop' do
43
+ other { lock.acquire(1) }
44
+
45
+ expect(elapsed { expect(lock.acquire(0.1)).to be false }).to be >= 0.1
46
+ expect(lock).not_to be_owned
47
+ end
48
+
49
+ it 'waits for another thread to release the loop' do
50
+ acquired = Concurrent::Event.new
51
+ released = false
52
+ threads << Thread.new do
53
+ lock.acquire(1)
54
+ acquired.set
55
+ sleep 0.1
56
+ released = true
57
+ lock.release
58
+ end
59
+ expect(acquired.wait(1)).to be true
60
+
61
+ expect(lock.acquire(1)).to be true
62
+ expect(released).to be true
63
+ end
64
+
65
+ it 'stops waiting when done after being notified' do
66
+ done = false
67
+ other { lock.acquire(1) }
68
+ threads << Thread.new do
69
+ sleep 0.1
70
+ done = true
71
+ lock.notify
72
+ end
73
+
74
+ expect(elapsed { expect(lock.acquire(1) { done }).to be false }).to be < 0.5
75
+ end
76
+
77
+ it 'treats a dead thread as not holding the loop' do
78
+ Thread.new { lock.acquire(1) }.join
79
+
80
+ expect(lock.acquire(0.1)).to be true
81
+ end
82
+ end
83
+
84
+ describe '#release' do
85
+ it 'lets a waiting thread acquire the loop' do
86
+ lock.acquire(1)
87
+ waiter = Thread.new { lock.acquire(1) }
88
+ sleep 0.05
89
+
90
+ lock.release
91
+
92
+ expect(waiter.join(1)&.value).to be true
93
+ expect(lock).not_to be_owned
94
+ end
95
+ end
96
+
97
+ describe 'with a background thread' do
98
+ let(:background) { Queue.new }
99
+
100
+ # A background thread running the loop until interrupted, like an
101
+ # event loop waiting for sockets. Blocks on `interrupts` on purpose,
102
+ # and is killed after each example.
103
+ def start_background
104
+ threads << Thread.new do
105
+ loop do
106
+ lock.acquire_background
107
+ background << :acquired
108
+ interrupts.pop
109
+ lock.release
110
+ end
111
+ end
112
+
113
+ expect(background.pop(timeout: 1)).to eq :acquired
114
+ end
115
+
116
+ it 'interrupts the background thread to release the loop' do
117
+ start_background
118
+
119
+ expect(lock.acquire(1)).to be true
120
+ expect(background).to be_empty
121
+ end
122
+
123
+ it 'blocks the background thread while a thread holds the loop' do
124
+ start_background
125
+ lock.acquire(1)
126
+
127
+ sleep 0.1
128
+ expect(background).to be_empty
129
+
130
+ lock.release
131
+ expect(background.pop(timeout: 1)).to eq :acquired
132
+ end
133
+ end
134
+
135
+ describe '#reset!' do
136
+ it 'forgets threads holding the loop' do
137
+ other { lock.acquire(1) }
138
+
139
+ lock.reset!
140
+
141
+ expect(lock.acquire(0.1)).to be true
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify::Adapter::Telemetry do
6
+ subject(:call) { adapter.call(request).value! }
7
+
8
+ let(:adapter) do
9
+ Class.new(Restify::Adapter::Base) do
10
+ def call_native(request, writer)
11
+ writer.fulfill(Restify::Response.new(request, request.uri, 200, {}, ''))
12
+ end
13
+ end.new
14
+ end
15
+
16
+ let(:request) { Restify::Request.new(uri:) }
17
+ let(:span) { OTLE_EXPORTER.finished_spans.last }
18
+
19
+ before { call }
20
+
21
+ context 'with an explicit port' do
22
+ let(:uri) { Addressable::URI.parse('https://example.org:8443/resources') }
23
+
24
+ it 'records the given port' do
25
+ expect(span.name).to eq 'GET https://example.org:8443'
26
+ expect(span.attributes).to eq({
27
+ 'http.request.method' => 'GET',
28
+ 'http.response.status_code' => 200,
29
+ 'server.address' => 'example.org',
30
+ 'server.port' => 8443,
31
+ 'url.full' => 'https://example.org:8443/resources',
32
+ 'url.scheme' => 'https',
33
+ })
34
+ end
35
+ end
36
+
37
+ context 'without a port' do
38
+ let(:uri) { Addressable::URI.parse('https://example.org/resources') }
39
+
40
+ it 'records the default port of the scheme' do
41
+ expect(span.name).to eq 'GET https://example.org:443'
42
+ expect(span.attributes).to include(
43
+ 'server.port' => 443,
44
+ 'url.full' => 'https://example.org/resources',
45
+ )
46
+ end
47
+ end
48
+
49
+ context 'with a URI given as a string' do
50
+ let(:uri) { 'http://example.org/resources' }
51
+
52
+ it 'parses it' do
53
+ expect(span.name).to eq 'GET http://example.org:80'
54
+ expect(span.attributes).to include(
55
+ 'server.address' => 'example.org',
56
+ 'server.port' => 80,
57
+ 'url.full' => 'http://example.org/resources',
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify::Adapter::Typhoeus do
6
+ let(:request) { Restify::Request.new(uri: 'http://localhost:9292/base') }
7
+
8
+ before do
9
+ stub_request(:get, 'http://stubserver/base').to_return(status: 200, body: '{}')
10
+ end
11
+
12
+ describe 'sync' do
13
+ let(:adapter) { described_class.new(sync: true) }
14
+
15
+ it 'runs requests from many threads' do
16
+ threads = Array.new(4) do
17
+ Thread.new { Array.new(10) { adapter.call(request).value!.code } }
18
+ end
19
+
20
+ # Do not hang the suite on a deadlock.
21
+ expect(threads.map {|thread| thread.join(5)&.value }).to all eq Array.new(10, 200)
22
+ ensure
23
+ threads&.each(&:kill)
24
+ end
25
+ end
26
+ end
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
- require 'active_support'
5
- require 'active_support/cache'
6
4
 
7
5
  describe Restify::Cache do
8
6
  subject(:cache) { described_class.new(store) }
9
7
 
10
- let(:store) { instance_double(ActiveSupport::Cache::Store) }
8
+ let(:store) { Object.new }
11
9
 
12
10
  describe '#call' do
13
11
  let(:request) { instance_double(Restify::Request) }
@@ -59,7 +59,7 @@ describe Restify::Context do
59
59
  let(:load) { YAML.load(dump) }
60
60
  end
61
61
 
62
- include_examples 'serialization'
62
+ it_behaves_like 'serialization'
63
63
  end
64
64
 
65
65
  context 'Marshall' do
@@ -68,7 +68,62 @@ describe Restify::Context do
68
68
  let(:dump) { Marshal.dump(context) }
69
69
  let(:load) { Marshal.load(dump) } # rubocop:disable Security/MarshalLoad
70
70
 
71
- include_examples 'serialization'
71
+ it_behaves_like 'serialization'
72
+ end
73
+ end
74
+
75
+ describe '#request' do
76
+ subject(:request) { context.request(:get, '/').value! }
77
+
78
+ let(:adapter) do
79
+ response = Restify::Response.new(nil, Addressable::URI.parse(uri), 200, {}, 'body')
80
+
81
+ instance_double(Restify::Adapter::Base).tap do |adapter|
82
+ allow(adapter).to receive(:call).and_return(Restify::Promise.fulfilled(response))
83
+ end
84
+ end
85
+
86
+ let(:cache) do
87
+ Object.new.tap do |cache|
88
+ def cache.call(request)
89
+ yield(request)
90
+ end
91
+ end
92
+ end
93
+
94
+ let(:kwargs) { {adapter:} }
95
+
96
+ it 'calls the adapter without a cache' do
97
+ expect(adapter).to receive(:call).with(Restify::Request)
98
+
99
+ request
100
+ end
101
+
102
+ context 'with a cache' do
103
+ let(:kwargs) { {adapter:, cache:} }
104
+
105
+ it 'lets the cache call the adapter' do
106
+ expect(cache).to receive(:call).with(Restify::Request).and_call_original
107
+ expect(adapter).to receive(:call).with(Restify::Request)
108
+
109
+ request
110
+ end
111
+ end
112
+
113
+ context 'with a global cache' do
114
+ around do |example|
115
+ Restify.cache = cache
116
+ example.run
117
+ ensure
118
+ Restify.cache = nil
119
+ end
120
+
121
+ it 'lets the cache call the adapter' do
122
+ expect(cache).to receive(:call).with(Restify::Request).and_call_original
123
+ expect(adapter).to receive(:call).with(Restify::Request)
124
+
125
+ request
126
+ end
72
127
  end
73
128
  end
74
129
  end
@@ -38,7 +38,7 @@ describe Restify do
38
38
  # Latest span has to be from the Ethon instrumentation and must
39
39
  # have the Restify span as a parent.
40
40
  spans[0].tap do |span|
41
- expect(span.instrumentation_scope.name).to match(/Ethon/)
41
+ expect(span.instrumentation_scope.name).to include('Ethon')
42
42
  expect(span.parent_span_id).to eq spans[1].span_id
43
43
  end
44
44
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify do
6
+ # Restify follows relations from URLs in server responses, and libcurl
7
+ # supports far more than HTTP. A server must therefore never be able
8
+ # to make it access e.g. a `file://` or `gopher://` URL.
9
+ describe 'Protocol restrictions' do
10
+ subject(:request) { Restify.new(uri).get.value! }
11
+
12
+ context 'with a file URI' do
13
+ let(:uri) { "file://#{File.expand_path(__FILE__)}" }
14
+
15
+ it 'refuses the request' do
16
+ expect { request }.to raise_error Restify::NetworkError, /protocol/i
17
+ end
18
+ end
19
+
20
+ context 'with a gopher URI' do
21
+ let(:uri) { 'gopher://localhost:70/1' }
22
+
23
+ it 'refuses the request' do
24
+ expect { request }.to raise_error Restify::NetworkError, /protocol/i
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify do
6
+ subject(:resource) { Restify.new('http://localhost:9292/old/resource').get.value! }
7
+
8
+ before do
9
+ stub_request(:get, 'http://stubserver/old/resource')
10
+ .to_return do
11
+ <<~HTTP
12
+ HTTP/1.1 302 Found
13
+ Location: http://localhost:9292/new/resource
14
+
15
+ HTTP
16
+ end
17
+
18
+ stub_request(:get, 'http://stubserver/new/resource')
19
+ .to_return do
20
+ <<~HTTP
21
+ HTTP/1.1 200 OK
22
+ Content-Type: application/json
23
+
24
+ {"name":"John Smith","sibling_url":"./sibling"}
25
+ HTTP
26
+ end
27
+ end
28
+
29
+ describe 'following redirects' do
30
+ it 'returns the redirected resource' do
31
+ expect(resource['name']).to eq 'John Smith'
32
+ expect(resource.response.code).to eq 200
33
+ end
34
+
35
+ it 'exposes the effective URI on the response' do
36
+ expect(resource.response.uri.to_s)
37
+ .to eq 'http://localhost:9292/new/resource'
38
+ end
39
+
40
+ it 'resolves relative relations against the effective URI' do
41
+ expect(resource.rel(:sibling).expand({}).to_s)
42
+ .to eq 'http://localhost:9292/new/sibling'
43
+ end
44
+ end
45
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Restify, adapter: 'Restify::Adapter::Typhoeus' do
5
+ describe Restify do
6
6
  before do
7
7
  stub_request(:get, 'http://stubserver/base').to_timeout
8
8
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify do
6
+ subject(:resource) { Restify.new('http://localhost:9292/base').get.value! }
7
+
8
+ before do
9
+ stub_request(:get, 'http://stubserver/base')
10
+ .to_return(
11
+ status: 200,
12
+ headers: {
13
+ 'Content-Type' => 'application/json',
14
+ 'Link' => '<http://localhost:9292/other>; rel="other"',
15
+ },
16
+ body:,
17
+ )
18
+ end
19
+
20
+ describe 'Response bodies' do
21
+ # #31: A content type is set, but the body is empty.
22
+ context 'when empty' do
23
+ let(:body) { '' }
24
+
25
+ it 'returns a resource without data' do
26
+ expect(resource.data).to be_nil
27
+ expect(resource).to have_relation :other
28
+ end
29
+ end
30
+
31
+ context 'when invalid' do
32
+ let(:body) { '{"name": ' }
33
+
34
+ it 'returns a resource with relations from headers' do
35
+ expect(resource).to have_relation :other
36
+ end
37
+
38
+ it 'raises on accessing data' do
39
+ expect { resource.data }.to raise_error(Restify::ParseError, /Could not parse response body/) do |error|
40
+ expect(error.cause).to be_a JSON::ParserError
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify do
6
+ describe 'Response body encoding' do
7
+ subject(:response) { Restify.new('http://localhost:9292/base').get.value!.response }
8
+
9
+ let(:text) { 'Grüße, Ærø – 日本' }
10
+
11
+ before do
12
+ stub_request(:get, 'http://stubserver/base').to_return(
13
+ status: 200,
14
+ headers: {'Content-Type' => content_type},
15
+ body: body,
16
+ )
17
+ end
18
+
19
+ context 'with an UTF-8 charset' do
20
+ let(:content_type) { 'text/plain; charset=UTF-8' }
21
+ let(:body) { text.encode(Encoding::UTF_8) }
22
+
23
+ it 'returns the body in the given encoding' do
24
+ expect(response.body.encoding).to eq Encoding::UTF_8
25
+ expect(response.body).to eq text
26
+ end
27
+ end
28
+
29
+ context 'with an ISO-8859-1 charset' do
30
+ let(:content_type) { 'text/plain; charset=ISO-8859-1' }
31
+ let(:text) { 'Grüße, Ærø' }
32
+ let(:body) { text.encode(Encoding::ISO_8859_1) }
33
+
34
+ it 'returns the body in the given encoding' do
35
+ expect(response.body.encoding).to eq Encoding::ISO_8859_1
36
+ expect(response.body).to eq text.encode(Encoding::ISO_8859_1)
37
+ expect(response.body.encode(Encoding::UTF_8)).to eq text
38
+ end
39
+ end
40
+
41
+ context 'with a quoted charset' do
42
+ let(:content_type) { 'text/plain; charset="utf-8"' }
43
+ let(:body) { text.encode(Encoding::UTF_8) }
44
+
45
+ it 'returns the body in the given encoding' do
46
+ expect(response.body.encoding).to eq Encoding::UTF_8
47
+ expect(response.body).to eq text
48
+ end
49
+ end
50
+
51
+ context 'with an unknown charset' do
52
+ let(:content_type) { 'text/plain; charset=nonsense' }
53
+ let(:body) { text.encode(Encoding::UTF_8) }
54
+
55
+ it 'returns the body as binary' do
56
+ expect(response.body.encoding).to eq Encoding::BINARY
57
+ expect(response.body.b).to eq text.b
58
+ end
59
+ end
60
+
61
+ context 'without a charset' do
62
+ let(:content_type) { 'text/plain' }
63
+ let(:body) { text.encode(Encoding::UTF_8) }
64
+
65
+ it 'returns the body as binary' do
66
+ expect(response.body.encoding).to eq Encoding::BINARY
67
+ expect(response.body.b).to eq text.b
68
+ end
69
+ end
70
+
71
+ context 'with a JSON body' do
72
+ let(:content_type) { 'application/json; charset=ISO-8859-1' }
73
+ let(:text) { 'Grüße, Ærø' }
74
+ let(:body) { %("#{text}").encode(Encoding::ISO_8859_1) }
75
+
76
+ it 'decodes the body using the given encoding' do
77
+ expect(response.body.encoding).to eq Encoding::ISO_8859_1
78
+ expect(response.decoded_body).to eq text
79
+ end
80
+ end
81
+ end
82
+ end
@@ -5,11 +5,12 @@ require 'spec_helper'
5
5
  describe Restify do
6
6
  before do
7
7
  stub_request(:get, 'http://stubserver/base')
8
- .to_return(status: http_status, headers:)
8
+ .to_return(status: http_status, headers:, body:)
9
9
  end
10
10
 
11
11
  let(:http_status) { '200 OK' }
12
12
  let(:headers) { {} }
13
+ let(:body) { '' }
13
14
 
14
15
  describe 'Error handling' do
15
16
  subject(:request) { Restify.new('http://localhost:9292/base').get.value! }
@@ -20,6 +21,26 @@ describe Restify do
20
21
  it 'throws a BadRequest exception' do
21
22
  expect { request }.to raise_error Restify::BadRequest
22
23
  end
24
+
25
+ # #31: Rails sets a JSON content type even for empty bodies.
26
+ context 'with a JSON content type and an empty body' do
27
+ let(:headers) { {'Content-Type' => 'application/json'} }
28
+
29
+ it 'throws a BadRequest exception' do
30
+ expect { request }.to raise_error Restify::BadRequest
31
+ end
32
+ end
33
+
34
+ context 'with an invalid JSON body' do
35
+ let(:headers) { {'Content-Type' => 'application/json'} }
36
+ let(:body) { '{"errors": ' }
37
+
38
+ it 'throws a BadRequest exception with the raw body as errors' do
39
+ expect { request }.to raise_error(Restify::BadRequest) do |error|
40
+ expect(error.errors).to eq '{"errors": '
41
+ end
42
+ end
43
+ end
23
44
  end
24
45
 
25
46
  context 'for 401 status codes' do
@@ -4,11 +4,13 @@ require 'spec_helper'
4
4
 
5
5
  describe Restify do
6
6
  before do
7
- WebMock.enable!
7
+ WebMock.enable!(except: %i[typhoeus])
8
8
  end
9
9
 
10
10
  after do
11
11
  WebMock.disable!(except: %i[net_http])
12
+ WebMock.disable_net_connect!
13
+ WebMock::CallbackRegistry.reset
12
14
  end
13
15
 
14
16
  describe 'WebMock' do
@@ -23,5 +25,94 @@ describe Restify do
23
25
  it 'raises error for not stubbed requests' do
24
26
  expect { resource }.to raise_error WebMock::NetConnectNotAllowedError
25
27
  end
28
+
29
+ it 'returns stubbed responses' do
30
+ stub_request(:get, 'http://www.example.com/base')
31
+ .to_return(
32
+ headers: {'Content-Type' => 'application/json', 'X-Custom' => 'yes'},
33
+ body: '{"name": "restify", "self_url": "http://www.example.com/self"}',
34
+ )
35
+
36
+ expect(resource['name']).to eq 'restify'
37
+ expect(resource).to have_relation :self
38
+ expect(resource.response.headers).to include('X_CUSTOM' => 'yes')
39
+ end
40
+
41
+ it 'records requests' do
42
+ stub_request(:post, 'http://www.example.com/base')
43
+
44
+ Restify.new('http://www.example.com/base').post({name: 'restify'}).value!
45
+
46
+ expect(
47
+ a_request(:post, 'http://www.example.com/base')
48
+ .with(body: {name: 'restify'}.to_json, headers: {'Content-Type' => 'application/json'}),
49
+ ).to have_been_made.once
50
+ end
51
+
52
+ it 'raises network errors on stubbed timeouts' do
53
+ stub_request(:get, 'http://www.example.com/base').to_timeout
54
+
55
+ expect { resource }.to raise_error Restify::NetworkError, /timeout/i
56
+ end
57
+
58
+ it 'raises stubbed errors' do
59
+ stub_request(:get, 'http://www.example.com/base').to_raise(Errno::ECONNREFUSED)
60
+
61
+ expect { resource }.to raise_error Errno::ECONNREFUSED
62
+ end
63
+
64
+ it 'creates a span for stubbed requests' do
65
+ stub_request(:get, 'http://www.example.com/base')
66
+
67
+ resource
68
+
69
+ expect(OTLE_EXPORTER.finished_spans.map {|span| span.instrumentation_scope.name }).to eq ['restify']
70
+ end
71
+
72
+ context 'when disabled' do
73
+ it 'does not intercept requests' do
74
+ stub_request(:get, 'http://localhost:9292/echo').to_return(body: 'stubbed')
75
+ WebMock.disable!(except: %i[net_http])
76
+
77
+ expect(Restify.new('http://localhost:9292/echo').get.value!.response.body).not_to eq 'stubbed'
78
+ end
79
+ end
80
+
81
+ # Like WebMock does for other libcurl-based libraries: Redirects are
82
+ # returned as they are.
83
+ it 'returns stubbed redirects as they are' do
84
+ stub_request(:get, 'http://www.example.com/base')
85
+ .to_return(status: 302, headers: {'Location' => '/other'})
86
+
87
+ expect(resource.response.code).to eq 302
88
+ expect(resource.response.headers).to include('LOCATION' => '/other')
89
+ expect(a_request(:get, 'http://www.example.com/other')).not_to have_been_made
90
+ end
91
+
92
+ context 'with net connect allowed' do
93
+ before { WebMock.allow_net_connect! }
94
+
95
+ it 'passes requests on to the adapter' do
96
+ expect(Restify.new('http://localhost:9292/echo').get.value!.data).to include('REQUEST_METHOD' => 'GET')
97
+ end
98
+
99
+ it 'invokes callbacks for real requests once' do
100
+ responses = Queue.new
101
+ WebMock.after_request(real_requests_only: true) do |signature, response|
102
+ responses << [signature.uri.to_s, response.status[0]]
103
+ end
104
+
105
+ Restify.new('http://localhost:9292/echo').get.value!
106
+
107
+ expect(responses.pop(timeout: 1)).to eq ['http://localhost:9292/echo', 200]
108
+ expect(responses).to be_empty
109
+ end
110
+
111
+ it 'records real requests once' do
112
+ Restify.new('http://localhost:9292/echo').get.value!
113
+
114
+ expect(a_request(:get, 'http://localhost:9292/echo')).to have_been_made.once
115
+ end
116
+ end
26
117
  end
27
118
  end