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.
- checksums.yaml +4 -4
- data/.document +3 -3
- data/.github/workflows/ci.yml +18 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +11 -1
- data/Gemfile +0 -8
- data/README.markdown +121 -73
- data/gibbon.gemspec +15 -5
- data/lib/gibbon/api_request.rb +30 -1
- data/lib/gibbon/gibbon_error.rb +3 -0
- data/lib/gibbon/gibbon_helpers.rb +9 -2
- data/lib/gibbon/mailchimp_error.rb +20 -1
- data/lib/gibbon/request.rb +94 -3
- data/lib/gibbon/response.rb +15 -2
- data/lib/gibbon/version.rb +2 -1
- data/lib/gibbon.rb +1 -0
- data/spec/gibbon/api_request_spec.rb +173 -0
- data/spec/gibbon/gibbon_helpers_spec.rb +27 -0
- data/spec/gibbon/gibbon_spec.rb +16 -0
- data/spec/gibbon/mailchimp_error_spec.rb +18 -0
- data/spec/gibbon/request_spec.rb +141 -0
- data/spec/gibbon/response_spec.rb +17 -0
- metadata +51 -21
- data/.travis.yml +0 -13
data/lib/gibbon/request.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
18
|
-
@debug = debug
|
|
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
|
data/lib/gibbon/response.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/gibbon/version.rb
CHANGED
data/lib/gibbon.rb
CHANGED
|
@@ -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
|
data/spec/gibbon/gibbon_spec.rb
CHANGED
|
@@ -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
|