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
@@ -4,24 +4,33 @@ require "faraday_middleware"
4
4
  require "scalingo/bearer_token"
5
5
  require "scalingo/errors"
6
6
  require "scalingo/auth"
7
+ require "scalingo/billing"
7
8
  require "scalingo/regional"
8
9
 
9
10
  module Scalingo
10
11
  class Client
11
12
  extend Forwardable
12
13
 
14
+ attr_reader :config
15
+
16
+ def initialize(attributes = {})
17
+ @config = Configuration.new(attributes, Scalingo.config)
18
+
19
+ define_regions!
20
+ end
21
+
13
22
  ## Authentication helpers / Token management
14
23
  attr_reader :token
15
24
 
16
25
  def token=(input)
17
- @token = input.is_a?(BearerToken) ? input : BearerToken.new(input.to_s)
26
+ @token = input.is_a?(BearerToken) ? input : BearerToken.new(input.to_s, raise_on_expired: config.raise_on_expired_token)
18
27
  end
19
28
 
20
29
  def authenticated?
21
30
  token.present? && !token.expired?
22
31
  end
23
32
 
24
- def authenticate_with(access_token: nil, bearer_token: nil, expires_in: nil)
33
+ def authenticate_with(access_token: nil, bearer_token: nil, expires_at: nil)
25
34
  if !access_token && !bearer_token
26
35
  raise ArgumentError, "You must supply one of `access_token` or `bearer_token`"
27
36
  end
@@ -30,18 +39,19 @@ module Scalingo
30
39
  raise ArgumentError, "You cannot supply both `access_token` and `bearer_token`"
31
40
  end
32
41
 
33
- if expires_in && !bearer_token
34
- raise ArgumentError, "`expires_in` can only be used with `bearer_token`. `access_token` have a 1 hour expiration."
42
+ if expires_at && !bearer_token
43
+ raise ArgumentError, "`expires_at` can only be used with `bearer_token`. `access_token` have a 1 hour expiration."
35
44
  end
36
45
 
37
46
  if access_token
38
- expiration = Time.now + Scalingo.config.exchanged_token_validity
47
+ expiration = Time.now + config.exchanged_token_validity
39
48
  response = auth.tokens.exchange(access_token)
40
49
 
41
50
  if response.successful?
42
51
  self.token = BearerToken.new(
43
52
  response.data[:token],
44
- expires_in: expiration,
53
+ expires_at: expiration,
54
+ raise_on_expired: config.raise_on_expired_token,
45
55
  )
46
56
  end
47
57
 
@@ -49,35 +59,33 @@ module Scalingo
49
59
  end
50
60
 
51
61
  if bearer_token
52
- self.token = expires_in ? BearerToken.new(bearer_token.to_s, expires_in: expires_in) : bearer_token
62
+ self.token = if expires_at
63
+ token = bearer_token.is_a?(BearerToken) ? bearer_token.value : bearer_token.to_s
64
+
65
+ BearerToken.new(
66
+ token,
67
+ expires_at: expires_at,
68
+ raise_on_expired: config.raise_on_expired_token,
69
+ )
70
+ else
71
+ bearer_token
72
+ end
73
+
74
+ true
53
75
  end
54
76
  end
55
77
 
56
78
  ## API clients
57
79
  def auth
58
- @auth ||= Auth.new(self, Scalingo.config.urls.auth)
80
+ @auth ||= Auth.new(self, config.auth)
59
81
  end
60
82
 
61
- def region
62
- public_send(Scalingo.config.default_region)
83
+ def billing
84
+ @billing ||= Billing.new(self, config.billing)
63
85
  end
64
86
 
65
- Scalingo.config.regions.each do |region|
66
- if Scalingo.config.urls[region].blank?
67
- raise ArgumentError, "Scalingo: no url configured for region #{region}"
68
- end
69
-
70
- define_method(region) do
71
- region_client = instance_variable_get :"@#{region}"
72
-
73
- unless region_client
74
- region_client = Regional.new(self, Scalingo.config.urls[region])
75
-
76
- instance_variable_set :"@#{region}", region_client
77
- end
78
-
79
- region_client
80
- end
87
+ def region(name = nil)
88
+ public_send(name || config.default_region)
81
89
  end
82
90
 
83
91
  ## Delegations
@@ -101,5 +109,23 @@ module Scalingo
101
109
  def_delegator :region, :notifiers
102
110
  def_delegator :region, :operations
103
111
  def_delegator :region, :scm_repo_links
112
+
113
+ private
114
+
115
+ def define_regions!
116
+ config.regions.each_pair do |region, _|
117
+ define_singleton_method(region) do
118
+ region_client = instance_variable_get :"@#{region}"
119
+
120
+ unless region_client
121
+ region_client = Regional.new(self, config.regions.public_send(region))
122
+
123
+ instance_variable_set :"@#{region}", region_client
124
+ end
125
+
126
+ region_client
127
+ end
128
+ end
129
+ end
104
130
  end
105
131
  end
@@ -0,0 +1,98 @@
1
+ require "active_support/core_ext/numeric/time"
2
+ require "scalingo/version"
3
+ require "ostruct"
4
+
5
+ module Scalingo
6
+ class Configuration
7
+ ATTRIBUTES = [
8
+ # URL to the authentication API
9
+ :auth,
10
+
11
+ # URL to the billing API
12
+ :billing,
13
+
14
+ # List of regions under the form {"region_id": root_url}
15
+ :regions,
16
+
17
+ # Default region. Must match a key in `regions`
18
+ :default_region,
19
+
20
+ # Wether to use https or http
21
+ :https,
22
+
23
+ # Configure the User Agent header
24
+ :user_agent,
25
+
26
+ # For how long is a bearer token considered valid (it will raise passed this delay).
27
+ # Set to nil to never raise.
28
+ :exchanged_token_validity,
29
+
30
+ # Raise an exception when trying to use an authenticated connection without a bearer token set
31
+ # Having this setting to true prevents performing requests that would fail due to lack of authentication headers.
32
+ :raise_on_missing_authentication,
33
+
34
+ # Raise an exception when the bearer token in use is supposed to be invalid
35
+ :raise_on_expired_token,
36
+
37
+ # These headers will be added to every request. Individual methods may override them.
38
+ # This should be a hash or a callable object that returns a hash.
39
+ :additional_headers
40
+ ]
41
+
42
+ ATTRIBUTES.each { |attr| attr_accessor(attr) }
43
+
44
+ def self.default
45
+ new(
46
+ auth: "https://auth.scalingo.com/v1",
47
+ billing: "https://cashmachine.scalingo.com",
48
+ regions: {
49
+ agora_fr1: "https://api.agora-fr1.scalingo.com/v1",
50
+ osc_fr1: "https://api.osc-fr1.scalingo.com/v1",
51
+ osc_secnum_fr1: "https://api.osc-secnum-fr1.scalingo.com/v1"
52
+ },
53
+ default_region: :osc_fr1,
54
+ user_agent: "Scalingo Ruby Client v#{Scalingo::VERSION}",
55
+ exchanged_token_validity: 1.hour,
56
+ raise_on_missing_authentication: true,
57
+ raise_on_expired_token: false,
58
+ additional_headers: {},
59
+ )
60
+ end
61
+
62
+ def initialize(attributes = {}, parent = nil)
63
+ ATTRIBUTES.each do |name|
64
+ public_send("#{name}=", attributes.fetch(name, parent&.public_send(name)))
65
+ end
66
+ end
67
+
68
+ def regions=(input)
69
+ if input.is_a?(OpenStruct)
70
+ @regions = input
71
+ return
72
+ end
73
+
74
+ if input.is_a?(Hash)
75
+ @regions = OpenStruct.new(input)
76
+ return
77
+ end
78
+
79
+ raise ArgumentError, "wrong type for argument"
80
+ end
81
+
82
+ def default_region=(input)
83
+ input = input.to_sym
84
+
85
+ raise ArgumentError, "No regions named `#{input}`" unless regions.respond_to?(input)
86
+
87
+ @default_region = input
88
+ end
89
+ end
90
+
91
+ def self.config
92
+ @config ||= Configuration.default
93
+ end
94
+
95
+ def self.configure
96
+ yield config
97
+ end
98
+ end
@@ -83,7 +83,7 @@ module Scalingo
83
83
  def categories(headers = nil, &block)
84
84
  data = nil
85
85
 
86
- response = connection(allow_guest: true).get(
86
+ response = connection(fallback_to_guest: true).get(
87
87
  "addon_categories",
88
88
  data,
89
89
  headers,
@@ -96,7 +96,7 @@ module Scalingo
96
96
  def providers(headers = nil, &block)
97
97
  data = nil
98
98
 
99
- response = connection(allow_guest: true).get(
99
+ response = connection(fallback_to_guest: true).get(
100
100
  "addon_providers",
101
101
  data,
102
102
  headers,
@@ -44,7 +44,7 @@ module Scalingo
44
44
  def sizes(headers = nil, &block)
45
45
  data = nil
46
46
 
47
- response = connection(allow_guest: true).get(
47
+ response = connection(fallback_to_guest: true).get(
48
48
  "features/container_sizes",
49
49
  data,
50
50
  headers,
@@ -29,7 +29,7 @@ module Scalingo
29
29
  def types(headers = nil, &block)
30
30
  data = nil
31
31
 
32
- response = connection(allow_guest: true).get(
32
+ response = connection(fallback_to_guest: true).get(
33
33
  "event_types",
34
34
  data,
35
35
  headers,
@@ -42,7 +42,7 @@ module Scalingo
42
42
  def categories(headers = nil, &block)
43
43
  data = nil
44
44
 
45
- response = connection(allow_guest: true).get(
45
+ response = connection(fallback_to_guest: true).get(
46
46
  "event_categories",
47
47
  data,
48
48
  headers,
@@ -3,7 +3,7 @@ module Scalingo
3
3
  def get(url, payload = {}, headers = nil, &block)
4
4
  data = payload.compact
5
5
 
6
- response = connection(allow_guest: true).get(
6
+ response = connection(fallback_to_guest: true).get(
7
7
  url,
8
8
  data,
9
9
  headers,
@@ -28,7 +28,7 @@ module Scalingo
28
28
  def types(headers = nil, &block)
29
29
  data = nil
30
30
 
31
- response = connection(allow_guest: true).get(
31
+ response = connection(fallback_to_guest: true).get(
32
32
  "features/metrics",
33
33
  data,
34
34
  headers,
@@ -83,7 +83,7 @@ module Scalingo
83
83
  def platforms(headers = nil, &block)
84
84
  data = nil
85
85
 
86
- response = connection(allow_guest: true).get(
86
+ response = connection(fallback_to_guest: true).get(
87
87
  "notification_platforms",
88
88
  data,
89
89
  headers,
@@ -3,10 +3,18 @@ require "scalingo/api/endpoint"
3
3
  module Scalingo
4
4
  class Regional::Operations < API::Endpoint
5
5
  def find(app_id, operation_id, headers = nil, &block)
6
+ get(
7
+ "apps/#{app_id}/operations/#{operation_id}",
8
+ headers,
9
+ &block
10
+ )
11
+ end
12
+
13
+ def get(url, headers = nil, &block)
6
14
  data = nil
7
15
 
8
16
  response = connection.get(
9
- "apps/#{app_id}/operations/#{operation_id}",
17
+ url,
10
18
  data,
11
19
  headers,
12
20
  &block
@@ -1,3 +1,3 @@
1
1
  module Scalingo
2
- VERSION = "3.0.0.beta.1"
2
+ VERSION = "3.0.0.beta.2"
3
3
  end
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "882d7733-923b-40dc-88e6-f1324e48c42a",
3
+ "create": {
4
+ "valid": {
5
+ "name": "Billing profile",
6
+ "address_line1": "1, rue de la paix",
7
+ "address_zip": "75001",
8
+ "address_city": "Paris",
9
+ "address_country": "France"
10
+ },
11
+ "invalid": {
12
+ "some": "attribute"
13
+ }
14
+ },
15
+ "update": {
16
+ "valid": {
17
+ "address_city": "New Somecity"
18
+ },
19
+ "invalid": {
20
+ "address_country": "not a country"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "path": "/profiles",
3
+ "method": "post",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token",
7
+ "Content-Type": "application/json"
8
+ },
9
+ "json_body": {
10
+ "profile": {
11
+ "name": "Billing profile",
12
+ "address_line1": "1, rue de la paix",
13
+ "address_zip": "75001",
14
+ "address_city": "Paris",
15
+ "address_country": "France"
16
+ }
17
+ }
18
+ },
19
+ "response": {
20
+ "status": 201,
21
+ "headers": {
22
+ "Date": "Fri, 12 Jun 2020 15:46:11 GMT",
23
+ "Content-Type": "application/json; charset=utf-8",
24
+ "Transfer-Encoding": "chunked",
25
+ "Connection": "keep-alive",
26
+ "Cache-Control": "no-cache"
27
+ },
28
+ "json_body": {
29
+ "profile": {
30
+ "id": "882d7733-923b-40dc-88e6-f1324e48c42a",
31
+ "name": "Billing profile",
32
+ "email": null,
33
+ "address_line1": "1, rue de la paix",
34
+ "address_line2": null,
35
+ "address_city": "Paris",
36
+ "balance": 0,
37
+ "address_zip": "75001",
38
+ "address_state": null,
39
+ "address_country": "FR",
40
+ "vat_number": null,
41
+ "company": null,
42
+ "payment_method": "sepa",
43
+ "created_at": "2020-05-29T13:01:46.217Z",
44
+ "updated_at": "2020-06-12T15:45:43.535Z",
45
+ "credit": 0,
46
+ "stripe_payment_method": null
47
+ }
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "path": "/profiles",
3
+ "method": "post",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token",
7
+ "Content-Type": "application/json"
8
+ },
9
+ "json_body": {
10
+ "profile": {
11
+ }
12
+ }
13
+ },
14
+ "response": {
15
+ "status": 400,
16
+ "headers": {
17
+ "Date": "Fri, 12 Jun 2020 15:46:10 GMT",
18
+ "Content-Type": "application/json; charset=utf-8",
19
+ "Transfer-Encoding": "chunked",
20
+ "Connection": "keep-alive",
21
+ "Cache-Control": "no-cache"
22
+ },
23
+ "json_body": {
24
+ "error": "profile is required"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "path": "/profiles",
3
+ "method": "post",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token",
7
+ "Content-Type": "application/json"
8
+ },
9
+ "json_body": {
10
+ "profile": {
11
+ "some": "attribute"
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
+ "name": [
27
+ "can't be blank"
28
+ ],
29
+ "address_line1": [
30
+ "can't be blank"
31
+ ],
32
+ "address_zip": [
33
+ "can't be blank"
34
+ ],
35
+ "address_city": [
36
+ "can't be blank"
37
+ ],
38
+ "address_country": [
39
+ "can't be blank"
40
+ ]
41
+ }
42
+ }
43
+ }
44
+ }