Detailed changes
@@ -158,6 +158,9 @@ MMS_MIME_TYPES = [
"video/x-flv"
]
+# 1 MB
+MAX_MEDIA_SIZE = 1000000
+
def panic(e)
if e.is_a?(Exception)
Sentry.capture_exception(e, hint: { background: false })
@@ -430,7 +433,7 @@ module SGXbwmsgsv2
EM::HttpRequest.new(URI.parse(un.text), tls: {verify_peer: true}).ahead.then { |http|
# If content is too large, or MIME type is not supported, place the link inside the body and do not send MMS.
- if http.response_header["CONTENT_LENGTH"].to_i > 3500000 ||
+ if http.response_header["CONTENT_LENGTH"].to_i > MAX_MEDIA_SIZE ||
!MMS_MIME_TYPES.include?(http.response_header["CONTENT_TYPE"])
unless body.include?(un.text)
s.body = body.empty? ? un.text : "#{body}\n#{un.text}"
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require "jennifer"
+require_relative "../rantly_extensions/data_extensions"
+
+class MessageStanza
+ # @param redis [FakeRedis]
+ def initialize(redis)
+ @redis = redis
+ end
+
+ include Jennifer.rant(self) { |component|
+ recv_nums(transient: true) {
+ array(range(1, 5)) {
+ choose(
+ value { nanpa_phone },
+ value { non_nanp_phone },
+ value { shortcode }
+ )
+ }
+ }
+ to derived_from(:recv_nums), transient: true do |recv_nums|
+ recv_nums.map { |num| "#{num}@#{component}" }
+ end
+ phone(transient: true) { nanpa_phone }
+ from(transient: true) { bare_jid }
+ media(transient: true) {
+ array(range(0, 5)) {
+ [media_url, range(0, MAX_MEDIA_SIZE << 1)]
+ }
+ }
+ registered?(transient: true) { true }
+ body(transient: true) { choose(nil, "", value { message_body }) }
+ id(transient: true) { SecureRandom.uuid }
+ subject { _utf8_body(range(1, 10)) }
+ stanza derived_from(:body, :to, :from, :subject, :media, :id, :recv_nums) { |body, to_jids, from, subject, media, id, recv_nums|
+ stanza_body = body
+ if media.any? && boolean
+ url = media.first.first
+ stanza_body = stanza_body.to_s.empty? ? url : "#{stanza_body}\n#{url}"
+ end
+
+ if recv_nums.length == 1
+ m = Blather::Stanza::Message.new(to_jids.first, stanza_body)
+ else
+ m = Blather::Stanza::Message.new(component, stanza_body)
+ addrs = Nokogiri::XML::Node.new('addresses', m.document)
+ addrs['xmlns'] = 'http://jabber.org/protocol/address'
+ recv_nums.each do |num|
+ addr = Nokogiri::XML::Node.new('address', m.document)
+ addr['type'] = 'to'
+ addr['uri'] = "sms:#{num}"
+ addrs.add_child(addr)
+ end
+ m.add_child(addrs)
+ end
+
+ m.from = from
+ m.id = id
+ m.subject = subject if subject
+
+ media.each do |url, _size|
+ x = Nokogiri::XML::Node.new('x', m.document)
+ ns = x.add_namespace(nil, 'jabber:x:oob')
+ url_node = Nokogiri::XML::Node.new('url', m.document)
+ url_node.namespace = ns
+ url_node.content = url
+ x.add_child(url_node)
+ m.add_child(x)
+ end
+
+ m
+ }
+ redis_state derived_from(:from, :phone, :registered?), transient: true do |from, phone, registered|
+ @redis.reset!
+ @redis.set("catapult_jid-", "HERE")
+ next unless registered
+
+ @redis.set("catapult_jid-#{phone}", from)
+ @redis.rpush("catapult_cred-#{from}", 'account', 'token', 'secret', phone)
+ end
+ http_stubs derived_from(:media, :registered?), transient: true do |media, registered|
+ WebMock.reset!
+ next [] unless registered
+
+ media.map do |url, size|
+ ext = File.extname(URI.parse(url).path)
+ content_type = freq(
+ [95, proc { Rack::Mime.mime_type(ext) }],
+ [5, proc { Rack::Mime.mime_type(".#{string(:alpha)}") }]
+ )
+ WebMock::API.stub_request(:head, url).to_return(
+ status: 200,
+ headers: {
+ "Content-Length" => size.to_s,
+ "Content-Type" => content_type
+ }
+ )
+ end
+ end
+ bandwidth_stub derived_from(:registered?, :media), transient: true do |registered, _media|
+ next unless registered
+
+ WebMock::API.stub_request(:post, BW_MESSAGES_URL).to_return(
+ status: 202,
+ body: JSON.dump(id: "bw-msg-#{SecureRandom.hex(4)}")
+ )
+ end
+ assertions derived_from(:http_stubs, :bandwidth_stub) { |stubs, bw|
+ a = stubs.map { |stub| -> { assert_requested(stub) } }
+ a << -> { assert_requested(bw) } if bw
+ a
+ }
+ written_state(transient: true) {
+ SGXbwmsgsv2.instance_variable_set(:@written, [])
+ }
+ }
+end
@@ -131,28 +131,34 @@ class Rantly
# @param ascii_only [Boolean, nil] truthy=force ASCII, nil=random
# @param nil_pct [Integer] weight (out of 100) for nil result
# @param empty_pct [Integer] weight (out of 100) for empty string result
+ # @param segments [Integer, nil] exact number of segments, overrides length generation
# @return [String, nil]
- def message_body(ascii_only: nil, nil_pct: 2, empty_pct: 2)
+ def message_body(ascii_only: nil, nil_pct: 2, empty_pct: 2, segments: nil)
text_pct = 100 - nil_pct - empty_pct
freq(
[nil_pct, proc { nil }],
[empty_pct, proc { "" }],
- [text_pct, proc { _message_body_text(ascii_only: ascii_only) }]
+ [text_pct, proc { _message_body_text(ascii_only: ascii_only, segments: segments) }]
)
end
# @param ascii_only [Boolean, nil]
+ # @param segments [Integer, nil]
# @return [String]
- def _message_body_text(ascii_only: nil)
+ def _message_body_text(ascii_only: nil, segments: nil)
use_ascii = ascii_only || boolean
segment_size = use_ascii ? SEGMENT_SIZE_GSM7 : SEGMENT_SIZE_UCS2
- threshold = segment_size * 3
- len = freq(
- [70, proc { range(1, threshold) }],
- [30, proc { range(threshold + 1, threshold + segment_size * 2) }]
- )
+ len = if segments
+ range(segment_size * (segments - 1) + 1, segment_size * segments)
+ else
+ threshold = segment_size * 3
+ freq(
+ [70, proc { range(1, threshold) }],
+ [30, proc { range(threshold + 1, threshold + segment_size * 2) }]
+ )
+ end
body = if use_ascii
sized(len) { string(:print) }
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "../../sgx-bwmsgsv2"
+require "rantly/minitest_extensions"
+require_relative "generators/stanza"
+
+class MaxMediaSizePropertyTest < Minitest::Test
+ def test_oversized_media_inlined_as_text
+ property_of {
+ MessageStanza.new(REDIS)
+ .recv_nums { [nanpa_phone] }
+ .media { [[deliverable_media_url, range(MAX_MEDIA_SIZE + 1, MAX_MEDIA_SIZE << 1)]] }
+ .body { message_body(segments: 1) }
+ .bandwidth_stub { |_registered, media|
+ (url, *), * = media
+ WebMock::API.stub_request(:post, BW_MESSAGES_URL).with(
+ body: WebMock::API.hash_including(text: /#{Regexp.escape(url)}/)
+ ).to_return(
+ status: 202, body: JSON.dump(id: "bw-msg-oversized")
+ )
+ }
+ .generate(ARGV[0])
+ }.check { |_metadata, example|
+ process_stanza(example["stanza"])
+ example["assertions"].each { |a| instance_exec(&a) }
+ }
+ end
+ em :test_oversized_media_inlined_as_text
+
+ def test_undersized_media_sent_as_mms
+ property_of {
+ MessageStanza.new(REDIS)
+ .recv_nums { [nanpa_phone] }
+ .media { [[deliverable_media_url, range(1, MAX_MEDIA_SIZE)]] }
+ .http_stubs { |media, _registered|
+ media.map do |u, s|
+ ext = File.extname(URI.parse(u).path)
+ WebMock::API.stub_request(:head, u).to_return(
+ status: 200,
+ headers: {
+ "Content-Length" => s.to_s,
+ "Content-Type" => Rack::Mime.mime_type(ext)
+ }
+ )
+ end
+ }
+ .bandwidth_stub { |_registered, media|
+ (url, *), * = media
+ WebMock::API.stub_request(:post, BW_MESSAGES_URL).with(
+ body: WebMock::API.hash_including(media: url)
+ ).to_return(
+ status: 202, body: JSON.dump(id: "bw-msg-undersized")
+ )
+ }
+ .generate(ARGV[0])
+ }.check { |_metadata, example|
+ process_stanza(example["stanza"])
+ example["assertions"].each { |a| instance_exec(&a) }
+ }
+ end
+ em :test_undersized_media_sent_as_mms
+end
@@ -138,7 +138,7 @@ def build_group_mms(body, dests, from: "test@example.com", id: nil)
m
end
-# @param body [String]
+# @param body [String, nil]
# @param dests [Array<String>]
# @param from [String]
# @param id [String, nil]
@@ -147,7 +147,8 @@ def build_outbound_message(body, dests, from: "test@example.com", id: nil)
if dests.length == 1 || dests.is_a?(String)
dests = [dests] if dests.is_a?(String)
- m = Blather::Stanza::Message.new("#{dests.first}@#{ARGV[0]}", body)
+ m = Blather::Stanza::Message.new("#{dests.first}@#{ARGV[0]}")
+ m.body = body if body
m.from = from
m.id = id if id
m