backport: lower max media size to 1MB

Phillip Davis created

Change summary

sgx-bwmsgsv2.rb                                    |   5 
test/property/generators/stanza.rb                 | 118 ++++++++++++++++
test/property/rantly_extensions/data_extensions.rb |  22 +-
test/property/test_max_media_size.rb               |  63 ++++++++
test/test_helper.rb                                |   5 
5 files changed, 202 insertions(+), 11 deletions(-)

Detailed changes

sgx-bwmsgsv2.rb 🔗

@@ -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}"

test/property/generators/stanza.rb 🔗

@@ -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

test/property/rantly_extensions/data_extensions.rb 🔗

@@ -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) }

test/property/test_max_media_size.rb 🔗

@@ -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

test/test_helper.rb 🔗

@@ -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