scalingo 3.0.0.beta.1 → 3.0.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/CHANGELOG.md +13 -1
  4. data/README.md +41 -27
  5. data/lib/scalingo.rb +1 -1
  6. data/lib/scalingo/api/client.rb +34 -17
  7. data/lib/scalingo/api/endpoint.rb +1 -1
  8. data/lib/scalingo/api/response.rb +21 -32
  9. data/lib/scalingo/bearer_token.rb +11 -5
  10. data/lib/scalingo/billing.rb +11 -0
  11. data/lib/scalingo/billing/profile.rb +46 -0
  12. data/lib/scalingo/client.rb +52 -26
  13. data/lib/scalingo/configuration.rb +98 -0
  14. data/lib/scalingo/regional/addons.rb +2 -2
  15. data/lib/scalingo/regional/containers.rb +1 -1
  16. data/lib/scalingo/regional/events.rb +2 -2
  17. data/lib/scalingo/regional/logs.rb +1 -1
  18. data/lib/scalingo/regional/metrics.rb +1 -1
  19. data/lib/scalingo/regional/notifiers.rb +1 -1
  20. data/lib/scalingo/regional/operations.rb +9 -1
  21. data/lib/scalingo/version.rb +1 -1
  22. data/samples/billing/profile/_meta.json +23 -0
  23. data/samples/billing/profile/create-201.json +50 -0
  24. data/samples/billing/profile/create-400.json +27 -0
  25. data/samples/billing/profile/create-422.json +44 -0
  26. data/samples/billing/profile/show-200.json +41 -0
  27. data/samples/billing/profile/show-404.json +22 -0
  28. data/samples/billing/profile/update-200.json +47 -0
  29. data/samples/billing/profile/update-422.json +32 -0
  30. data/scalingo.gemspec +1 -3
  31. data/spec/scalingo/api/client_spec.rb +168 -0
  32. data/spec/scalingo/api/endpoint_spec.rb +30 -0
  33. data/spec/scalingo/api/response_spec.rb +285 -0
  34. data/spec/scalingo/auth_spec.rb +15 -0
  35. data/spec/scalingo/bearer_token_spec.rb +72 -0
  36. data/spec/scalingo/billing/profile_spec.rb +55 -0
  37. data/spec/scalingo/client_spec.rb +93 -0
  38. data/spec/scalingo/configuration_spec.rb +55 -0
  39. data/spec/scalingo/regional/operations_spec.rb +11 -3
  40. data/spec/scalingo/regional_spec.rb +14 -0
  41. metadata +33 -40
  42. data/Gemfile.lock +0 -110
  43. data/lib/scalingo/config.rb +0 -38
@@ -0,0 +1,41 @@
1
+ {
2
+ "path": "/profile",
3
+ "method": "get",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token"
7
+ }
8
+ },
9
+ "response": {
10
+ "status": 200,
11
+ "headers": {
12
+ "Date": "Fri, 12 Jun 2020 15:46:10 GMT",
13
+ "Etag": "W/\"d1a1d2eed6a6fc5fb399b497a4572e89\"",
14
+ "Content-Type": "application/json; charset=utf-8",
15
+ "Transfer-Encoding": "chunked",
16
+ "Connection": "keep-alive",
17
+ "Cache-Control": "max-age=0, private, must-revalidate"
18
+ },
19
+ "json_body": {
20
+ "profile": {
21
+ "id": "882d7733-923b-40dc-88e6-f1324e48c42a",
22
+ "name": "Billing",
23
+ "email": null,
24
+ "address_line1": "Somewhere",
25
+ "address_line2": null,
26
+ "address_city": "Somecity",
27
+ "balance": 0,
28
+ "address_zip": "12345",
29
+ "address_state": null,
30
+ "address_country": "FR",
31
+ "vat_number": null,
32
+ "company": null,
33
+ "payment_method": "sepa",
34
+ "created_at": "2020-05-29T13:01:46.217Z",
35
+ "updated_at": "2020-06-12T15:45:43.535Z",
36
+ "credit": 0,
37
+ "stripe_payment_method": null
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "path": "/profile",
3
+ "method": "get",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token"
7
+ }
8
+ },
9
+ "response": {
10
+ "status": 404,
11
+ "headers": {
12
+ "Date": "Fri, 12 Jun 2020 15:46:10 GMT",
13
+ "Content-Type": "application/json; charset=utf-8",
14
+ "Transfer-Encoding": "chunked",
15
+ "Connection": "keep-alive",
16
+ "Cache-Control": "no-cache"
17
+ },
18
+ "json_body": {
19
+ "error": "no billing profile"
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "path": "/profiles/882d7733-923b-40dc-88e6-f1324e48c42a",
3
+ "method": "put",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token",
7
+ "Content-Type": "application/json"
8
+ },
9
+ "json_body": {
10
+ "profile": {
11
+ "address_city": "New Somecity"
12
+ }
13
+ }
14
+ },
15
+ "response": {
16
+ "status": 200,
17
+ "headers": {
18
+ "Date": "Fri, 12 Jun 2020 15:46:11 GMT",
19
+ "Etag": "W/\"885ba0ce422a076feb961e3bed89db0c\"",
20
+ "Content-Type": "application/json; charset=utf-8",
21
+ "Transfer-Encoding": "chunked",
22
+ "Connection": "keep-alive",
23
+ "Cache-Control": "max-age=0, private, must-revalidate"
24
+ },
25
+ "json_body": {
26
+ "profile": {
27
+ "id": "882d7733-923b-40dc-88e6-f1324e48c42a",
28
+ "name": "Billing",
29
+ "email": null,
30
+ "address_line1": "Somewhere",
31
+ "address_line2": null,
32
+ "address_city": "New Somecity",
33
+ "balance": 0,
34
+ "address_zip": "12345",
35
+ "address_state": null,
36
+ "address_country": "FR",
37
+ "vat_number": null,
38
+ "company": null,
39
+ "payment_method": "sepa",
40
+ "created_at": "2020-05-29T13:01:46.217Z",
41
+ "updated_at": "2020-06-12T15:46:11.559Z",
42
+ "credit": 0,
43
+ "stripe_payment_method": null
44
+ }
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "path": "/profiles/882d7733-923b-40dc-88e6-f1324e48c42a",
3
+ "method": "put",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token",
7
+ "Content-Type": "application/json"
8
+ },
9
+ "json_body": {
10
+ "profile": {
11
+ "address_country": "not a country"
12
+ }
13
+ }
14
+ },
15
+ "response": {
16
+ "status": 422,
17
+ "headers": {
18
+ "Date": "Fri, 12 Jun 2020 15:46:11 GMT",
19
+ "Content-Type": "application/json; charset=utf-8",
20
+ "Transfer-Encoding": "chunked",
21
+ "Connection": "keep-alive",
22
+ "Cache-Control": "no-cache"
23
+ },
24
+ "json_body": {
25
+ "errors": {
26
+ "address_country": [
27
+ "is not a valid country"
28
+ ]
29
+ }
30
+ }
31
+ }
32
+ }
@@ -35,14 +35,12 @@ Gem::Specification.new do |s|
35
35
  s.test_files = Dir["spec/**/*_spec.rb"]
36
36
 
37
37
  s.add_dependency "activesupport", [">= 5", "< 7"]
38
- s.add_dependency "activemodel", [">= 5", "< 7"]
39
- s.add_dependency "dry-configurable", "~> 0.11"
40
38
  s.add_dependency "faraday", "~> 1.0.1"
41
39
  s.add_dependency "faraday_middleware", "~> 1.0.0"
42
40
  s.add_dependency "multi_json", ">= 1.0.3", "~> 1.0"
43
41
 
44
42
  s.add_development_dependency "bundler", "~> 2.0"
45
- s.add_development_dependency "rake", "~> 10.0"
43
+ s.add_development_dependency "rake", "~> 13.0"
46
44
  s.add_development_dependency "rspec", "~> 3.0"
47
45
  s.add_development_dependency "rubocop", "~> 0.83.0"
48
46
  s.add_development_dependency "standard", "~> 0.4.2"
@@ -0,0 +1,168 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Scalingo::API::Client do
4
+ let(:url) { "http://localhost" }
5
+
6
+ subject { described_class.new(scalingo, url) }
7
+
8
+ describe "initialize" do
9
+ it "stores the scalingo client and the url" do
10
+ instance = described_class.new(:scalingo, :url)
11
+
12
+ expect(instance.scalingo).to eq(:scalingo)
13
+ expect(instance.url).to eq(:url)
14
+ end
15
+ end
16
+
17
+ describe "self.register_handler(s)!" do
18
+ it "is called for each key/value pair" do
19
+ expect(described_class).to receive(:register_handler!).with(:a, :b).once
20
+ expect(described_class).to receive(:register_handler!).with(:c, :d).once
21
+
22
+ described_class.register_handlers!(a: :b, c: :d)
23
+ end
24
+
25
+ it "defines a lazy-loaded memoized getter, returning an instance of the class supplied" do
26
+ mock = double
27
+
28
+ described_class.register_handler!(:handler, mock)
29
+ instance = described_class.new(:scalingo, :url)
30
+
31
+ # Only 1 instanciation should be done, no matter how many calls are done below
32
+ expect(mock).to receive(:new).with(instance).and_return("1st").once
33
+
34
+ # Not yet loaded...
35
+ expect(instance.instance_variable_get(:"@handler")).to eq(nil)
36
+ instance.handler
37
+
38
+ # Memoized...
39
+ expect(instance.instance_variable_get(:"@handler")).not_to eq(nil)
40
+
41
+ # More calls won't try to perform more instanciations
42
+ instance.handler
43
+ instance.handler
44
+ end
45
+ end
46
+
47
+ describe "headers" do
48
+ before do
49
+ expect(scalingo.config).to receive(:user_agent).and_return(user_agent).once
50
+ end
51
+
52
+ let(:user_agent) { "user agent" }
53
+ let(:extra_hash) { {"X-Other" => "other"} }
54
+ let(:extra_block) {
55
+ proc { {"X-Another" => "another"} }
56
+ }
57
+
58
+ it "only returns the user agent if nothing else is configured" do
59
+ expect(subject.headers).to eq("User-Agent" => user_agent)
60
+ end
61
+
62
+ it "allows additional headers to be globally configured" do
63
+ expect(scalingo.config).to receive(:additional_headers).and_return(extra_hash)
64
+
65
+ expect(subject.headers).to eq("User-Agent" => user_agent, "X-Other" => "other")
66
+ end
67
+
68
+ it "additional headers can be a block" do
69
+ expect(scalingo.config).to receive(:additional_headers).and_return(extra_block)
70
+
71
+ expect(subject.headers).to eq("User-Agent" => user_agent, "X-Another" => "another")
72
+ end
73
+ end
74
+
75
+ describe "connection_options" do
76
+ it "returns the url and headers" do
77
+ expect(subject).to receive(:url).and_return("url").once
78
+ expect(subject).to receive(:headers).and_return("headers").once
79
+
80
+ expect(subject.connection_options).to eq(url: "url", headers: "headers")
81
+ end
82
+ end
83
+
84
+ describe "unauthenticated_connection" do
85
+ it "returns a memoized object" do
86
+ expect(Faraday).to receive(:new).with(subject.connection_options).and_return("faraday").once
87
+
88
+ expect(subject.unauthenticated_connection).to eq "faraday"
89
+
90
+ subject.unauthenticated_connection
91
+ subject.unauthenticated_connection
92
+ end
93
+
94
+ it "has no authentication header set" do
95
+ expect(subject.unauthenticated_connection.headers.key?("Authorization")).not_to be true
96
+ end
97
+ end
98
+
99
+ describe "authenticated_connection" do
100
+ context "without bearer token" do
101
+ let(:scalingo) { scalingo_guest }
102
+
103
+ it "raises if configured to" do
104
+ expect(scalingo.config).to receive(:raise_on_missing_authentication).and_return(true).once
105
+
106
+ expect {
107
+ subject.authenticated_connection
108
+ }.to raise_error(Scalingo::Error::Unauthenticated)
109
+ end
110
+
111
+ it "returns an unauthenticated connection if configured to not raise" do
112
+ expect(scalingo.config).to receive(:raise_on_missing_authentication).and_return(false).once
113
+
114
+ expect(subject).to receive(:unauthenticated_connection).and_return(:object).once
115
+ expect(subject.authenticated_connection).to eq(:object)
116
+ end
117
+ end
118
+
119
+ context "with bearer token" do
120
+ it "has an authentication header set with a bearer scheme" do
121
+ expect(subject.connection.headers["Authorization"]).to eq "Bearer #{subject.scalingo.token.value}"
122
+ end
123
+ end
124
+ end
125
+
126
+ describe "connection" do
127
+ context "logged" do
128
+ context "no fallback to guest" do
129
+ it "calls and return the authenticated_connection" do
130
+ expect(subject).to receive(:authenticated_connection).and_return(:conn)
131
+ expect(subject.connection).to eq(:conn)
132
+ end
133
+ end
134
+
135
+ context "with fallback to guest" do
136
+ it "calls and return the authenticated_connection" do
137
+ expect(subject).to receive(:authenticated_connection).and_return(:conn)
138
+ expect(subject.connection(fallback_to_guest: true)).to eq(:conn)
139
+ end
140
+ end
141
+ end
142
+
143
+ context "not logged" do
144
+ let(:scalingo) { scalingo_guest }
145
+
146
+ context "no fallback to guest" do
147
+ it "raises when set to raise" do
148
+ expect(scalingo.config).to receive(:raise_on_missing_authentication).and_return(true).once
149
+
150
+ expect { subject.connection }.to raise_error(Scalingo::Error::Unauthenticated)
151
+ end
152
+
153
+ it "calls and return the unauthenticated_connection when set not to raise" do
154
+ expect(scalingo.config).to receive(:raise_on_missing_authentication).and_return(false).once
155
+ expect(subject).to receive(:unauthenticated_connection).and_return(:conn)
156
+ expect(subject.connection(fallback_to_guest: true)).to eq(:conn)
157
+ end
158
+ end
159
+
160
+ context "with fallback to guest" do
161
+ it "calls and return the unauthenticated_connection" do
162
+ expect(subject).to receive(:unauthenticated_connection).and_return(:conn)
163
+ expect(subject.connection(fallback_to_guest: true)).to eq(:conn)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,30 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Scalingo::API::Endpoint do
4
+ let(:client) { double }
5
+
6
+ subject { described_class.new(client) }
7
+
8
+ describe "initialize" do
9
+ it "stores the client" do
10
+ instance = described_class.new(:client)
11
+
12
+ expect(instance.client).to eq(:client)
13
+ end
14
+ end
15
+
16
+ describe "connection" do
17
+ it "delegates the connection to the client" do
18
+ expect(client).to receive(:connection).and_return(:value).once
19
+ expect(subject.connection).to eq :value
20
+ end
21
+ end
22
+
23
+ describe "unpack" do
24
+ it "forwards unpack to Response" do
25
+ expect(Scalingo::API::Response).to receive(:unpack).with(client, :a, :b, :c).and_return(:d).once
26
+
27
+ expect(subject.send(:unpack, :a, :b, :c)).to eq :d
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,285 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Scalingo::API::Response do
4
+ let(:client) { regional }
5
+ let(:status) { 200 }
6
+ let(:headers) { {} }
7
+ let(:data) { "" }
8
+ let(:full_body) { "" }
9
+ let(:meta_object) { nil }
10
+
11
+ subject {
12
+ described_class.new(
13
+ client: client,
14
+ status: status,
15
+ headers: headers,
16
+ data: data,
17
+ full_body: full_body,
18
+ meta: meta_object,
19
+ )
20
+ }
21
+
22
+ describe "self.unpack" do
23
+ let(:body) { "" }
24
+ let(:success) { true }
25
+
26
+ let(:response) {
27
+ OpenStruct.new(
28
+ body: body,
29
+ status: status,
30
+ headers: headers,
31
+ success?: success,
32
+ )
33
+ }
34
+
35
+ it "passes the client supplied" do
36
+ object = described_class.unpack(:some_client, response)
37
+
38
+ expect(object.client).to eq :some_client
39
+ end
40
+
41
+ it "passes the response status" do
42
+ object = described_class.unpack(:client, response)
43
+
44
+ expect(object.status).to eq status
45
+ end
46
+
47
+ it "passes the response headers" do
48
+ object = described_class.unpack(:client, response)
49
+
50
+ expect(object.headers).to eq headers
51
+ end
52
+
53
+ context "with an empty body" do
54
+ let(:body) { "" }
55
+
56
+ it "without key" do
57
+ object = described_class.unpack(client, response)
58
+
59
+ expect(object.data).to eq ""
60
+ expect(object.full_body).to eq ""
61
+ expect(object.meta).to eq nil
62
+ end
63
+
64
+ it "ignores key if supplied" do
65
+ object = described_class.unpack(client, response, key: :key)
66
+
67
+ expect(object.data).to eq ""
68
+ expect(object.full_body).to eq ""
69
+ expect(object.meta).to eq nil
70
+ end
71
+ end
72
+
73
+ context "with a nil body" do
74
+ let(:body) { nil }
75
+
76
+ it "without key" do
77
+ object = described_class.unpack(client, response)
78
+
79
+ expect(object.data).to eq nil
80
+ expect(object.full_body).to eq nil
81
+ expect(object.meta).to eq nil
82
+ end
83
+
84
+ it "ignores key if supplied" do
85
+ object = described_class.unpack(client, response, key: :key)
86
+
87
+ expect(object.data).to eq nil
88
+ expect(object.full_body).to eq nil
89
+ expect(object.meta).to eq nil
90
+ end
91
+ end
92
+
93
+ context "with a string body" do
94
+ let(:body) { "this is a string body, probably due to an error" }
95
+
96
+ it "without key" do
97
+ object = described_class.unpack(client, response)
98
+
99
+ expect(object.data).to eq body
100
+ expect(object.full_body).to eq body
101
+ expect(object.meta).to eq nil
102
+ end
103
+
104
+ it "ignores key if supplied" do
105
+ object = described_class.unpack(client, response, key: :key)
106
+
107
+ expect(object.data).to eq body
108
+ expect(object.full_body).to eq body
109
+ expect(object.meta).to eq nil
110
+ end
111
+ end
112
+
113
+ context "with an json (array) body" do
114
+ let(:body) {
115
+ [{key: :value}]
116
+ }
117
+
118
+ it "without key" do
119
+ object = described_class.unpack(client, response)
120
+
121
+ expect(object.data).to eq body
122
+ expect(object.full_body).to eq body
123
+ expect(object.meta).to eq nil
124
+ end
125
+
126
+ it "ignores key if supplied" do
127
+ object = described_class.unpack(client, response, key: :root)
128
+
129
+ expect(object.data).to eq body
130
+ expect(object.full_body).to eq body
131
+ expect(object.meta).to eq nil
132
+ end
133
+ end
134
+
135
+ context "with a json (hash) body" do
136
+ let(:body) {
137
+ {root: {key: :value}}
138
+ }
139
+
140
+ it "without key" do
141
+ object = described_class.unpack(client, response)
142
+
143
+ expect(object.data).to eq body
144
+ expect(object.full_body).to eq body
145
+ expect(object.meta).to eq nil
146
+ end
147
+
148
+ it "with valid key" do
149
+ object = described_class.unpack(client, response, key: :root)
150
+
151
+ expect(object.data).to eq({key: :value})
152
+ expect(object.full_body).to eq body
153
+ expect(object.meta).to eq nil
154
+ end
155
+
156
+ it "with invalid key" do
157
+ object = described_class.unpack(client, response, key: :other)
158
+
159
+ expect(object.data).to eq nil
160
+ expect(object.full_body).to eq body
161
+ expect(object.meta).to eq nil
162
+ end
163
+
164
+ context "with meta" do
165
+ let(:body) {
166
+ {root: {key: :value}, meta: {meta1: :value}}
167
+ }
168
+
169
+ it "extracts the meta object" do
170
+ object = described_class.unpack(client, response)
171
+
172
+ expect(object.meta).to eq({meta1: :value})
173
+ end
174
+ end
175
+ end
176
+
177
+ context "with an error response" do
178
+ let(:success) { false }
179
+ let(:body) { {root: {key: :value}} }
180
+
181
+ it "does not dig in the response hash, even with a valid key" do
182
+ object = described_class.unpack(client, response, key: :root)
183
+
184
+ expect(object.data).to eq body
185
+ expect(object.full_body).to eq body
186
+ expect(object.meta).to eq nil
187
+ end
188
+ end
189
+ end
190
+
191
+ describe "successful?" do
192
+ context "is true when 2XX" do
193
+ let(:status) { 200 }
194
+ it { expect(subject.successful?).to be true }
195
+ end
196
+
197
+ context "is false when 3XX" do
198
+ let(:status) { 300 }
199
+ it { expect(subject.successful?).to be false }
200
+ end
201
+
202
+ context "is false when 4XX" do
203
+ let(:status) { 400 }
204
+ it { expect(subject.successful?).to be false }
205
+ end
206
+
207
+ context "is false when 5XX" do
208
+ let(:status) { 500 }
209
+ it { expect(subject.successful?).to be false }
210
+ end
211
+ end
212
+
213
+ describe "paginated?" do
214
+ context "with pagination metadata" do
215
+ let(:meta_object) {
216
+ {pagination: {page: 1}}
217
+ }
218
+
219
+ it { expect(subject.paginated?).to be true }
220
+ end
221
+
222
+ context "without pagination metadata" do
223
+ let(:meta_object) {
224
+ {messages: []}
225
+ }
226
+
227
+ it { expect(subject.paginated?).to be false }
228
+ end
229
+ end
230
+
231
+ describe "operation" do
232
+ context "with an operation url" do
233
+ before do
234
+ load_meta!(api: :regional, folder: :operations)
235
+ register_stubs!("find-200", api: :regional, folder: :operations)
236
+ end
237
+
238
+ let(:url) {
239
+ path = "/apps/#{meta[:app_id]}/operations/#{meta[:id]}"
240
+ File.join(Scalingo::ENDPOINTS[:regional], path)
241
+ }
242
+
243
+ let(:headers) {
244
+ {location: url}
245
+ }
246
+
247
+ it { expect(subject.operation?).to be true }
248
+ it { expect(subject.operation_url).to eq url }
249
+
250
+ it "can request the operation" do
251
+ response = subject.operation
252
+
253
+ expect(response).to be_successful
254
+ expect(response.data[:id]).to be_present
255
+ expect(response.data[:status]).to be_present
256
+ expect(response.data[:type]).to be_present
257
+ end
258
+
259
+ it "delegates the operation to the given client" do
260
+ mock = double
261
+
262
+ expect(subject.client).to receive(:operations).and_return(mock)
263
+ expect(mock).to receive(:get).with(url).and_return(:response)
264
+
265
+ expect(subject.operation).to eq :response
266
+ end
267
+
268
+ context "when the client doesn't know about operations" do
269
+ let(:client) { auth }
270
+
271
+ it "fails silently" do
272
+ expect(subject.operation).to eq nil
273
+ end
274
+ end
275
+ end
276
+
277
+ context "without an operation url" do
278
+ let(:meta_object) { {} }
279
+
280
+ it { expect(subject.operation?).to be false }
281
+ it { expect(subject.operation_url).to eq nil }
282
+ it { expect(subject.operation).to eq nil }
283
+ end
284
+ end
285
+ end