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, {}, "Missing params\n"], 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						.text { message_body(nil_pct: 0, empty_pct: 0) }
259						.generate(registered, jid, dir)
260				}
261				.type { "message-received" }
262				.generate
263		}.check { |metadata, example|
264			result = invoke_webhook(example)
265			assert_equal 200, result[0]
266
267			entries = REDIS.stream_entries("messages").sync
268			assert_equal 1, entries.length
269
270			fields = entries.first[:fields]
271			expected_keys = %w[
272				event source timestamp owner from to
273				bandwidth_id body media_urls
274			].sort
275			assert_equal expected_keys, fields.keys.sort
276
277			assert_equal "in", fields["event"]
278			assert_equal "bwmsgsv2", fields["source"]
279			assert_equal example["message"]["time"], fields["timestamp"]
280			assert_equal example["message"]["owner"], fields["owner"]
281			assert_equal example["message"]["from"], fields["from"]
282			assert_equal JSON.dump(example["message"]["to"]), fields["to"]
283			assert_equal example["message"]["id"], fields["bandwidth_id"]
284			assert_equal example["message"]["text"].to_s, fields["body"]
285
286			expected_media = Array(example["message"]["media"]).reject { |u|
287				u.end_with?(".smil", ".txt", ".xml")
288			}
289			assert_equal JSON.dump(expected_media), fields["media_urls"]
290		}
291	end
292	em :test_inbound_received_emits_correct_in_stream_event
293
294	def test_inbound_resend_emits_correct_resend_stream_event
295		property_of {
296			Webhook
297				.new(REDIS)
298				.type { "message-received" }
299				.message { |registered, jid, dir, top_level_to|
300					Message
301						.new(REDIS)
302						.to {
303							array(integer(2)) { nanpa_phone } +
304								[top_level_to] +
305								array(integer(2)) { nanpa_phone }
306						}
307						.text { message_body(nil_pct: 0, empty_pct: 0) }
308						.generate(registered, jid, dir)
309				}
310				.generate
311		}.check { |metadata, example|
312			result = invoke_webhook(
313				example,
314				extra_env: { "HTTP_X_JMP_RESEND_OF" => metadata["resend_id"] }
315			)
316			assert_equal 200, result[0]
317
318			entries = REDIS.stream_entries("messages").sync
319			assert_equal 1, entries.length
320
321			fields = entries.first[:fields]
322			expected_keys = %w[
323				event source original_stream_id owner
324				original_bandwidth_id
325			].sort
326			assert_equal expected_keys, fields.keys.sort
327
328			assert_equal "resend", fields["event"]
329			assert_equal "bwmsgsv2", fields["source"]
330			assert_equal metadata["resend_id"], fields["original_stream_id"]
331			assert_equal example["message"]["owner"], fields["owner"]
332			assert_equal example["message"]["id"], fields["original_bandwidth_id"]
333		}
334	end
335	em :test_inbound_resend_emits_correct_resend_stream_event
336
337	def test_inbound_empty_or_nil_text_no_media_returns_400
338		property_of {
339			Webhook
340				.new(REDIS)
341				.type { "message-received" }
342				.message { |registered, jid, dir, _|
343					Message
344						.new(REDIS)
345						.to { [nanpa_phone] }
346						.text { choose(nil, "") }
347						.media { nil }
348						.generate(registered, jid, dir)
349				}
350				.generate
351		}.check { |metadata, example|
352			result = invoke_webhook(example)
353			assert_equal [400, {}, "Missing params\n"], result
354			assert_empty written
355			entries = REDIS.stream_entries("messages").sync
356			assert_empty entries
357		}
358	end
359	em :test_inbound_empty_or_nil_text_no_media_returns_400
360
361	def test_inbound_unknown_type_sends_notification
362		property_of {
363			Webhook
364				.new(REDIS)
365				.type { "unknown" }
366				.direction { "in" }
367				.message { |registered, jid, dir, top_level_to|
368					Message
369						.new(REDIS)
370						.to { [nanpa_phone] }
371						.generate(registered, jid, dir)
372				}
373				.generate
374		}.check { |metadata, example|
375			result = invoke_webhook(example)
376			assert_equal 200, result[0]
377			assert_equal 1, written.length
378
379			msg = written.shift
380			assert_kind_of Blather::Stanza::Message, msg
381			assert_equal(
382				metadata["jid"],
383				msg.to.to_s,
384				"Notification should be sent to customer's jid"
385			)
386
387			expected_from = example["message"]["from"]
388			unless expected_from.start_with?("+")
389				expected_from += ";phone-context=ca-us.phone-context.soprani.ca"
390			end
391			assert_equal(
392				"#{expected_from}@#{ARGV[0]}",
393				msg.from.to_s,
394				"Notification should be from sender's Cheogram jid"
395			)
396
397			expected_text = "unknown type (unknown)" \
398				" with text: #{example["message"]["text"]}"
399			assert_equal(
400				expected_text,
401				msg.body,
402				"Body should contain the unknown type and original text"
403			)
404
405			entries = REDIS.stream_entries("messages").sync
406			assert_empty entries, "Unknown inbound type should not emit a stream event"
407		}
408	end
409	em :test_inbound_unknown_type_sends_notification
410
411	def test_request_with_empty_params_produces_no_output
412		property_of {
413			Webhook.new(REDIS).generate
414		}.check { |metadata, example|
415			result = invoke_webhook(example, extra_env: { "params" => {} })
416			assert_equal [200, {}, "OK"], result
417			assert_empty written
418		}
419	end
420	em :test_request_with_empty_params_produces_no_output
421
422	def test_request_with_non_root_uri_produces_no_output
423		property_of {
424			Webhook.new(REDIS).generate
425		}.check { |metadata, example|
426			result = invoke_webhook(example, extra_env: { "REQUEST_URI" => "/wrong" })
427			assert_equal [200, {}, "OK"], result
428			assert_empty written
429		}
430	end
431	em :test_request_with_non_root_uri_produces_no_output
432
433	def test_request_with_non_post_method_produces_no_output
434		property_of {
435			Webhook.new(REDIS).generate
436		}.check { |metadata, example|
437			result = invoke_webhook(example, extra_env: { "REQUEST_METHOD" => "GET" })
438			assert_equal [200, {}, "OK"], result
439			assert_empty written
440		}
441	end
442	em :test_request_with_non_post_method_produces_no_output
443
444	def test_payload_without_message_or_type_returns_400
445		property_of {
446			metadata, example = Webhook.new(REDIS).generate
447			[choose("message", "type"), metadata, example]
448		}.check { |key, metadata, example|
449			example.delete(key)
450			result = invoke_webhook(example)
451			assert_equal [400, {}, "Missing params\n"], result
452			assert_empty written
453		}
454	end
455	em :test_payload_without_message_or_type_returns_400
456
457	def test_message_with_non_array_to_returns_400
458		property_of {
459			Webhook
460				.new(REDIS)
461				.message { |registered, jid, dir, top_level_to|
462					Message
463						.new(REDIS)
464						.to { top_level_to }
465						.owner { top_level_to }
466						.generate(registered, jid, dir)
467				}
468				.generate
469		}.check { |metadata, example|
470			result = invoke_webhook(example)
471			assert_equal [400, {}, "Missing params\n"], result
472			assert_empty written
473		}
474	end
475	em :test_message_with_non_array_to_returns_400
476
477	def test_message_with_empty_to_returns_400
478		property_of {
479			Webhook
480				.new(REDIS)
481				.message { |registered, jid, dir, top_level_to|
482					Message
483						.new(REDIS)
484						.to { [] }
485						.owner { top_level_to }
486						.generate(registered, jid, dir)
487				}
488				.generate
489		}.check { |metadata, example|
490			result = invoke_webhook(example)
491			assert_equal [400, {}, "Missing params\n"], result
492			assert_empty written
493		}
494	end
495	em :test_message_with_empty_to_returns_400
496
497	def test_inbound_nil_text_with_media_multi_recipient_writes_empty_body
498		property_of {
499			Webhook
500				.new(REDIS)
501				.type { "message-received" }
502				.message { |registered, jid, dir, top_level_to|
503					Message
504						.new(REDIS)
505						.to {
506							array(range(1, 3)) { nanpa_phone } +
507								[top_level_to] +
508								array(integer(2)) { nanpa_phone }
509						}
510						.text { nil }
511						.media { array(range(1, 3)) { media_url } }
512						.generate(registered, jid, dir)
513				}
514				.generate
515		}.check { |metadata, example|
516			result = invoke_webhook(example)
517			assert_equal 200, result[0]
518			assert_operator written.length, :>=, 1
519
520			msg = written.last
521			assert_kind_of Blather::Stanza::Message, msg
522			assert(
523				msg.body.to_s.empty?,
524				"Body should be nil/empty when text is nil"
525			)
526		}
527	end
528	em :test_inbound_nil_text_with_media_multi_recipient_writes_empty_body
529end