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.
- checksums.yaml +7 -0
- data/.gitattributes +4 -0
- data/.github/ISSUE_TEMPLATE.md +5 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +32 -0
- data/.rubocop_todo.yml +50 -0
- data/.travis.yml +42 -0
- data/CHANGELOG.md +2 -0
- data/CONTRIBUTORS +0 -0
- data/Gemfile +40 -0
- data/Guardfile +8 -0
- data/LICENSE +22 -0
- data/README.md +173 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/bin/telnyx-console +16 -0
- data/lib/telnyx.rb +151 -0
- data/lib/telnyx/api_operations/create.rb +12 -0
- data/lib/telnyx/api_operations/delete.rb +13 -0
- data/lib/telnyx/api_operations/list.rb +29 -0
- data/lib/telnyx/api_operations/nested_resource.rb +63 -0
- data/lib/telnyx/api_operations/request.rb +57 -0
- data/lib/telnyx/api_operations/save.rb +103 -0
- data/lib/telnyx/api_resource.rb +69 -0
- data/lib/telnyx/available_phone_number.rb +9 -0
- data/lib/telnyx/errors.rb +166 -0
- data/lib/telnyx/event.rb +9 -0
- data/lib/telnyx/list_object.rb +155 -0
- data/lib/telnyx/message.rb +9 -0
- data/lib/telnyx/messaging_phone_number.rb +10 -0
- data/lib/telnyx/messaging_profile.rb +32 -0
- data/lib/telnyx/messaging_sender_id.rb +12 -0
- data/lib/telnyx/messaging_short_code.rb +10 -0
- data/lib/telnyx/number_order.rb +11 -0
- data/lib/telnyx/number_reservation.rb +11 -0
- data/lib/telnyx/public_key.rb +7 -0
- data/lib/telnyx/singleton_api_resource.rb +24 -0
- data/lib/telnyx/telnyx_client.rb +545 -0
- data/lib/telnyx/telnyx_object.rb +521 -0
- data/lib/telnyx/telnyx_response.rb +50 -0
- data/lib/telnyx/util.rb +328 -0
- data/lib/telnyx/version.rb +5 -0
- data/lib/telnyx/webhook.rb +66 -0
- data/telnyx.gemspec +25 -0
- data/test/api_stub_helpers.rb +1 -0
- data/test/openapi/README.md +9 -0
- data/test/telnyx/api_operations_test.rb +85 -0
- data/test/telnyx/api_resource_test.rb +293 -0
- data/test/telnyx/available_phone_number_test.rb +14 -0
- data/test/telnyx/errors_test.rb +23 -0
- data/test/telnyx/list_object_test.rb +244 -0
- data/test/telnyx/message_test.rb +19 -0
- data/test/telnyx/messaging_phone_number_test.rb +33 -0
- data/test/telnyx/messaging_profile_test.rb +70 -0
- data/test/telnyx/messaging_sender_id_test.rb +46 -0
- data/test/telnyx/messaging_short_code_test.rb +33 -0
- data/test/telnyx/number_order_test.rb +39 -0
- data/test/telnyx/number_reservation_test.rb +12 -0
- data/test/telnyx/public_key_test.rb +13 -0
- data/test/telnyx/telnyx_client_test.rb +631 -0
- data/test/telnyx/telnyx_object_test.rb +497 -0
- data/test/telnyx/telnyx_response_test.rb +49 -0
- data/test/telnyx/util_test.rb +380 -0
- data/test/telnyx/webhook_test.rb +108 -0
- data/test/telnyx_mock.rb +78 -0
- data/test/telnyx_test.rb +40 -0
- data/test/test_data.rb +149 -0
- data/test/test_helper.rb +73 -0
- 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
|