gibbon 3.5.0 → 4.0.1

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.
@@ -1,11 +1,42 @@
1
1
  module Gibbon
2
+ # Builds and issues requests against MailChimp's Marketing API.
3
+ #
4
+ # `Request` defines no methods for MailChimp's resources. An unknown method
5
+ # appends a segment to an internal path and returns `self`, so
6
+ # `gibbon.lists(list_id).members` builds `lists/<list_id>/members`. A CRUD
7
+ # verb ({#create}, {#retrieve}, {#update}, {#upsert}, {#delete}) then issues
8
+ # the request and resets the path. Because of that reset, an instance is safe
9
+ # to reuse only after a verb has run.
10
+ #
11
+ # @example Fetching lists
12
+ # gibbon = Gibbon::Request.new(api_key: "your_api_key-us1")
13
+ # gibbon.lists.retrieve.body["lists"]
14
+ #
15
+ # @see https://mailchimp.com/developer/marketing/api/ MailChimp's resource documentation
2
16
  class Request
17
+ # Per-instance configuration. Each falls back to the matching class level
18
+ # setting, then to an environment variable or built-in default; see
19
+ # {#initialize} for the resolution order.
3
20
  attr_accessor :api_key, :api_endpoint, :timeout, :open_timeout, :proxy, :faraday_adapter, :symbolize_keys, :debug, :logger
4
21
 
22
+ # Default read timeout, in seconds.
5
23
  DEFAULT_TIMEOUT = 60
24
+ # Default connection open timeout, in seconds.
6
25
  DEFAULT_OPEN_TIMEOUT = 60
7
26
 
8
- def initialize(api_key: nil, api_endpoint: nil, timeout: nil, open_timeout: nil, proxy: nil, faraday_adapter: nil, symbolize_keys: false, debug: false, logger: nil)
27
+ # @param api_key [String, nil] API key or OAuth access token. Falls back to
28
+ # `Gibbon::Request.api_key`, then `ENV["MAILCHIMP_API_KEY"]`.
29
+ # @param api_endpoint [String, nil] Base URL, e.g. `"https://us1.api.mailchimp.com"`.
30
+ # Required when the key carries no data center suffix, as OAuth access
31
+ # tokens do not; otherwise it is derived from the key.
32
+ # @param timeout [Integer, nil] Read timeout in seconds. Defaults to {DEFAULT_TIMEOUT}.
33
+ # @param open_timeout [Integer, nil] Open timeout in seconds. Defaults to {DEFAULT_OPEN_TIMEOUT}.
34
+ # @param proxy [String, nil] Proxy URL. Falls back to `ENV["MAILCHIMP_PROXY"]`.
35
+ # @param faraday_adapter [Symbol, nil] Faraday adapter. Defaults to `Faraday.default_adapter`.
36
+ # @param symbolize_keys [Boolean] Whether parsed response bodies use symbol keys.
37
+ # @param debug [Boolean] Whether to log requests and responses.
38
+ # @param logger [Logger, nil] Logger used when `debug` is true. Defaults to `Logger.new(STDOUT)`.
39
+ def initialize(api_key: nil, api_endpoint: nil, timeout: nil, open_timeout: nil, proxy: nil, faraday_adapter: nil, symbolize_keys: nil, debug: nil, logger: nil)
9
40
  @path_parts = []
10
41
  @api_key = api_key || self.class.api_key || ENV['MAILCHIMP_API_KEY']
11
42
  @api_key = @api_key.strip if @api_key
@@ -14,11 +45,15 @@ module Gibbon
14
45
  @open_timeout = open_timeout || self.class.open_timeout || DEFAULT_OPEN_TIMEOUT
15
46
  @proxy = proxy || self.class.proxy || ENV['MAILCHIMP_PROXY']
16
47
  @faraday_adapter = faraday_adapter || self.class.faraday_adapter || Faraday.default_adapter
17
- @symbolize_keys = symbolize_keys || self.class.symbolize_keys || false
18
- @debug = debug || self.class.debug || false
48
+ @symbolize_keys = symbolize_keys.nil? ? (self.class.symbolize_keys || false) : symbolize_keys
49
+ @debug = debug.nil? ? (self.class.debug || false) : debug
19
50
  @logger = logger || self.class.logger || ::Logger.new(STDOUT)
20
51
  end
21
52
 
53
+ # Appends the method name to the request path, along with any arguments as
54
+ # further segments, and returns `self` so calls can be chained.
55
+ #
56
+ # @return [Request] self
22
57
  def method_missing(method, *args)
23
58
  # To support underscores, we replace them with hyphens when calling the API
24
59
  @path_parts << method.to_s.gsub("_", "-").downcase
@@ -27,10 +62,19 @@ module Gibbon
27
62
  self
28
63
  end
29
64
 
65
+ # Every method name is a potential path segment, so this always returns true.
66
+ #
67
+ # @return [true]
30
68
  def respond_to_missing?(method_name, include_private = false)
31
69
  true
32
70
  end
33
71
 
72
+ # Appends `send` as a path segment when called with no arguments, so that
73
+ # `gibbon.campaigns(id).actions.send.create` reaches
74
+ # `campaigns/<id>/actions/send`. Called with arguments, this behaves like
75
+ # `Object#send` and invokes the named method.
76
+ #
77
+ # @return [Request, Object] self, or the return value of the invoked method
34
78
  def send(*args)
35
79
  if args.length == 0
36
80
  method_missing(:send, args)
@@ -39,38 +83,75 @@ module Gibbon
39
83
  end
40
84
  end
41
85
 
86
+ # @return [String] the path accumulated so far, e.g. `"lists/abc123/members"`
42
87
  def path
43
88
  @path_parts.join('/')
44
89
  end
45
90
 
91
+ # Issues a `POST` to the accumulated path, then resets it.
92
+ #
93
+ # @param params [Hash, nil] query string parameters
94
+ # @param headers [Hash, nil] additional request headers
95
+ # @param body [Hash, nil] request body, serialized to JSON
96
+ # @return [Response, nil] the response, or nil when MailChimp returns an empty body
97
+ # @raise [GibbonError] if no usable API key is configured
98
+ # @raise [MailChimpError] if the request fails or the response cannot be parsed
46
99
  def create(params: nil, headers: nil, body: nil)
47
100
  APIRequest.new(builder: self).post(params: params, headers: headers, body: body)
48
101
  ensure
49
102
  reset
50
103
  end
51
104
 
105
+ # Issues a `PATCH` to the accumulated path, then resets it.
106
+ #
107
+ # @param (see #create)
108
+ # @return (see #create)
109
+ # @raise (see #create)
52
110
  def update(params: nil, headers: nil, body: nil)
53
111
  APIRequest.new(builder: self).patch(params: params, headers: headers, body: body)
54
112
  ensure
55
113
  reset
56
114
  end
57
115
 
116
+ # Issues a `PUT` to the accumulated path, then resets it. Updates the record
117
+ # if it exists and inserts it otherwise, where MailChimp supports it.
118
+ #
119
+ # @param (see #create)
120
+ # @return (see #create)
121
+ # @raise (see #create)
58
122
  def upsert(params: nil, headers: nil, body: nil)
59
123
  APIRequest.new(builder: self).put(params: params, headers: headers, body: body)
60
124
  ensure
61
125
  reset
62
126
  end
63
127
 
128
+ # Alias for {#retrieve}.
129
+ #
130
+ # @param (see #retrieve)
131
+ # @return (see #retrieve)
132
+ # @raise (see #retrieve)
64
133
  def get(params: nil, headers: nil)
65
134
  retrieve(params: params, headers: headers)
66
135
  end
67
136
 
137
+ # Issues a `GET` to the accumulated path, then resets it.
138
+ #
139
+ # @param params [Hash, nil] query string parameters
140
+ # @param headers [Hash, nil] additional request headers
141
+ # @return [Response, nil] the response, or nil when MailChimp returns an empty body
142
+ # @raise [GibbonError] if no usable API key is configured
143
+ # @raise [MailChimpError] if the request fails or the response cannot be parsed
68
144
  def retrieve(params: nil, headers: nil)
69
145
  APIRequest.new(builder: self).get(params: params, headers: headers)
70
146
  ensure
71
147
  reset
72
148
  end
73
149
 
150
+ # Issues a `DELETE` to the accumulated path, then resets it.
151
+ #
152
+ # @param (see #retrieve)
153
+ # @return (see #retrieve)
154
+ # @raise (see #retrieve)
74
155
  def delete(params: nil, headers: nil)
75
156
  APIRequest.new(builder: self).delete(params: params, headers: headers)
76
157
  ensure
@@ -84,12 +165,22 @@ module Gibbon
84
165
  end
85
166
 
86
167
  class << self
168
+ # Class level defaults applied to every new instance. Setting these is a
169
+ # convenient way to configure Gibbon once, e.g. from a Rails initializer.
87
170
  attr_accessor :api_key, :timeout, :open_timeout, :api_endpoint, :proxy, :faraday_adapter, :symbolize_keys, :debug, :logger
88
171
 
172
+ # Starts a call chain on a new instance built from the class level
173
+ # configuration, so `Gibbon::Request.lists.retrieve` works once
174
+ # {api_key} has been set.
175
+ #
176
+ # @return [Request] a new instance carrying the first path segment
89
177
  def method_missing(sym, *args, &block)
90
178
  new(api_key: self.api_key, api_endpoint: self.api_endpoint, timeout: self.timeout, open_timeout: self.open_timeout, faraday_adapter: self.faraday_adapter, symbolize_keys: self.symbolize_keys, debug: self.debug, proxy: self.proxy, logger: self.logger).send(sym, *args, &block)
91
179
  end
92
180
 
181
+ # Every method name is a potential path segment, so this always returns true.
182
+ #
183
+ # @return [true]
93
184
  def respond_to_missing?(method_name, include_private = false)
94
185
  true
95
186
  end
@@ -1,7 +1,20 @@
1
1
  module Gibbon
2
+ # A parsed response from MailChimp, returned by every CRUD verb on {Request}.
3
+ #
4
+ # @example
5
+ # response = gibbon.lists.retrieve
6
+ # response.body["lists"]
7
+ # response.headers["content-type"]
2
8
  class Response
3
- attr_accessor :body, :headers
4
-
9
+ # @return [Hash, Array] the parsed JSON response body. Keys are symbols when
10
+ # the request was built with `symbolize_keys: true`, strings otherwise.
11
+ attr_accessor :body
12
+
13
+ # @return [Hash] the response headers, keyed by lower case string.
14
+ attr_accessor :headers
15
+
16
+ # @param body [Hash, Array] the parsed response body
17
+ # @param headers [Hash] the response headers
5
18
  def initialize(body: {}, headers: {})
6
19
  @body = body
7
20
  @headers = headers
@@ -1,3 +1,4 @@
1
1
  module Gibbon
2
- VERSION = "3.5.0"
2
+ # The released version of the gem.
3
+ VERSION = "4.0.1"
3
4
  end
data/lib/gibbon.rb CHANGED
@@ -10,5 +10,6 @@ require 'gibbon/request'
10
10
  require 'gibbon/api_request'
11
11
  require 'gibbon/response'
12
12
 
13
+ # A Ruby wrapper for MailChimp's Marketing API. See {Gibbon::Request}.
13
14
  module Gibbon
14
15
  end
@@ -1,5 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'webmock/rspec'
3
+ require 'stringio'
3
4
 
4
5
  describe Gibbon::APIRequest do
5
6
  let(:api_key) { "1234-us1" }
@@ -66,4 +67,176 @@ describe Gibbon::APIRequest do
66
67
 
67
68
  include_examples 'client error handling'
68
69
  end
70
+
71
+ context 'Faraday::ServerError' do
72
+ let(:error_class) { Faraday::ServerError }
73
+
74
+ include_examples 'client error handling'
75
+ end
76
+
77
+ context "error responses over HTTP" do
78
+ it "raises a MailChimpError with details parsed from a 4xx JSON response" do
79
+ error_body = '{"title": "Invalid Resource", "detail": "The resource submitted could not be validated.", "status": 422}'
80
+ stub_request(:get, "#{@api_root}/lists")
81
+ .with(basic_auth: @basic_auth_credentials)
82
+ .to_return(status: 422, body: error_body, headers: { 'Content-Type' => 'application/json' })
83
+
84
+ begin
85
+ @gibbon.lists.retrieve
86
+ fail "expected a Gibbon::MailChimpError"
87
+ rescue Gibbon::MailChimpError => e
88
+ expect(e.status_code).to eq 422
89
+ expect(e.title).to eq "Invalid Resource"
90
+ expect(e.detail).to eq "The resource submitted could not be validated."
91
+ expect(e.raw_body).to eq error_body
92
+ expect(e.body).to eq MultiJson.load(error_body)
93
+ end
94
+ end
95
+
96
+ it "raises a MailChimpError on a 5xx response" do
97
+ stub_request(:get, "#{@api_root}/lists")
98
+ .with(basic_auth: @basic_auth_credentials)
99
+ .to_return(status: 503, body: '{"title": "Service Unavailable"}')
100
+
101
+ begin
102
+ @gibbon.lists.retrieve
103
+ fail "expected a Gibbon::MailChimpError"
104
+ rescue Gibbon::MailChimpError => e
105
+ expect(e.status_code).to eq 503
106
+ expect(e.title).to eq "Service Unavailable"
107
+ end
108
+ end
109
+ end
110
+
111
+ context "response parsing" do
112
+ it "returns a Gibbon::Response with the parsed body and headers" do
113
+ stub_request(:get, "#{@api_root}/lists")
114
+ .with(basic_auth: @basic_auth_credentials)
115
+ .to_return(status: 200, body: '{"lists": [{"id": "abc123"}]}', headers: { 'Content-Type' => 'application/json' })
116
+
117
+ response = @gibbon.lists.retrieve
118
+ expect(response).to be_a Gibbon::Response
119
+ expect(response.body).to eq({ "lists" => [{ "id" => "abc123" }] })
120
+ expect(response.headers["content-type"]).to eq "application/json"
121
+ end
122
+
123
+ it "symbolizes response keys when symbolize_keys is true" do
124
+ stub_request(:get, "#{@api_root}/lists")
125
+ .with(basic_auth: @basic_auth_credentials)
126
+ .to_return(status: 200, body: '{"lists": [{"id": "abc123"}]}')
127
+
128
+ response = Gibbon::Request.new(api_key: api_key, symbolize_keys: true).lists.retrieve
129
+ expect(response.body).to eq({ lists: [{ id: "abc123" }] })
130
+ end
131
+
132
+ it "returns nil for an empty response body" do
133
+ stub_request(:get, "#{@api_root}/lists")
134
+ .with(basic_auth: @basic_auth_credentials)
135
+ .to_return(status: 200, body: "")
136
+
137
+ expect(@gibbon.lists.retrieve).to be_nil
138
+ end
139
+
140
+ it "raises a MailChimpError with details intact for an unparseable success response" do
141
+ stub_request(:get, "#{@api_root}/lists")
142
+ .with(basic_auth: @basic_auth_credentials)
143
+ .to_return(status: 200, body: "not json")
144
+
145
+ begin
146
+ @gibbon.lists.retrieve
147
+ fail "expected a Gibbon::MailChimpError"
148
+ rescue Gibbon::MailChimpError => e
149
+ expect(e.title).to eq "UNPARSEABLE_RESPONSE"
150
+ expect(e.status_code).to eq 500
151
+ end
152
+ end
153
+ end
154
+
155
+ context "instance overrides of class defaults" do
156
+ before do
157
+ allow(Gibbon::Request).to receive(:symbolize_keys).and_return(true)
158
+ allow(Gibbon::Request).to receive(:debug).and_return(true)
159
+ end
160
+
161
+ let(:log_output) { StringIO.new }
162
+ let(:gibbon) do
163
+ Gibbon::Request.new(api_key: api_key, symbolize_keys: false, debug: false, logger: Logger.new(log_output))
164
+ end
165
+
166
+ it "returns string keys, including nested keys, when symbolization is disabled" do
167
+ stub_request(:get, "#{@api_root}/lists")
168
+ .to_return(body: '{"lists": [{"id": "abc123"}]}')
169
+
170
+ expect(gibbon.lists.retrieve.body).to eq({ "lists" => [{ "id" => "abc123" }] })
171
+ expect(Gibbon::Request.symbolize_keys).to be true
172
+ end
173
+
174
+ it "preserves error details with string keys when symbolization is disabled" do
175
+ error_body = '{"title": "Invalid Resource", "detail": "Invalid email address"}'
176
+ stub_request(:get, "#{@api_root}/lists").to_return(status: 422, body: error_body)
177
+
178
+ expect { gibbon.lists.retrieve }.to raise_error(Gibbon::MailChimpError) { |error|
179
+ expect(error.status_code).to eq 422
180
+ expect(error.title).to eq "Invalid Resource"
181
+ expect(error.detail).to eq "Invalid email address"
182
+ expect(error.body).to eq({ "title" => "Invalid Resource", "detail" => "Invalid email address" })
183
+ expect(error.raw_body).to eq error_body
184
+ }
185
+ end
186
+
187
+ it "does not log requests or responses when debug is disabled" do
188
+ stub_request(:get, "#{@api_root}/lists")
189
+ .to_return(body: '{"email_address": "private@example.com"}')
190
+
191
+ gibbon.lists.retrieve
192
+
193
+ expect(log_output.string).to be_empty
194
+ expect(Gibbon::Request.debug).to be true
195
+ end
196
+ end
197
+
198
+ context "configure_request" do
199
+ it "sets params, headers, body, and timeouts on the request" do
200
+ gibbon = Gibbon::Request.new(api_key: api_key, timeout: 45, open_timeout: 15)
201
+ api_request = Gibbon::APIRequest.new(builder: gibbon.lists)
202
+ request = Struct.new(:params, :headers, :body, :options).new({}, {}, nil, Faraday::RequestOptions.new)
203
+
204
+ api_request.send(:configure_request, request: request, params: { "count" => "10" }, headers: { "X-Custom" => "value" }, body: '{"a":1}')
205
+
206
+ expect(request.params).to eq({ "count" => "10" })
207
+ expect(request.headers).to eq({ "Content-Type" => "application/json", "X-Custom" => "value" })
208
+ expect(request.body).to eq '{"a":1}'
209
+ expect(request.options.timeout).to eq 45
210
+ expect(request.options.open_timeout).to eq 15
211
+ end
212
+ end
213
+
214
+ context "rest_client configuration" do
215
+ it "includes the logger middleware when debug is enabled" do
216
+ gibbon = Gibbon::Request.new(api_key: api_key, debug: true)
217
+ client = Gibbon::APIRequest.new(builder: gibbon.lists).send(:rest_client)
218
+ expect(client.builder.handlers).to include(Faraday::Response::Logger)
219
+ end
220
+
221
+ it "omits the logger middleware by default" do
222
+ client = Gibbon::APIRequest.new(builder: @gibbon.lists).send(:rest_client)
223
+ expect(client.builder.handlers).not_to include(Faraday::Response::Logger)
224
+ end
225
+
226
+ it "configures the proxy" do
227
+ gibbon = Gibbon::Request.new(api_key: api_key, proxy: "http://proxy.example.com:8080")
228
+ client = Gibbon::APIRequest.new(builder: gibbon.lists).send(:rest_client)
229
+ expect(client.proxy.host).to eq "proxy.example.com"
230
+ expect(client.proxy.port).to eq 8080
231
+ end
232
+ end
233
+
234
+ context "rest_client ssl configuration" do
235
+ it "requires TLS 1.2 as a minimum without capping the TLS version" do
236
+ api_request = Gibbon::APIRequest.new(builder: @gibbon.lists)
237
+ client = api_request.send(:rest_client)
238
+ expect(client.ssl.min_version).to eq :TLS1_2
239
+ expect(client.ssl.max_version).to be_nil
240
+ end
241
+ end
69
242
  end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gibbon::Helpers do
4
+ let(:helper) { Class.new { include Gibbon::Helpers }.new }
5
+
6
+ describe "#get_data_center_from_api_key" do
7
+ it "returns an empty string for a nil api key" do
8
+ expect(helper.get_data_center_from_api_key(nil)).to eq("")
9
+ end
10
+
11
+ it "returns an empty string for an api key without a data center" do
12
+ expect(helper.get_data_center_from_api_key("1234")).to eq("")
13
+ end
14
+
15
+ it "returns the data center with a trailing dot" do
16
+ expect(helper.get_data_center_from_api_key("1234-us1")).to eq("us1.")
17
+ end
18
+
19
+ it "uses the last segment of a key containing multiple hyphens" do
20
+ expect(helper.get_data_center_from_api_key("12-34-us10")).to eq("us10.")
21
+ end
22
+
23
+ it "strips non-alphanumeric characters to prevent domain injection" do
24
+ expect(helper.get_data_center_from_api_key("1234-attacker.net/path?")).to eq("attackernetpath.")
25
+ end
26
+ end
27
+ end
@@ -219,10 +219,26 @@ describe Gibbon do
219
219
  expect(Gibbon::Request.new.symbolize_keys).to eq(Gibbon::Request.symbolize_keys)
220
220
  end
221
221
 
222
+ it "allows instance symbolize_keys false to override the class setting" do
223
+ expect(Gibbon::Request.new(symbolize_keys: false).symbolize_keys).to be false
224
+ end
225
+
226
+ it "inherits the class symbolize_keys setting when explicitly passed nil" do
227
+ expect(Gibbon::Request.new(symbolize_keys: nil).symbolize_keys).to be true
228
+ end
229
+
222
230
  it "set debug on new instances" do
223
231
  expect(Gibbon::Request.new.debug).to eq(Gibbon::Request.debug)
224
232
  end
225
233
 
234
+ it "allows instance debug false to override the class setting" do
235
+ expect(Gibbon::Request.new(debug: false).debug).to be false
236
+ end
237
+
238
+ it "inherits the class debug setting when explicitly passed nil" do
239
+ expect(Gibbon::Request.new(debug: nil).debug).to be true
240
+ end
241
+
226
242
  it "set faraday_adapter on new instances" do
227
243
  expect(Gibbon::Request.new.faraday_adapter).to eq(Gibbon::Request.faraday_adapter)
228
244
  end
@@ -46,4 +46,22 @@ describe Gibbon::MailChimpError do
46
46
  it 'sets the status_code attribute' do
47
47
  expect(@gibbon.status_code).to eq(params[:status_code])
48
48
  end
49
+
50
+ context "without params" do
51
+ before do
52
+ @gibbon = Gibbon::MailChimpError.new(message)
53
+ end
54
+
55
+ it "leaves the attributes nil" do
56
+ expect(@gibbon.title).to be_nil
57
+ expect(@gibbon.detail).to be_nil
58
+ expect(@gibbon.body).to be_nil
59
+ expect(@gibbon.raw_body).to be_nil
60
+ expect(@gibbon.status_code).to be_nil
61
+ end
62
+
63
+ it "still includes the attributes in the message" do
64
+ expect(@gibbon.message).to eq "Foo @title=nil, @detail=nil, @body=nil, @raw_body=nil, @status_code=nil"
65
+ end
66
+ end
49
67
  end
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+ require 'webmock/rspec'
3
+
4
+ describe Gibbon::Request do
5
+ let(:api_key) { "1234-us1" }
6
+ let(:api_root) { "https://us1.api.mailchimp.com/3.0" }
7
+ let(:basic_auth_credentials) { ['apikey', api_key] }
8
+
9
+ before do
10
+ @gibbon = Gibbon::Request.new(api_key: api_key)
11
+ end
12
+
13
+ describe "path building" do
14
+ it "builds a path from chained calls" do
15
+ @gibbon.lists("list123").members
16
+ expect(@gibbon.path).to eq("lists/list123/members")
17
+ end
18
+
19
+ it "replaces underscores with hyphens" do
20
+ @gibbon.lists("list123").interest_categories
21
+ expect(@gibbon.path).to eq("lists/list123/interest-categories")
22
+ end
23
+
24
+ it "downcases path parts" do
25
+ @gibbon.LISTS
26
+ expect(@gibbon.path).to eq("lists")
27
+ end
28
+
29
+ it "responds to any method" do
30
+ expect(@gibbon.respond_to?(:anything_at_all)).to be true
31
+ end
32
+
33
+ it "appends 'send' to the path when called without arguments" do
34
+ @gibbon.campaigns("abc").actions.send
35
+ expect(@gibbon.path).to eq("campaigns/abc/actions/send")
36
+ end
37
+
38
+ it "dispatches send with arguments as a regular method call" do
39
+ expect(@gibbon.send(:api_key)).to eq(api_key)
40
+ end
41
+
42
+ it "resets the path after a request" do
43
+ stub_request(:get, "#{api_root}/lists").with(basic_auth: basic_auth_credentials).to_return(status: 200)
44
+ @gibbon.lists.retrieve
45
+ expect(@gibbon.path).to eq("")
46
+ end
47
+
48
+ it "resets the path when a request raises" do
49
+ stub_request(:get, "#{api_root}/lists").with(basic_auth: basic_auth_credentials).to_return(status: 500, body: "{}")
50
+ expect { @gibbon.lists.retrieve }.to raise_error(Gibbon::MailChimpError)
51
+ expect(@gibbon.path).to eq("")
52
+ end
53
+ end
54
+
55
+ describe "HTTP verb mapping" do
56
+ let(:body) { { name: "Foo" } }
57
+
58
+ it "maps create to POST" do
59
+ stub = stub_request(:post, "#{api_root}/lists")
60
+ .with(body: MultiJson.dump(body), basic_auth: basic_auth_credentials, headers: { 'Content-Type' => 'application/json' })
61
+ .to_return(status: 200)
62
+ @gibbon.lists.create(body: body)
63
+ expect(stub).to have_been_requested
64
+ end
65
+
66
+ it "maps update to PATCH" do
67
+ stub = stub_request(:patch, "#{api_root}/lists/list123")
68
+ .with(body: MultiJson.dump(body), basic_auth: basic_auth_credentials, headers: { 'Content-Type' => 'application/json' })
69
+ .to_return(status: 200)
70
+ @gibbon.lists("list123").update(body: body)
71
+ expect(stub).to have_been_requested
72
+ end
73
+
74
+ it "maps upsert to PUT" do
75
+ stub = stub_request(:put, "#{api_root}/lists/list123")
76
+ .with(body: MultiJson.dump(body), basic_auth: basic_auth_credentials, headers: { 'Content-Type' => 'application/json' })
77
+ .to_return(status: 200)
78
+ @gibbon.lists("list123").upsert(body: body)
79
+ expect(stub).to have_been_requested
80
+ end
81
+
82
+ it "maps retrieve to GET" do
83
+ stub = stub_request(:get, "#{api_root}/lists").with(basic_auth: basic_auth_credentials).to_return(status: 200)
84
+ @gibbon.lists.retrieve
85
+ expect(stub).to have_been_requested
86
+ end
87
+
88
+ it "maps get to GET" do
89
+ stub = stub_request(:get, "#{api_root}/lists").with(basic_auth: basic_auth_credentials).to_return(status: 200)
90
+ @gibbon.lists.get
91
+ expect(stub).to have_been_requested
92
+ end
93
+
94
+ it "maps delete to DELETE" do
95
+ stub = stub_request(:delete, "#{api_root}/lists/list123").with(basic_auth: basic_auth_credentials).to_return(status: 204)
96
+ @gibbon.lists("list123").delete
97
+ expect(stub).to have_been_requested
98
+ end
99
+ end
100
+
101
+ describe "request options" do
102
+ it "passes query params" do
103
+ stub = stub_request(:get, "#{api_root}/lists")
104
+ .with(query: { "count" => "10", "offset" => "20" }, basic_auth: basic_auth_credentials)
105
+ .to_return(status: 200)
106
+ @gibbon.lists.retrieve(params: { "count" => "10", "offset" => "20" })
107
+ expect(stub).to have_been_requested
108
+ end
109
+
110
+ it "passes custom headers" do
111
+ stub = stub_request(:get, "#{api_root}/lists")
112
+ .with(headers: { 'X-Custom' => 'custom-value' }, basic_auth: basic_auth_credentials)
113
+ .to_return(status: 200)
114
+ @gibbon.lists.retrieve(headers: { 'X-Custom' => 'custom-value' })
115
+ expect(stub).to have_been_requested
116
+ end
117
+ end
118
+
119
+ describe "api key handling" do
120
+ it "strips whitespace from the api key" do
121
+ gibbon = Gibbon::Request.new(api_key: " #{api_key} ")
122
+ expect(gibbon.api_key).to eq(api_key)
123
+ end
124
+ end
125
+
126
+ describe "class-level requests" do
127
+ before do
128
+ Gibbon::Request.api_key = api_key
129
+ end
130
+
131
+ after do
132
+ Gibbon::Request.api_key = nil
133
+ end
134
+
135
+ it "makes requests directly on the class" do
136
+ stub = stub_request(:get, "#{api_root}/lists").with(basic_auth: basic_auth_credentials).to_return(status: 200)
137
+ Gibbon::Request.lists.retrieve
138
+ expect(stub).to have_been_requested
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gibbon::Response do
4
+ it "defaults body and headers to empty hashes" do
5
+ response = Gibbon::Response.new
6
+ expect(response.body).to eq({})
7
+ expect(response.headers).to eq({})
8
+ end
9
+
10
+ it "exposes the body and headers it is given" do
11
+ body = { "id" => "abc" }
12
+ headers = { "content-type" => "application/json" }
13
+ response = Gibbon::Response.new(body: body, headers: headers)
14
+ expect(response.body).to eq(body)
15
+ expect(response.headers).to eq(headers)
16
+ end
17
+ end