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

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 (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