ruby_http_client 3.2.0 → 3.5.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.
@@ -6,9 +6,28 @@ headers = JSON.parse('
6
6
  "Authorization": "Bearer ' + ENV['SENDGRID_API_KEY'] + '"
7
7
  }
8
8
  ')
9
- host = 'https://api.sendgrid.com'
9
+ host = 'https://api.sendgrid.com'
10
10
  client = SendGrid::Client.new(host: host, request_headers: headers)
11
11
 
12
+ # You can pass in an http_options hash to set values for NET::HTTP attributes
13
+ # https://ruby-doc.org/stdlib-2.4.1/libdoc/net/http/rdoc/Net/HTTP.html
14
+ # client = SendGrid::Client.new(host: host,
15
+ # request_headers: headers,
16
+ # http_options: {open_timeout: 15, read_timeout: 30})
17
+
18
+ # If you want to make request via proxy, you can set your proxy server in two ways.
19
+ #
20
+ # (1) Pass proxy_options hash
21
+ #
22
+ # client = SendGrid::Client.new(host: host,
23
+ # request_headers: headers,
24
+ # proxy_options: { host: '127.0.0.1', port: 8080 })
25
+ #
26
+ # (2) Set 'http_proxy' environment variable
27
+ #
28
+ # ENV['http_proxy'] = 'user:pass@127.0.0.1:8080'
29
+ # client = SendGrid::Client.new(host: host, request_headers: headers)
30
+
12
31
  # GET Collection
13
32
  query_params = { 'limit' => 100, 'offset' => 0 }
14
33
  response = client.version('v3').api_keys.get(query_params: query_params)
@@ -69,3 +88,12 @@ puts response.headers
69
88
  response = client.api_keys._(api_key_id).delete
70
89
  puts response.status_code
71
90
  puts response.headers
91
+
92
+ # Rate limit information
93
+ response = client.version('v3').api_keys._(api_key_id).get
94
+ puts response.ratelimit.limit
95
+ puts response.ratelimit.remaining
96
+ puts response.ratelimit.reset
97
+ puts response.ratelimit.exceeded?
98
+ # Sleep the current thread until the reset has happened
99
+ response.ratelimit.wait!
@@ -6,10 +6,54 @@ module SendGrid
6
6
 
7
7
  # Holds the response from an API call.
8
8
  class Response
9
+ # Provide useful functionality around API rate limiting.
10
+ class Ratelimit
11
+ attr_reader :limit, :remaining, :reset
12
+
13
+ # * *Args* :
14
+ # - +limit+ -> The total number of requests allowed within a rate limit window
15
+ # - +remaining+ -> The number of requests that have been processed within this current rate limit window
16
+ # - +reset+ -> The time (in seconds since Unix Epoch) when the rate limit will reset
17
+ def initialize(limit, remaining, reset)
18
+ @limit = limit.to_i
19
+ @remaining = remaining.to_i
20
+ @reset = Time.at reset.to_i
21
+ end
22
+
23
+ def exceeded?
24
+ remaining <= 0
25
+ end
26
+
27
+ # * *Returns* :
28
+ # - The number of requests that have been used out of this
29
+ # rate limit window
30
+ def used
31
+ limit - remaining
32
+ end
33
+
34
+ # Sleep until the reset time arrives. If given a block, it will
35
+ # be called after sleeping is finished.
36
+ #
37
+ # * *Returns* :
38
+ # - The amount of time (in seconds) that the rate limit slept
39
+ # for.
40
+ def wait!
41
+ now = Time.now.utc.to_i
42
+ duration = (reset.to_i - now) + 1
43
+
44
+ sleep duration if duration >= 0
45
+
46
+ yield if block_given?
47
+
48
+ duration
49
+ end
50
+ end
51
+
9
52
  # * *Args* :
10
53
  # - +response+ -> A NET::HTTP response object
11
54
  #
12
55
  attr_reader :status_code, :body, :headers
56
+
13
57
  def initialize(response)
14
58
  @status_code = response.code
15
59
  @body = response.body
@@ -21,6 +65,20 @@ module SendGrid
21
65
  def parsed_body
22
66
  @parsed_body ||= JSON.parse(@body, symbolize_names: true)
23
67
  end
68
+
69
+ def ratelimit
70
+ return @ratelimit unless @ratelimit.nil?
71
+
72
+ limit = headers['X-RateLimit-Limit']
73
+ remaining = headers['X-RateLimit-Remaining']
74
+ reset = headers['X-RateLimit-Reset']
75
+
76
+ # Guard against possibility that one (or probably, all) of the
77
+ # needed headers were not returned.
78
+ @ratelimit = Ratelimit.new(limit, remaining, reset) if limit && remaining && reset
79
+
80
+ @ratelimit
81
+ end
24
82
  end
25
83
 
26
84
  # A simple REST client.
@@ -35,15 +93,19 @@ module SendGrid
35
93
  # Or just pass the version as part of the URL
36
94
  # (e.g. client._("/v3"))
37
95
  # - +url_path+ -> A list of the url path segments
96
+ # - +proxy_options+ -> A hash of proxy settings.
97
+ # (e.g. { host: '127.0.0.1', port: 8080 })
38
98
  #
39
- def initialize(host: nil, request_headers: nil, version: nil, url_path: nil)
99
+ def initialize(host: nil, request_headers: nil, version: nil, url_path: nil, http_options: {}, proxy_options: {}) # rubocop:disable Metrics/ParameterLists
40
100
  @host = host
41
101
  @request_headers = request_headers || {}
42
102
  @version = version
43
103
  @url_path = url_path || []
44
- @methods = %w(delete get patch post put)
104
+ @methods = %w[delete get patch post put]
45
105
  @query_params = nil
46
106
  @request_body = nil
107
+ @http_options = http_options
108
+ @proxy_options = proxy_options
47
109
  end
48
110
 
49
111
  # Update the headers for the request
@@ -90,7 +152,7 @@ module SendGrid
90
152
  # - The url string with the query parameters appended
91
153
  #
92
154
  def build_query_params(url, query_params)
93
- params = query_params.map { |key, value| "#{key}=#{value}" }.join('&')
155
+ params = URI.encode_www_form(query_params)
94
156
  url.concat("?#{params}")
95
157
  end
96
158
 
@@ -137,21 +199,10 @@ module SendGrid
137
199
  #
138
200
  def build_request(name, args)
139
201
  build_args(args) if args
140
- uri = build_url(query_params: @query_params)
141
- @http = add_ssl(Net::HTTP.new(uri.host, uri.port))
142
- net_http = Kernel.const_get('Net::HTTP::' + name.to_s.capitalize)
143
- @request = build_request_headers(net_http.new(uri.request_uri))
144
- if (@request_body &&
145
- (!@request_headers.has_key?('Content-Type') ||
146
- @request_headers['Content-Type'] == 'application/json')
147
- )
148
- @request.body = @request_body.to_json
149
- @request['Content-Type'] = 'application/json'
150
- elsif !@request_body and (name.to_s == "post")
151
- @request['Content-Type'] = ''
152
- else
153
- @request.body = @request_body
154
- end
202
+ # build the request & http object
203
+ build_http_request(name)
204
+ # set the content type & request body
205
+ update_content_type(name)
155
206
  make_request(@http, @request)
156
207
  end
157
208
 
@@ -169,6 +220,16 @@ module SendGrid
169
220
  Response.new(response)
170
221
  end
171
222
 
223
+ # Build HTTP request object
224
+ #
225
+ # * *Returns* :
226
+ # - Request object
227
+ def build_http(host, port)
228
+ params = [host, port]
229
+ params += @proxy_options.values_at(:host, :port, :user, :pass) unless @proxy_options.empty?
230
+ add_ssl(Net::HTTP.new(*params))
231
+ end
232
+
172
233
  # Allow for https calls
173
234
  #
174
235
  # * *Args* :
@@ -195,20 +256,22 @@ module SendGrid
195
256
  # - Client object
196
257
  #
197
258
  def _(name = nil)
198
- url_path = name ? @url_path.push(name) : @url_path
199
- @url_path = []
259
+ url_path = name ? @url_path + [name] : @url_path
200
260
  Client.new(host: @host, request_headers: @request_headers,
201
- version: @version, url_path: url_path)
261
+ version: @version, url_path: url_path,
262
+ http_options: @http_options)
202
263
  end
203
264
 
204
265
  # Dynamically add segments to the url, then call a method.
205
266
  # (e.g. client.name.name.get())
206
267
  #
207
268
  # * *Args* :
208
- # - The args are autmoatically passed in
269
+ # - The args are automatically passed in
209
270
  # * *Returns* :
210
271
  # - Client object or Response object
211
272
  #
273
+ # rubocop:disable Style/MethodMissingSuper
274
+ # rubocop:disable Style/MissingRespondToMissing
212
275
  def method_missing(name, *args, &_block)
213
276
  # Capture the version
214
277
  if name.to_s == 'version'
@@ -217,8 +280,42 @@ module SendGrid
217
280
  end
218
281
  # We have reached the end of the method chain, make the API call
219
282
  return build_request(name, args) if @methods.include?(name.to_s)
283
+
220
284
  # Add a segment to the URL
221
285
  _(name)
222
286
  end
287
+
288
+ private
289
+
290
+ def build_http_request(http_method)
291
+ uri = build_url(query_params: @query_params)
292
+ net_http = Kernel.const_get('Net::HTTP::' + http_method.to_s.capitalize)
293
+
294
+ @http = build_http(uri.host, uri.port)
295
+ @request = build_request_headers(net_http.new(uri.request_uri))
296
+ end
297
+
298
+ def update_content_type(http_method)
299
+ if @request_body && content_type_json?
300
+ # If body is a hash or array, encode it; else leave it alone
301
+ @request.body = if [Hash, Array].include?(@request_body.class)
302
+ @request_body.to_json
303
+ else
304
+ @request_body
305
+ end
306
+ @request['Content-Type'] = 'application/json'
307
+ elsif !@request_body && http_method.to_s == 'post'
308
+ @request['Content-Type'] = ''
309
+ else
310
+ @request.body = @request_body
311
+ end
312
+ end
313
+
314
+ def content_type_json?
315
+ !@request_headers.key?('Content-Type') ||
316
+ @request_headers['Content-Type'] == 'application/json'
317
+ end
318
+ # rubocop:enable Style/MethodMissingSuper
319
+ # rubocop:enable Style/MissingRespondToMissing
223
320
  end
224
321
  end
@@ -1,20 +1,23 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
 
5
4
  Gem::Specification.new do |spec|
6
- spec.name = 'ruby_http_client'
7
- spec.version = '3.2.0'
8
- spec.authors = ['Elmer Thomas']
9
- spec.email = 'dx@sendgrid.com'
10
- spec.summary = 'A simple REST client'
5
+ spec.name = 'ruby_http_client'
6
+ spec.version = '3.5.1'
7
+ spec.authors = ['Elmer Thomas']
8
+ spec.email = 'help@twilio.com'
9
+ spec.summary = 'A simple REST client'
11
10
  spec.description = 'Quickly and easily access any REST or REST-like API.'
12
- spec.homepage = 'http://github.com/sendgrid/ruby-http-client'
13
- spec.license = 'MIT'
14
- spec.files = `git ls-files -z`.split("\x0")
15
- spec.executables = spec.files.grep(/^bin/) { |f| File.basename(f) }
16
- spec.test_files = spec.files.grep(/^(test|spec|features)/)
11
+ spec.homepage = 'http://github.com/sendgrid/ruby-http-client'
12
+ spec.license = 'MIT'
13
+ spec.files = `git ls-files -z`.split("\x0")
14
+ spec.executables = spec.files.grep(/^bin/) { |f| File.basename(f) }
15
+ spec.test_files = spec.files.grep(/^(test|spec|features)/)
17
16
  spec.require_paths = ['lib']
18
17
 
19
- spec.add_development_dependency 'rake', '~> 0'
18
+ spec.add_development_dependency 'codecov'
19
+ spec.add_development_dependency 'minitest'
20
+ spec.add_development_dependency 'rake'
21
+ spec.add_development_dependency 'rubocop', '~> 0.88.0'
22
+ spec.add_development_dependency 'simplecov', '~> 0.18.5'
20
23
  end
@@ -0,0 +1,7 @@
1
+ if ENV['CI'] == 'true'
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+
5
+ require 'codecov'
6
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
7
+ end
@@ -1,3 +1,4 @@
1
+ require './test/test_helper'
1
2
  require 'ruby_http_client'
2
3
  require 'minitest/autorun'
3
4
 
@@ -11,6 +12,26 @@ class MockResponse
11
12
  end
12
13
  end
13
14
 
15
+ class MockHttpResponse
16
+ attr_reader :code, :body, :headers
17
+
18
+ def initialize(code, body, headers)
19
+ @code = code
20
+ @body = body
21
+ @headers = headers
22
+ end
23
+
24
+ alias to_hash headers
25
+ end
26
+
27
+ class MockResponseWithRequestBody < MockResponse
28
+ attr_reader :request_body
29
+
30
+ def initialize(response)
31
+ @request_body = response['request_body']
32
+ end
33
+ end
34
+
14
35
  class MockRequest < SendGrid::Client
15
36
  def make_request(_http, _request)
16
37
  response = {}
@@ -21,6 +42,17 @@ class MockRequest < SendGrid::Client
21
42
  end
22
43
  end
23
44
 
45
+ class MockRequestWithRequestBody < SendGrid::Client
46
+ def make_request(_http, request)
47
+ response = {}
48
+ response['code'] = 200
49
+ response['body'] = { 'message' => 'success' }
50
+ response['headers'] = { 'headers' => 'test' }
51
+ response['request_body'] = request.body
52
+ MockResponseWithRequestBody.new(response)
53
+ end
54
+ end
55
+
24
56
  class TestClient < Minitest::Test
25
57
  def setup
26
58
  @headers = JSON.parse('
@@ -31,9 +63,14 @@ class TestClient < Minitest::Test
31
63
  ')
32
64
  @host = 'http://localhost:4010'
33
65
  @version = 'v3'
66
+ @http_options = { open_timeout: 60, read_timeout: 60 }
34
67
  @client = MockRequest.new(host: @host,
35
68
  request_headers: @headers,
36
69
  version: @version)
70
+ @client_with_options = MockRequest.new(host: @host,
71
+ request_headers: @headers,
72
+ version: @version,
73
+ http_options: @http_options)
37
74
  end
38
75
 
39
76
  def test_init
@@ -61,9 +98,9 @@ class TestClient < Minitest::Test
61
98
 
62
99
  def test_build_query_params
63
100
  url = ''
64
- query_params = { 'limit' => 100, 'offset' => 0 }
101
+ query_params = { 'limit' => 100, 'offset' => 0, 'categories' => %w[category1 category2] }
65
102
  url = @client.build_query_params(url, query_params)
66
- assert_equal('?limit=100&offset=0', url)
103
+ assert_equal('?limit=100&offset=0&categories=category1&categories=category2', url)
67
104
  end
68
105
 
69
106
  def test_build_url
@@ -91,8 +128,8 @@ class TestClient < Minitest::Test
91
128
  args = nil
92
129
  response = @client.build_request(name, args)
93
130
  assert_equal(200, response.status_code)
94
- assert_equal({'message' => 'success'}, response.body)
95
- assert_equal({'headers' => 'test'}, response.headers)
131
+ assert_equal({ 'message' => 'success' }, response.body)
132
+ assert_equal({ 'headers' => 'test' }, response.headers)
96
133
  end
97
134
 
98
135
  def test_build_request_post_empty_content_type
@@ -103,7 +140,7 @@ class TestClient < Minitest::Test
103
140
  request_headers: headers,
104
141
  version: 'v3'
105
142
  )
106
- args = [{'request_body' => {"hogekey" => "hogevalue"}}]
143
+ args = [{ 'request_body' => { 'hogekey' => 'hogevalue' } }]
107
144
  client.build_request('post', args)
108
145
  assert_equal('application/json', client.request['Content-Type'])
109
146
  assert_equal('{"hogekey":"hogevalue"}', client.request.body)
@@ -120,7 +157,7 @@ class TestClient < Minitest::Test
120
157
  )
121
158
  client.build_request('get', nil)
122
159
  assert_equal('application/json', client.request['Content-Type'])
123
- assert_equal(nil, client.request.body)
160
+ assert_nil(client.request.body)
124
161
  end
125
162
 
126
163
  def test_build_request_post_empty_body
@@ -134,7 +171,7 @@ class TestClient < Minitest::Test
134
171
  )
135
172
  client.build_request('post', nil)
136
173
  assert_equal('', client.request['Content-Type'])
137
- assert_equal(nil, client.request.body)
174
+ assert_nil(client.request.body)
138
175
  end
139
176
 
140
177
  def test_build_request_post_multipart
@@ -143,15 +180,71 @@ class TestClient < Minitest::Test
143
180
  }
144
181
  client = MockRequest.new(
145
182
  host: 'https://localhost',
146
- request_headers: headers,
183
+ request_headers: headers
147
184
  )
148
185
  name = 'post'
149
- args = [{'request_body' => 'hogebody'}]
186
+ args = [{ 'request_body' => 'hogebody' }]
150
187
  client.build_request(name, args)
151
188
  assert_equal('multipart/form-data; boundary=xYzZY', client.request['Content-Type'])
152
189
  assert_equal('hogebody', client.request.body)
153
190
  end
154
191
 
192
+ def test_json_body_encode_hash
193
+ headers = {
194
+ 'Content-Type' => 'application/json'
195
+ }
196
+ client = MockRequestWithRequestBody.new(
197
+ host: 'https://localhost',
198
+ request_headers: headers
199
+ )
200
+ name = 'post'
201
+ args = [{ 'request_body' => { 'this_is' => 'json' } }]
202
+ response = client.build_request(name, args)
203
+ assert_equal('{"this_is":"json"}', response.request_body)
204
+ end
205
+
206
+ def test_json_body_encode_array
207
+ headers = {
208
+ 'Content-Type' => 'application/json'
209
+ }
210
+ client = MockRequestWithRequestBody.new(
211
+ host: 'https://localhost',
212
+ request_headers: headers
213
+ )
214
+ name = 'post'
215
+ args = [{ 'request_body' => [{ 'this_is' => 'json' }] }]
216
+ response = client.build_request(name, args)
217
+ assert_equal('[{"this_is":"json"}]', response.request_body)
218
+ end
219
+
220
+ def test_json_body_do_not_reencode
221
+ headers = {
222
+ 'Content-Type' => 'application/json'
223
+ }
224
+ client = MockRequestWithRequestBody.new(
225
+ host: 'https://localhost',
226
+ request_headers: headers
227
+ )
228
+ name = 'post'
229
+ args = [{ 'request_body' => '{"this_is":"json"}' }]
230
+ response = client.build_request(name, args)
231
+ assert_equal('{"this_is":"json"}', response.request_body)
232
+ end
233
+
234
+ def test_json_body_do_not_reencode_simplejson
235
+ headers = {
236
+ 'Content-Type' => 'application/json'
237
+ }
238
+ client = MockRequestWithRequestBody.new(
239
+ host: 'https://localhost',
240
+ request_headers: headers
241
+ )
242
+ name = 'post'
243
+ args = [{ 'request_body' => 'true' }]
244
+ response = client.build_request(name, args)
245
+ assert_equal('true', response.request_body)
246
+ end
247
+
155
248
  def add_ssl
156
249
  uri = URI.parse('https://localhost:4010')
157
250
  http = Net::HTTP.new(uri.host, uri.port)
@@ -165,10 +258,143 @@ class TestClient < Minitest::Test
165
258
  assert_equal(['test'], url1.url_path)
166
259
  end
167
260
 
261
+ def test_ratelimit_core
262
+ expiry = Time.now.to_i + 1
263
+ rl = SendGrid::Response::Ratelimit.new(500, 100, expiry)
264
+ rl2 = SendGrid::Response::Ratelimit.new(500, 0, expiry)
265
+
266
+ refute rl.exceeded?
267
+ assert rl2.exceeded?
268
+
269
+ assert_equal(rl.used, 400)
270
+ assert_equal(rl2.used, 500)
271
+ end
272
+
273
+ def test_response_ratelimit_parsing
274
+ headers = {
275
+ 'X-RateLimit-Limit' => '500',
276
+ 'X-RateLimit-Remaining' => '300',
277
+ 'X-RateLimit-Reset' => Time.now.to_i.to_s
278
+ }
279
+
280
+ body = ''
281
+ code = 204
282
+ http_response = MockHttpResponse.new(code, body, headers)
283
+ response = SendGrid::Response.new(http_response)
284
+
285
+ refute_nil response.ratelimit
286
+ refute response.ratelimit.exceeded?
287
+ end
288
+
168
289
  def test_method_missing
169
290
  response = @client.get
170
291
  assert_equal(200, response.status_code)
171
- assert_equal({'message' => 'success'}, response.body)
172
- assert_equal({'headers' => 'test'}, response.headers)
292
+ assert_equal({ 'message' => 'success' }, response.body)
293
+ assert_equal({ 'headers' => 'test' }, response.headers)
294
+ end
295
+
296
+ def test_http_options
297
+ url1 = @client_with_options._('test')
298
+ assert_equal(@host, @client_with_options.host)
299
+ assert_equal(@headers, @client_with_options.request_headers)
300
+ assert_equal(['test'], url1.url_path)
301
+ end
302
+
303
+ def test_proxy_options
304
+ proxy_options = {
305
+ host: '127.0.0.1', port: 8080, user: 'anonymous', pass: 'secret'
306
+ }
307
+ client = MockRequest.new(
308
+ host: 'https://api.sendgrid.com',
309
+ request_headers: { 'Authorization' => 'Bearer xxx' },
310
+ proxy_options: proxy_options
311
+ ).version('v3').api_keys
312
+
313
+ assert(client.proxy_address, '127.0.0.1')
314
+ assert(client.proxy_pass, 'secret')
315
+ assert(client.proxy_port, 8080)
316
+ assert(client.proxy_user, 'anonymous')
317
+ end
318
+
319
+ def test_proxy_from_http_proxy_environment_variable
320
+ ENV['http_proxy'] = 'anonymous:secret@127.0.0.1:8080'
321
+
322
+ client = MockRequest.new(
323
+ host: 'https://api.sendgrid.com',
324
+ request_headers: { 'Authorization' => 'Bearer xxx' }
325
+ ).version('v3').api_keys
326
+
327
+ assert(client.proxy_address, '127.0.0.1')
328
+ assert(client.proxy_pass, 'secret')
329
+ assert(client.proxy_port, 8080)
330
+ assert(client.proxy_user, 'anonymous')
331
+ ensure
332
+ ENV.delete('http_proxy')
333
+ end
334
+
335
+ # def test_docker_exists
336
+ # assert(File.file?('./Dockerfile') || File.file?('./docker/Dockerfile'))
337
+ # end
338
+
339
+ # def test_docker_compose_exists
340
+ # assert(File.file?('./docker-compose.yml') || File.file?('./docker/docker-compose.yml'))
341
+ # end
342
+
343
+ def test_env_sample_exists
344
+ assert(File.file?('./.env_sample'))
345
+ end
346
+
347
+ def test_gitignore_exists
348
+ assert(File.file?('./.gitignore'))
349
+ end
350
+
351
+ def test_travis_exists
352
+ assert(File.file?('./.travis.yml'))
353
+ end
354
+
355
+ def test_codeclimate_exists
356
+ assert(File.file?('./.codeclimate.yml'))
357
+ end
358
+
359
+ def test_changelog_exists
360
+ assert(File.file?('./CHANGELOG.md'))
361
+ end
362
+
363
+ def test_code_of_conduct_exists
364
+ assert(File.file?('./CODE_OF_CONDUCT.md'))
365
+ end
366
+
367
+ def test_contributing_exists
368
+ assert(File.file?('./CONTRIBUTING.md'))
369
+ end
370
+
371
+ def test_issue_template_exists
372
+ assert(File.file?('./ISSUE_TEMPLATE.md'))
373
+ end
374
+
375
+ def test_license_exists
376
+ assert(File.file?('./LICENSE.md') || File.file?('./LICENSE.txt'))
377
+ end
378
+
379
+ def test_pull_request_template_exists
380
+ assert(File.file?('./PULL_REQUEST_TEMPLATE.md'))
381
+ end
382
+
383
+ def test_readme_exists
384
+ assert(File.file?('./README.md'))
385
+ end
386
+
387
+ def test_troubleshooting_exists
388
+ assert(File.file?('./TROUBLESHOOTING.md'))
389
+ end
390
+
391
+ def test_use_cases_exists
392
+ assert(File.file?('use_cases/README.md'))
393
+ end
394
+
395
+ def test_license_date_is_updated
396
+ license_end_year = IO.read('LICENSE.md').match(/Copyright \(C\) (\d{4}), Twilio SendGrid/)[1].to_i
397
+ current_year = Time.new.year
398
+ assert_equal(current_year, license_end_year)
173
399
  end
174
400
  end