telnyx 0.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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +4 -0
  3. data/.github/ISSUE_TEMPLATE.md +5 -0
  4. data/.gitignore +9 -0
  5. data/.rubocop.yml +32 -0
  6. data/.rubocop_todo.yml +50 -0
  7. data/.travis.yml +42 -0
  8. data/CHANGELOG.md +2 -0
  9. data/CONTRIBUTORS +0 -0
  10. data/Gemfile +40 -0
  11. data/Guardfile +8 -0
  12. data/LICENSE +22 -0
  13. data/README.md +173 -0
  14. data/Rakefile +28 -0
  15. data/VERSION +1 -0
  16. data/bin/telnyx-console +16 -0
  17. data/lib/telnyx.rb +151 -0
  18. data/lib/telnyx/api_operations/create.rb +12 -0
  19. data/lib/telnyx/api_operations/delete.rb +13 -0
  20. data/lib/telnyx/api_operations/list.rb +29 -0
  21. data/lib/telnyx/api_operations/nested_resource.rb +63 -0
  22. data/lib/telnyx/api_operations/request.rb +57 -0
  23. data/lib/telnyx/api_operations/save.rb +103 -0
  24. data/lib/telnyx/api_resource.rb +69 -0
  25. data/lib/telnyx/available_phone_number.rb +9 -0
  26. data/lib/telnyx/errors.rb +166 -0
  27. data/lib/telnyx/event.rb +9 -0
  28. data/lib/telnyx/list_object.rb +155 -0
  29. data/lib/telnyx/message.rb +9 -0
  30. data/lib/telnyx/messaging_phone_number.rb +10 -0
  31. data/lib/telnyx/messaging_profile.rb +32 -0
  32. data/lib/telnyx/messaging_sender_id.rb +12 -0
  33. data/lib/telnyx/messaging_short_code.rb +10 -0
  34. data/lib/telnyx/number_order.rb +11 -0
  35. data/lib/telnyx/number_reservation.rb +11 -0
  36. data/lib/telnyx/public_key.rb +7 -0
  37. data/lib/telnyx/singleton_api_resource.rb +24 -0
  38. data/lib/telnyx/telnyx_client.rb +545 -0
  39. data/lib/telnyx/telnyx_object.rb +521 -0
  40. data/lib/telnyx/telnyx_response.rb +50 -0
  41. data/lib/telnyx/util.rb +328 -0
  42. data/lib/telnyx/version.rb +5 -0
  43. data/lib/telnyx/webhook.rb +66 -0
  44. data/telnyx.gemspec +25 -0
  45. data/test/api_stub_helpers.rb +1 -0
  46. data/test/openapi/README.md +9 -0
  47. data/test/telnyx/api_operations_test.rb +85 -0
  48. data/test/telnyx/api_resource_test.rb +293 -0
  49. data/test/telnyx/available_phone_number_test.rb +14 -0
  50. data/test/telnyx/errors_test.rb +23 -0
  51. data/test/telnyx/list_object_test.rb +244 -0
  52. data/test/telnyx/message_test.rb +19 -0
  53. data/test/telnyx/messaging_phone_number_test.rb +33 -0
  54. data/test/telnyx/messaging_profile_test.rb +70 -0
  55. data/test/telnyx/messaging_sender_id_test.rb +46 -0
  56. data/test/telnyx/messaging_short_code_test.rb +33 -0
  57. data/test/telnyx/number_order_test.rb +39 -0
  58. data/test/telnyx/number_reservation_test.rb +12 -0
  59. data/test/telnyx/public_key_test.rb +13 -0
  60. data/test/telnyx/telnyx_client_test.rb +631 -0
  61. data/test/telnyx/telnyx_object_test.rb +497 -0
  62. data/test/telnyx/telnyx_response_test.rb +49 -0
  63. data/test/telnyx/util_test.rb +380 -0
  64. data/test/telnyx/webhook_test.rb +108 -0
  65. data/test/telnyx_mock.rb +78 -0
  66. data/test/telnyx_test.rb +40 -0
  67. data/test/test_data.rb +149 -0
  68. data/test/test_helper.rb +73 -0
  69. metadata +162 -0
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require ::File.expand_path("../../test_helper", __FILE__)
4
+
5
+ module Telnyx
6
+ class TelnyxResponseTest < Test::Unit::TestCase
7
+ context ".from_faraday_hash" do
8
+ should "converts to TelnyxResponse" do
9
+ body = '{"foo": "bar"}'
10
+ headers = { "X-Request-Id" => "request-id" }
11
+
12
+ http_resp = {
13
+ body: body,
14
+ headers: headers,
15
+ status: 200,
16
+ }
17
+
18
+ resp = TelnyxResponse.from_faraday_hash(http_resp)
19
+
20
+ assert_equal JSON.parse(body, symbolize_names: true), resp.data
21
+ assert_equal body, resp.http_body
22
+ assert_equal headers, resp.http_headers
23
+ assert_equal 200, resp.http_status
24
+ assert_equal "request-id", resp.request_id
25
+ end
26
+ end
27
+
28
+ context ".from_faraday_response" do
29
+ should "converts to TelnyxResponse" do
30
+ body = '{"foo": "bar"}'
31
+ headers = { "X-Request-Id" => "request-id" }
32
+
33
+ env = Faraday::Env.from(
34
+ status: 200, body: body,
35
+ response_headers: headers
36
+ )
37
+ http_resp = Faraday::Response.new(env)
38
+
39
+ resp = TelnyxResponse.from_faraday_response(http_resp)
40
+
41
+ assert_equal JSON.parse(body, symbolize_names: true), resp.data
42
+ assert_equal body, resp.http_body
43
+ assert_equal headers, resp.http_headers
44
+ assert_equal 200, resp.http_status
45
+ assert_equal "request-id", resp.request_id
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,380 @@
1
+ # frozen_string_literal: true
2
+
3
+ require ::File.expand_path("../../test_helper", __FILE__)
4
+
5
+ module Telnyx
6
+ class UtilTest < Test::Unit::TestCase
7
+ context "OPTS_COPYABLE" do
8
+ should "include :apibase" do
9
+ assert_include Telnyx::Util::OPTS_COPYABLE, :api_base
10
+ end
11
+ end
12
+
13
+ context "OPTS_PERSISTABLE" do
14
+ should "include :client" do
15
+ assert_include Telnyx::Util::OPTS_PERSISTABLE, :client
16
+ end
17
+
18
+ should "not include :idempotency_key" do
19
+ refute_includes Telnyx::Util::OPTS_PERSISTABLE, :idempotency_key
20
+ end
21
+ end
22
+
23
+ should "#encode_parameters should prepare parameters for an HTTP request" do
24
+ params = {
25
+ a: 3,
26
+ b: "+foo?",
27
+ c: "bar&baz",
28
+ d: { a: "a", b: "b" },
29
+ e: [0, 1],
30
+ f: "",
31
+
32
+ # note the empty hash won't even show up in the request
33
+ g: [],
34
+ }
35
+ assert_equal(
36
+ "a=3&b=%2Bfoo%3F&c=bar%26baz&d[a]=a&d[b]=b&e[0]=0&e[1]=1&f=",
37
+ Telnyx::Util.encode_parameters(params)
38
+ )
39
+ end
40
+
41
+ should "#url_encode should prepare strings for HTTP" do
42
+ assert_equal "foo", Telnyx::Util.url_encode("foo")
43
+ assert_equal "foo", Telnyx::Util.url_encode(:foo)
44
+ assert_equal "foo%2B", Telnyx::Util.url_encode("foo+")
45
+ assert_equal "foo%26", Telnyx::Util.url_encode("foo&")
46
+ assert_equal "foo[bar]", Telnyx::Util.url_encode("foo[bar]")
47
+ end
48
+
49
+ should "#flatten_params should encode parameters according to Rails convention" do
50
+ params = [
51
+ [:a, 3],
52
+ [:b, "foo?"],
53
+ [:c, "bar&baz"],
54
+ [:d, { a: "a", b: "b" }],
55
+ [:e, [0, 1]],
56
+ [:f, [
57
+ { foo: "1", ghi: "2" },
58
+ { foo: "3", bar: "4" },
59
+ ],],
60
+ ]
61
+ assert_equal([
62
+ ["a", 3],
63
+ ["b", "foo?"],
64
+ ["c", "bar&baz"],
65
+ ["d[a]", "a"],
66
+ ["d[b]", "b"],
67
+ ["e[0]", 0],
68
+ ["e[1]", 1],
69
+
70
+ # *The key here is the order*. In order to be properly interpreted as
71
+ # an array of hashes on the server, everything from a single hash must
72
+ # come in at once. A duplicate key in an array triggers a new element.
73
+ ["f[0][foo]", "1"],
74
+ ["f[0][ghi]", "2"],
75
+ ["f[1][foo]", "3"],
76
+ ["f[1][bar]", "4"],
77
+ ], Telnyx::Util.flatten_params(params))
78
+ end
79
+
80
+ should "#symbolize_names should convert names to symbols" do
81
+ start = {
82
+ "foo" => "bar",
83
+ "array" => [{ "foo" => "bar" }],
84
+ "nested" => {
85
+ 1 => 2,
86
+ :symbol => 9,
87
+ "string" => nil,
88
+ },
89
+ }
90
+ finish = {
91
+ foo: "bar",
92
+ array: [{ foo: "bar" }],
93
+ nested: {
94
+ 1 => 2,
95
+ :symbol => 9,
96
+ :string => nil,
97
+ },
98
+ }
99
+
100
+ symbolized = Telnyx::Util.symbolize_names(start)
101
+ assert_equal(finish, symbolized)
102
+ end
103
+
104
+ should "#normalize_opts should reject nil keys" do
105
+ assert_raise { Telnyx::Util.normalize_opts(nil) }
106
+ assert_raise { Telnyx::Util.normalize_opts(api_key: nil) }
107
+ end
108
+
109
+ should "#convert_to_telnyx_object should pass through unknown types" do
110
+ obj = Util.convert_to_telnyx_object(7, {})
111
+ assert_equal 7, obj
112
+ end
113
+
114
+ should "#convert_to_telnyx_object should turn hashes into TelnyxObjects" do
115
+ obj = Util.convert_to_telnyx_object({ foo: "bar" }, {})
116
+ assert obj.is_a?(TelnyxObject)
117
+ assert_equal "bar", obj.foo
118
+ end
119
+
120
+ should "#convert_to_telnyx_object should marshal other classes" do
121
+ obj = Util.convert_to_telnyx_object({ record_type: "messaging_profile" }, {})
122
+ assert obj.is_a?(MessagingProfile)
123
+ end
124
+
125
+ should "#convert_to_telnyx_object should marshal arrays" do
126
+ obj = Util.convert_to_telnyx_object([1, 2, 3], {})
127
+ assert_equal [1, 2, 3], obj
128
+ end
129
+
130
+ context ".log_*" do
131
+ setup do
132
+ @old_log_level = Telnyx.log_level
133
+ Telnyx.log_level = nil
134
+
135
+ @old_stderr = $stderr
136
+ $stderr = StringIO.new
137
+
138
+ @old_stdout = $stdout
139
+ $stdout = StringIO.new
140
+ end
141
+
142
+ teardown do
143
+ Telnyx.log_level = @old_log_level
144
+ $stderr = @old_stderr
145
+ $stdout = @old_stdout
146
+ end
147
+
148
+ context ".log_debug" do
149
+ should "not log if logging is disabled" do
150
+ Util.log_debug("foo")
151
+ assert_equal "", $stdout.string
152
+ end
153
+
154
+ should "log if level set to debug" do
155
+ Telnyx.log_level = Telnyx::LEVEL_DEBUG
156
+ Util.log_debug("foo")
157
+ assert_equal "message=foo level=debug \n", $stdout.string
158
+ end
159
+
160
+ should "not log if level set to error" do
161
+ Telnyx.log_level = Telnyx::LEVEL_ERROR
162
+ Util.log_debug("foo")
163
+ assert_equal "", $stdout.string
164
+ end
165
+
166
+ should "not log if level set to info" do
167
+ Telnyx.log_level = Telnyx::LEVEL_INFO
168
+ Util.log_debug("foo")
169
+ assert_equal "", $stdout.string
170
+ end
171
+ end
172
+
173
+ context ".log_error" do
174
+ should "not log if logging is disabled" do
175
+ Util.log_error("foo")
176
+ assert_equal "", $stdout.string
177
+ end
178
+
179
+ should "log if level set to debug" do
180
+ Telnyx.log_level = Telnyx::LEVEL_DEBUG
181
+ Util.log_error("foo")
182
+ assert_equal "message=foo level=error \n", $stderr.string
183
+ end
184
+
185
+ should "log if level set to error" do
186
+ Telnyx.log_level = Telnyx::LEVEL_ERROR
187
+ Util.log_error("foo")
188
+ assert_equal "message=foo level=error \n", $stderr.string
189
+ end
190
+
191
+ should "log if level set to info" do
192
+ Telnyx.log_level = Telnyx::LEVEL_INFO
193
+ Util.log_error("foo")
194
+ assert_equal "message=foo level=error \n", $stderr.string
195
+ end
196
+ end
197
+
198
+ context ".log_info" do
199
+ should "not log if logging is disabled" do
200
+ Util.log_info("foo")
201
+ assert_equal "", $stdout.string
202
+ end
203
+
204
+ should "log if level set to debug" do
205
+ Telnyx.log_level = Telnyx::LEVEL_DEBUG
206
+ Util.log_info("foo")
207
+ assert_equal "message=foo level=info \n", $stdout.string
208
+ end
209
+
210
+ should "not log if level set to error" do
211
+ Telnyx.log_level = Telnyx::LEVEL_ERROR
212
+ Util.log_debug("foo")
213
+ assert_equal "", $stdout.string
214
+ end
215
+
216
+ should "log if level set to info" do
217
+ Telnyx.log_level = Telnyx::LEVEL_INFO
218
+ Util.log_info("foo")
219
+ assert_equal "message=foo level=info \n", $stdout.string
220
+ end
221
+ end
222
+ end
223
+
224
+ context ".log_* with a logger" do
225
+ setup do
226
+ @out = StringIO.new
227
+ logger = ::Logger.new(@out)
228
+
229
+ # Set a really simple formatter to make matching output as easy as
230
+ # possible.
231
+ logger.formatter = proc { |_severity, _datetime, _progname, message|
232
+ message
233
+ }
234
+
235
+ Telnyx.logger = logger
236
+ end
237
+
238
+ context ".log_debug" do
239
+ should "log to the logger" do
240
+ Util.log_debug("foo")
241
+ assert_equal "message=foo ", @out.string
242
+ end
243
+ end
244
+
245
+ context ".log_error" do
246
+ should "log to the logger" do
247
+ Util.log_error("foo")
248
+ assert_equal "message=foo ", @out.string
249
+ end
250
+ end
251
+
252
+ context ".log_info" do
253
+ should "log to the logger" do
254
+ Util.log_info("foo")
255
+ assert_equal "message=foo ", @out.string
256
+ end
257
+ end
258
+ end
259
+
260
+ context ".normalize_headers" do
261
+ should "normalize the format of a header key" do
262
+ assert_equal({ "Request-Id" => nil },
263
+ Util.normalize_headers("Request-Id" => nil))
264
+ assert_equal({ "Request-Id" => nil },
265
+ Util.normalize_headers("request-id" => nil))
266
+ assert_equal({ "Request-Id" => nil },
267
+ Util.normalize_headers("Request-ID" => nil))
268
+ assert_equal({ "Request-Id" => nil },
269
+ Util.normalize_headers(request_id: nil))
270
+ end
271
+
272
+ should "tolerate bad formatting" do
273
+ assert_equal({ "Request-Id" => nil },
274
+ Util.normalize_headers("-Request--Id-" => nil))
275
+ assert_equal({ "Request-Id" => nil },
276
+ Util.normalize_headers(request__id: nil))
277
+ end
278
+ end
279
+
280
+ #
281
+ # private
282
+ #
283
+ # I don't feel particularly good about using #send to invoke these, but I
284
+ # want them hidden from the public interface, and each method is far easier
285
+ # to test in isolation (as opposed to going through a public method).
286
+ #
287
+
288
+ context ".colorize" do
289
+ should "colorize for a TTY" do
290
+ assert_equal "\033[0;32;49mfoo\033[0m",
291
+ Util.send(:colorize, "foo", :green, true)
292
+ end
293
+
294
+ should "not colorize otherwise" do
295
+ assert_equal "foo", Util.send(:colorize, "foo", :green, false)
296
+ end
297
+ end
298
+
299
+ context ".level_name" do
300
+ should "convert levels to names" do
301
+ assert_equal "debug", Util.send(:level_name, LEVEL_DEBUG)
302
+ assert_equal "error", Util.send(:level_name, LEVEL_ERROR)
303
+ assert_equal "info", Util.send(:level_name, LEVEL_INFO)
304
+ end
305
+ end
306
+
307
+ context ".log_internal" do
308
+ should "log in a terminal friendly way" do
309
+ out = StringIO.new
310
+
311
+ # Sketchy as anything, but saves us from pulling in a mocking library.
312
+ # Open this instance of StringIO, and add a method override so that it
313
+ # looks like a TTY.
314
+ out.instance_eval do
315
+ def isatty
316
+ true
317
+ end
318
+ end
319
+
320
+ Util.send(:log_internal, "message", { foo: "bar" },
321
+ color: :green, level: Telnyx::LEVEL_DEBUG, logger: nil, out: out)
322
+ assert_equal "\e[0;32;49mDEBU\e[0m message \e[0;32;49mfoo\e[0m=bar\n",
323
+ out.string
324
+ end
325
+
326
+ should "log in a data friendly way" do
327
+ out = StringIO.new
328
+ Util.send(:log_internal, "message", { foo: "bar" },
329
+ color: :green, level: Telnyx::LEVEL_DEBUG, logger: nil, out: out)
330
+ assert_equal "message=message level=debug foo=bar\n",
331
+ out.string
332
+ end
333
+
334
+ should "log to a logger if set" do
335
+ out = StringIO.new
336
+ logger = ::Logger.new(out)
337
+
338
+ # Set a really simple formatter to make matching output as easy as
339
+ # possible.
340
+ logger.formatter = proc { |_severity, _datetime, _progname, message|
341
+ message
342
+ }
343
+
344
+ Util.send(:log_internal, "message", { foo: "bar" },
345
+ color: :green, level: Telnyx::LEVEL_DEBUG, logger: logger, out: $stdout)
346
+ assert_equal "message=message foo=bar",
347
+ out.string
348
+ end
349
+ end
350
+
351
+ context ".wrap_logfmt_value" do
352
+ should "pass through simple values" do
353
+ assert_equal "abc", Util.send(:wrap_logfmt_value, "abc")
354
+ assert_equal "123", Util.send(:wrap_logfmt_value, "123")
355
+ assert_equal "a-b_c/d", Util.send(:wrap_logfmt_value, "a-b_c/d")
356
+ end
357
+
358
+ should "pass through numerics" do
359
+ assert_equal 123, Util.send(:wrap_logfmt_value, 123)
360
+ assert_equal 1.23, Util.send(:wrap_logfmt_value, 1.23)
361
+ end
362
+
363
+ should "wrap more complex values in double quotes" do
364
+ assert_equal %("abc=123"), Util.send(:wrap_logfmt_value, %(abc=123))
365
+ end
366
+
367
+ should "escape double quotes already in the value" do
368
+ assert_equal %("abc=\\"123\\""), Util.send(:wrap_logfmt_value, %(abc="123"))
369
+ end
370
+
371
+ should "remove newlines" do
372
+ assert_equal %("abc"), Util.send(:wrap_logfmt_value, "a\nb\nc")
373
+ end
374
+
375
+ should "not error if given a non-string" do
376
+ assert_equal "true", Util.send(:wrap_logfmt_value, true)
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require ::File.expand_path("../../test_helper", __FILE__)
4
+
5
+ module Telnyx
6
+ class WebhookTest < Test::Unit::TestCase
7
+ EVENT_PAYLOAD = <<-PAYLOAD.freeze
8
+ {
9
+ "data": {
10
+ "record_type": "event",
11
+ "id": "0ccc7b54-4df3-4bca-a65a-3da1ecc777f0",
12
+ "event_type": "port_request.ported",
13
+ "created_at": "2018-02-02T22:25:27.521992Z",
14
+ "payload": {
15
+ "id": "5ccc7b54-4df3-4bca-a65a-3da1ecc777f0"
16
+ }
17
+ }
18
+ }
19
+ PAYLOAD
20
+
21
+ # rubocop:disable Metrics/LineLength
22
+ PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwy/jPkkgBo7oQermYujj\nAmSqN+aHNg+D4K85lKn6T3khJ8O2t/FrgN5qSGqg+0U5hoIHZflEon28lbLdf6gZ\njPeKQ2a24w5zroR6e4MM00RyJWA6MWXdo6Tn6xqKMYuT8LffEJGnXCH4yTIkxAVD\nyK0dfewhtrlpmW5ojXcDCrZ3Oo1o588PLNwSIuQwU7wHZwOLglWxFt6LZ9Ps8zYf\nQNH/pXNczf1E4rGZ1QxrzqFbndvjCE5VDRhULhycT/X0H2EMvNgHsDQk4OhENnzo\nCal3vO5+P9MgC7NSZCR8Ubebq0tanL5dj5GGYyjWmeq3QhfDLX2mTpIv/B0e8+hg\n8QIDAQAB\n-----END PUBLIC KEY-----\n".freeze
23
+
24
+ PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwy/jPkkgBo7oQermYujjAmSqN+aHNg+D4K85lKn6T3khJ8O2\nt/FrgN5qSGqg+0U5hoIHZflEon28lbLdf6gZjPeKQ2a24w5zroR6e4MM00RyJWA6\nMWXdo6Tn6xqKMYuT8LffEJGnXCH4yTIkxAVDyK0dfewhtrlpmW5ojXcDCrZ3Oo1o\n588PLNwSIuQwU7wHZwOLglWxFt6LZ9Ps8zYfQNH/pXNczf1E4rGZ1QxrzqFbndvj\nCE5VDRhULhycT/X0H2EMvNgHsDQk4OhENnzoCal3vO5+P9MgC7NSZCR8Ubebq0ta\nnL5dj5GGYyjWmeq3QhfDLX2mTpIv/B0e8+hg8QIDAQABAoIBAQCNwoP6wsVdvgD1\njxNQlu/41v/Bpc5h9xbC4sChNmqzubfY144nPlHjwKXUfoz4sag8Bsg0ybuNgGCt\nIME6a+5SsZ5boYgGlIJ0J4eFmQKBll6IwsDBC8jTh3thB1+C6GrEE+cQc5jnk0zL\nY33MWD6IyyJ2SD+cJEGLy+JnjB5LckGCQXWPQXwvpIKgGmFoLQzHCKfeKHZ3olB8\nC1+YKrQzLtyuuH9obDWxRSrqI5gOI/76PWmo+weNa4OrfFtBf5O9bo5OD17ilIT/\nuNpxb/7rOkpwU9x6D00/D/S7ecCdVoL2yBB5L635TNQKXxhvdSmBg1ceLlztwsUL\nOHIlglTZAoGBAOY3wyincm8iAUxLE+Z3AeTD94pjND4g2JXFF9E7UDxgRD6E3n38\nubNRdAMkxDmDYgyIOZsebykMadQ2vNiWqTjOBr6hxyQMFutHWrIJOU+peFCepn8u\nNX3Xg44l7KcwwqX5svoqgFl1FKwNpBOSo50oGX4lAgqtjYqEeMInfjZ7AoGBANkL\nz0wBNAr9oXsc0BN2WkQXB34RU/WcrymhxHfc+ZRzRShk9LOdBTuMYnj4rtjdG849\nJDDWlMk7UlzGjI07G5aT+n8Aq69BhV0IARC9PafTncE6G3sHswAQudHiurLflP9C\noj6kTakunrq8Kgj3Q1p6Ie+Hv7E01A3D0Difr4CDAoGAXORrLuBB4G3MMEirAvdK\nIFCidYiJ7/e47NXWQmq4eWQupTtfu15aX+yh7xLKypok2gGtnNWu7NVBbouXr507\nMtyPBCSrAfSO2uizw9rM8UPkdENP00mF8/0d7CGJV/zozafve9niaDZB3Rqz9eHZ\nevRPNQMhy8Uzs4y4XT8qQjkCgYBNuLjmkpe8R86Hc23fSkZQk56POk1CanUfB1p/\nQZXt3skpCd3GY7f39vFcOFEEP0kxtRs8kdp9pMx9hGvYNw5OAXd1+xt/iorjIXag\nM+PcMR8QjmpAyCUFJPglfHc2jnGgZpAKtnNI3fThEXhL9Z8cyxdT2tx97FjzBOeP\nHz+NWQKBgQCU0bSxTp2rbOCxHosQ/GDDTY0JkQ2z5q1SkibSiEnyAZ3yCHpXZRD7\nsa5BWs4qlasSKmxdmT9xgRDAL6CJH6kJizF3UIaIPOvPjIroOa7Mk1OFNbOi6Cao\n0LcWp5w1I2r5g7sOIRM/AcS3yVT5RJO4KB8WyDOvxCfP8cFsTacZmQ==\n-----END RSA PRIVATE KEY-----\n".freeze
25
+ # rubocop:enable Metrics/LineLength
26
+
27
+ def generate_signature(opts = {})
28
+ opts[:timestamp] ||= Time.now.to_i
29
+ opts[:payload] ||= EVENT_PAYLOAD
30
+ opts[:private_key] ||= PRIVATE_KEY
31
+
32
+ private_key = OpenSSL::PKey::RSA.new(opts[:private_key])
33
+ signature = private_key.sign(OpenSSL::Digest::SHA256.new, "#{opts[:timestamp]}|#{opts[:payload]}")
34
+ Base64.encode64(signature)
35
+ end
36
+
37
+ def setup
38
+ super
39
+ ENV["TELNYX_PUBLIC_KEY"] = PUBLIC_KEY
40
+ end
41
+
42
+ context ".construct_event" do
43
+ should "return an Event instance from a valid JSON payload and valid signature header" do
44
+ timestamp = Time.now.to_i
45
+ signature = generate_signature(timestamp: timestamp)
46
+ event = Telnyx::Webhook.construct_event(EVENT_PAYLOAD, signature, timestamp)
47
+ assert event.is_a?(Telnyx::Event)
48
+ end
49
+
50
+ should "raise a JSON::ParserError from an invalid JSON payload" do
51
+ assert_raises JSON::ParserError do
52
+ payload = "this is not valid JSON"
53
+ timestamp = Time.now.to_i
54
+ signature = generate_signature(payload: payload, timestamp: timestamp)
55
+ Telnyx::Webhook.construct_event(payload, signature, timestamp)
56
+ end
57
+ end
58
+
59
+ should "raise a SignatureVerificationError from a valid JSON payload and an invalid signature header" do
60
+ signature = "bad_signature"
61
+ timestamp = Time.now.to_i
62
+ assert_raises Telnyx::SignatureVerificationError do
63
+ Telnyx::Webhook.construct_event(EVENT_PAYLOAD, signature, timestamp)
64
+ end
65
+ end
66
+ end
67
+
68
+ context ".verify" do
69
+ should "raise a SignatureVerificationError when the signature does not have the expected format" do
70
+ signature = "FAKEFAKEFAKE"
71
+ e = assert_raises(Telnyx::SignatureVerificationError) do
72
+ Telnyx::Webhook::Signature.verify(EVENT_PAYLOAD, signature, Time.now.to_i)
73
+ end
74
+ assert_match("Signature is invalid and does not match the payload", e.message)
75
+ end
76
+
77
+ should "raise a SignatureVerificationError when there are no valid signatures for the payload" do
78
+ timestamp = Time.now.to_i
79
+ signature = generate_signature(payload: "foo", timestamp: timestamp)
80
+ e = assert_raises(Telnyx::SignatureVerificationError) do
81
+ Telnyx::Webhook::Signature.verify(EVENT_PAYLOAD, signature, timestamp)
82
+ end
83
+ assert_match("Signature is invalid and does not match the payload", e.message)
84
+ end
85
+
86
+ should "raise a SignatureVerificationError when the timestamp is not within the tolerance" do
87
+ timestamp = Time.now.to_i - 15
88
+ signature = generate_signature(timestamp: Time.now.to_i - 15)
89
+ e = assert_raises(Telnyx::SignatureVerificationError) do
90
+ Telnyx::Webhook::Signature.verify(EVENT_PAYLOAD, signature, timestamp, tolerance: 10)
91
+ end
92
+ assert_match("Timestamp outside the tolerance zone", e.message)
93
+ end
94
+
95
+ should "return true when the signature is valid and the timestamp is within the tolerance" do
96
+ timestamp = Time.now.to_i
97
+ signature = generate_signature
98
+ assert(Telnyx::Webhook::Signature.verify(EVENT_PAYLOAD, signature, timestamp, tolerance: 10))
99
+ end
100
+
101
+ should "return true when the signature is valid and the timestamp is off but no tolerance is provided" do
102
+ timestamp = 12_345
103
+ signature = generate_signature(timestamp: timestamp)
104
+ assert(Telnyx::Webhook::Signature.verify(EVENT_PAYLOAD, signature, timestamp))
105
+ end
106
+ end
107
+ end
108
+ end