test_webhook_handler.rb

  1# frozen_string_literal: true
  2
  3require "test_helper"
  4require_relative "../../sgx-bwmsgsv2"
  5require "rantly/minitest_extensions"
  6require_relative "generators/webhook"
  7require_relative "generators/message"
  8
  9def panic(e)
 10	$panic = e
 11end
 12
 13MMS_PROXY = "https://proxy.test.example.com/"
 14
 15class WebhookPropertyTest < Minitest::Test
 16	def setup
 17		reset_stanzas!
 18		reset_redis!
 19	end
 20
 21	def test_single_recipient_message_delivered_sends_one_receipt
 22		property_of {
 23			Webhook
 24				.new(REDIS)
 25				.type { "message-delivered" }
 26				.message { |registered, jid, dir, top_level_to|
 27					Message
 28						.new(REDIS)
 29						.to { [top_level_to] }
 30						.generate(registered, jid, dir)
 31				}
 32				.generate
 33		}.check { |metadata, example|
 34			result = invoke_webhook(example)
 35			assert_equal 200, result[0]
 36			assert_equal 1, written.length, "Should only send the delivery receipt"
 37			receipt = written.shift
 38			assert_equal(
 39				receipt.to,
 40				metadata["jid"],
 41				"Should send receipt to customer's jid"
 42			)
 43			assert_equal(
 44				receipt.from.to_s,
 45				"#{example["to"]}@#{ARGV[0]}",
 46				"Should send receipt from sender's Cheogram jid"
 47			)
 48		}
 49	end
 50	em :test_single_recipient_message_delivered_sends_one_receipt
 51
 52	def test_multi_recipient_outbound_sends_no_receipts
 53		property_of {
 54			Webhook
 55				.new(REDIS)
 56				.type { choose(*Webhook::OUTBOUND_TYPES) }
 57				.message { |registered, jid, dir, top_level_to|
 58					Message
 59						.new(REDIS)
 60						.to {
 61							array(integer(2)) { nanpa_phone } +
 62								[top_level_to] +
 63								array(range(1, 3)) { nanpa_phone }
 64						}
 65						.generate(registered, jid, dir)
 66				}
 67				.generate
 68		}.check { |metadata, example|
 69			result = invoke_webhook(example)
 70			assert_equal 200, result[0]
 71			assert_equal 0, written.length, "Should not group chat receipts"
 72		}
 73	end
 74	em :test_multi_recipient_outbound_sends_no_receipts
 75
 76	def test_single_recipient_message_failed_sends_one_error
 77		property_of {
 78			Webhook
 79				.new(REDIS)
 80				.type { "message-failed" }
 81				.message { |registered, jid, dir, top_level_to|
 82					Message
 83						.new(REDIS)
 84						.to { [top_level_to] }
 85						.generate(registered, jid, dir)
 86				}
 87				.generate
 88		}.check { |metadata, example|
 89			result = invoke_webhook(example)
 90			assert_equal 200, result[0]
 91			assert_equal(
 92				written.length,
 93				1,
 94				"Message failed should only send one notification"
 95			)
 96			assert_kind_of(
 97				::Blather::StanzaError,
 98				written.shift,
 99				"Should not notify message-failed"
100			)
101		}
102	end
103	em :test_single_recipient_message_failed_sends_one_error
104
105	def test_outbound_unregistered_returns_403
106		property_of {
107			Webhook
108				.new(REDIS)
109				.registered {
110					false
111				}
112				.type {
113					choose(*Webhook::OUTBOUND_TYPES)
114				}
115				.generate
116		}.check { |metadata, example|
117			result = invoke_webhook(example)
118			assert_equal [403, {}, "Customer not found\n"], result
119			assert_empty written
120			entries = REDIS.stream_entries("messages").sync
121			assert_empty entries
122		}
123	end
124	em :test_outbound_unregistered_returns_403
125
126	def test_unknown_outbound_returns_200
127		property_of {
128			Webhook
129				.new(REDIS)
130				.type { "unknown" }
131				.direction { "out" }
132				.generate
133		}.check { |metadata, example|
134			result = invoke_webhook(example)
135			assert_equal [200, {}, "OK"], result
136			assert_empty written
137			entries = REDIS.stream_entries("messages").sync
138			assert_empty entries
139		}
140	end
141	em :test_unknown_outbound_returns_200
142
143	def test_unknown_direction_returns_400_ok_except_when_message_failed
144		property_of {
145			Webhook
146				.new(REDIS)
147				.direction { "unknown" }
148				.type {
149					choose(*(Webhook::INBOUND_TYPES << Webhook::OUTBOUND_TYPES)
150							.reject { |ty|
151								ty == "message-failed"
152							}
153					)
154				}
155				.generate
156		}.check { |metadata, example|
157			result = invoke_webhook(example)
158			assert_equal [400, {}, "OK"], result
159			assert_empty written
160			entries = REDIS.stream_entries("messages").sync
161			assert_empty entries
162		}
163	end
164	em :test_unknown_direction_returns_400_ok_except_when_message_failed
165
166	def test_unknown_direction_returns_400_missing_params_when_message_failed
167		property_of {
168			Webhook
169				.new(REDIS)
170				.direction { "unknown" }
171				.type {
172					"message-failed"
173				}
174				.generate
175		}.check { |metadata, example|
176			result = invoke_webhook(example)
177			assert_equal [400, {}, "Missing params\n"], result
178			assert_empty written
179			entries = REDIS.stream_entries("messages").sync
180			assert_empty entries
181		}
182	end
183	em :test_unknown_direction_returns_400_missing_params_when_message_failed
184
185	def test_delivered_emits_correct_stream_event
186		property_of {
187			Webhook
188				.new(REDIS)
189				.type { "message-delivered" }
190				.generate
191		}.check { |metadata, example|
192			invoke_webhook(example)
193
194			entries = REDIS.stream_entries("messages").sync
195			assert_equal 1, entries.length
196
197			fields = entries.first[:fields]
198			expected_keys = %w[
199				event source timestamp stanza_id bandwidth_id
200			].sort
201			assert_equal expected_keys, fields.keys.sort
202
203			assert_equal "delivered", fields["event"]
204			assert_equal "bwmsgsv2", fields["source"]
205
206			assert_equal metadata["stanza_id"], fields["stanza_id"]
207			assert_equal example["message"]["id"], fields["bandwidth_id"]
208			assert_equal example["message"]["time"], fields["timestamp"]
209		}
210	end
211	em :test_delivered_emits_correct_stream_event
212
213	def test_failed_emits_correct_stream_event
214		property_of {
215			Webhook
216				.new(REDIS)
217				.type { "message-failed" }
218				.generate
219		}.check { |metadata, example|
220			invoke_webhook(example)
221
222			entries = REDIS.stream_entries("messages").sync
223			assert_equal 1, entries.length
224
225			fields = entries.first[:fields]
226			expected_keys = %w[
227				event source timestamp stanza_id bandwidth_id
228				error_code error_description
229			].sort
230			assert_equal expected_keys, fields.keys.sort
231
232			assert_equal "failed", fields["event"]
233			assert_equal "bwmsgsv2", fields["source"]
234
235			tag_parts = example["message"]["tag"].split(/ /, 2)
236			expected_stanza_id = WEBrick::HTTPUtils.unescape(tag_parts[0])
237			assert_equal expected_stanza_id, fields["stanza_id"]
238			assert_equal example["message"]["id"], fields["bandwidth_id"]
239			assert_equal example["message"]["time"], fields["timestamp"]
240			assert_equal example["message"]["errorCode"].to_s, fields["error_code"]
241			assert_equal example["message"]["description"].to_s, fields["error_description"]
242		}
243	end
244	em :test_failed_emits_correct_stream_event
245
246	def test_inbound_received_emits_correct_in_stream_event
247		property_of {
248			Webhook
249				.new(REDIS)
250				.message { |registered, jid, dir, top_level_to|
251					Message
252						.new(REDIS)
253						.to {
254							array(integer(2)) { nanpa_phone } +
255								[top_level_to] +
256								array(integer(2)) { nanpa_phone }
257						}
258						.generate(registered, jid, dir)
259				}
260				.type { "message-received" }
261				.generate
262		}.check { |metadata, example|
263			result = invoke_webhook(example)
264			assert_equal 200, result[0]
265
266			entries = REDIS.stream_entries("messages").sync
267			assert_equal 1, entries.length
268
269			fields = entries.first[:fields]
270			expected_keys = %w[
271				event source timestamp owner from to
272				bandwidth_id body media_urls
273			].sort
274			assert_equal expected_keys, fields.keys.sort
275
276			assert_equal "in", fields["event"]
277			assert_equal "bwmsgsv2", fields["source"]
278			assert_equal example["message"]["time"], fields["timestamp"]
279			assert_equal example["message"]["owner"], fields["owner"]
280			assert_equal example["message"]["from"], fields["from"]
281			assert_equal JSON.dump(example["message"]["to"]), fields["to"]
282			assert_equal example["message"]["id"], fields["bandwidth_id"]
283			assert_equal example["message"]["text"].to_s, fields["body"]
284
285			expected_media = Array(example["message"]["media"]).reject { |u|
286				u.end_with?(".smil", ".txt", ".xml")
287			}
288			assert_equal JSON.dump(expected_media), fields["media_urls"]
289		}
290	end
291	em :test_inbound_received_emits_correct_in_stream_event
292
293	def test_inbound_resend_emits_correct_resend_stream_event
294		property_of {
295			Webhook
296				.new(REDIS)
297				.type { "message-received" }
298				.message { |registered, jid, dir, top_level_to|
299					Message
300						.new(REDIS)
301						.to {
302							array(integer(2)) { nanpa_phone } +
303								[top_level_to] +
304								array(integer(2)) { nanpa_phone }
305						}
306						.generate(registered, jid, dir)
307				}
308				.generate
309		}.check { |metadata, example|
310			result = invoke_webhook(
311				example,
312				extra_env: { "HTTP_X_JMP_RESEND_OF" => metadata["resend_id"] }
313			)
314			assert_equal 200, result[0]
315
316			entries = REDIS.stream_entries("messages").sync
317			assert_equal 1, entries.length
318
319			fields = entries.first[:fields]
320			expected_keys = %w[
321				event source original_stream_id owner
322				original_bandwidth_id
323			].sort
324			assert_equal expected_keys, fields.keys.sort
325
326			assert_equal "resend", fields["event"]
327			assert_equal "bwmsgsv2", fields["source"]
328			assert_equal metadata["resend_id"], fields["original_stream_id"]
329			assert_equal example["message"]["owner"], fields["owner"]
330			assert_equal example["message"]["id"], fields["original_bandwidth_id"]
331		}
332	end
333	em :test_inbound_resend_emits_correct_resend_stream_event
334
335	def test_inbound_empty_text_no_media_returns_400
336		property_of {
337			Webhook
338				.new(REDIS)
339				.type { "message-received" }
340				.message { |registered, jid, dir, _|
341					Message
342						.new(REDIS)
343						.to { [nanpa_phone] }
344						.text { "" }
345						.media { nil }
346						.generate(registered, jid, dir)
347				}
348				.generate
349		}.check { |metadata, example|
350			result = invoke_webhook(example)
351			assert_equal [400, {}, "Missing params\n"], result
352			assert_empty written
353			entries = REDIS.stream_entries("messages").sync
354			assert_empty entries
355		}
356	end
357	em :test_inbound_empty_text_no_media_returns_400
358
359	def test_inbound_unknown_type_sends_notification
360		property_of {
361			Webhook
362				.new(REDIS)
363				.type { "unknown" }
364				.direction { "in" }
365				.message { |registered, jid, dir, top_level_to|
366					Message
367						.new(REDIS)
368						.to { [nanpa_phone] }
369						.generate(registered, jid, dir)
370				}
371				.generate
372		}.check { |metadata, example|
373			result = invoke_webhook(example)
374			assert_equal 200, result[0]
375			assert_equal 1, written.length
376
377			msg = written.shift
378			assert_kind_of Blather::Stanza::Message, msg
379			assert_equal(
380				metadata["jid"],
381				msg.to.to_s,
382				"Notification should be sent to customer's jid"
383			)
384
385			expected_from = example["message"]["from"]
386			unless expected_from.start_with?("+")
387				expected_from += ";phone-context=ca-us.phone-context.soprani.ca"
388			end
389			assert_equal(
390				"#{expected_from}@#{ARGV[0]}",
391				msg.from.to_s,
392				"Notification should be from sender's Cheogram jid"
393			)
394
395			expected_text = "unknown type (unknown)" \
396				" with text: #{example["message"]["text"]}"
397			assert_equal(
398				expected_text,
399				msg.body,
400				"Body should contain the unknown type and original text"
401			)
402
403			entries = REDIS.stream_entries("messages").sync
404			assert_empty entries, "Unknown inbound type should not emit a stream event"
405		}
406	end
407	em :test_inbound_unknown_type_sends_notification
408
409	def test_request_with_empty_params_produces_no_output
410		property_of {
411			Webhook.new(REDIS).generate
412		}.check { |metadata, example|
413			result = invoke_webhook(example, extra_env: { "params" => {} })
414			assert_equal [200, {}, "OK"], result
415			assert_empty written
416		}
417	end
418	em :test_request_with_empty_params_produces_no_output
419
420	def test_request_with_non_root_uri_produces_no_output
421		property_of {
422			Webhook.new(REDIS).generate
423		}.check { |metadata, example|
424			result = invoke_webhook(example, extra_env: { "REQUEST_URI" => "/wrong" })
425			assert_equal [200, {}, "OK"], result
426			assert_empty written
427		}
428	end
429	em :test_request_with_non_root_uri_produces_no_output
430
431	def test_request_with_non_post_method_produces_no_output
432		property_of {
433			Webhook.new(REDIS).generate
434		}.check { |metadata, example|
435			result = invoke_webhook(example, extra_env: { "REQUEST_METHOD" => "GET" })
436			assert_equal [200, {}, "OK"], result
437			assert_empty written
438		}
439	end
440	em :test_request_with_non_post_method_produces_no_output
441
442	def test_payload_without_message_or_type_returns_400
443		property_of {
444			metadata, example = Webhook.new(REDIS).generate
445			[choose("message", "type"), metadata, example]
446		}.check { |key, metadata, example|
447			example.delete(key)
448			result = invoke_webhook(example)
449			assert_equal [400, {}, "Missing params\n"], result
450			assert_empty written
451		}
452	end
453	em :test_payload_without_message_or_type_returns_400
454
455	def test_message_with_non_array_to_returns_400
456		property_of {
457			Webhook
458				.new(REDIS)
459				.message { |registered, jid, dir, top_level_to|
460					Message
461						.new(REDIS)
462						.to { top_level_to }
463						.owner { top_level_to }
464						.generate(registered, jid, dir)
465				}
466				.generate
467		}.check { |metadata, example|
468			result = invoke_webhook(example)
469			assert_equal [400, {}, "Missing params\n"], result
470			assert_empty written
471		}
472	end
473	em :test_message_with_non_array_to_returns_400
474
475	def test_message_with_empty_to_returns_400
476		property_of {
477			Webhook
478				.new(REDIS)
479				.message { |registered, jid, dir, top_level_to|
480					Message
481						.new(REDIS)
482						.to { [] }
483						.owner { top_level_to }
484						.generate(registered, jid, dir)
485				}
486				.generate
487		}.check { |metadata, example|
488			result = invoke_webhook(example)
489			assert_equal [400, {}, "Missing params\n"], result
490			assert_empty written
491		}
492	end
493	em :test_message_with_empty_to_returns_400
494
495	def test_inbound_nil_text_with_media_multi_recipient_writes_empty_body
496		property_of {
497			Webhook
498				.new(REDIS)
499				.type { "message-received" }
500				.message { |registered, jid, dir, top_level_to|
501					Message
502						.new(REDIS)
503						.to {
504							array(range(1, 3)) { nanpa_phone } +
505								[top_level_to] +
506								array(integer(2)) { nanpa_phone }
507						}
508						.text { nil }
509						.media { array(range(1, 3)) { media_url } }
510						.generate(registered, jid, dir)
511				}
512				.generate
513		}.check { |metadata, example|
514			result = invoke_webhook(example)
515			assert_equal 200, result[0]
516			assert_operator written.length, :>=, 1
517
518			msg = written.last
519			assert_kind_of Blather::Stanza::Message, msg
520			assert(
521				msg.body.to_s.empty?,
522				"Body should be nil/empty when text is nil"
523			)
524		}
525	end
526	em :test_inbound_nil_text_with_media_multi_recipient_writes_empty_body
527end