test_helper.rb

  1# frozen_string_literal: true
  2
  3begin
  4	require "simplecov"
  5	SimpleCov.start do
  6		add_filter "/test/"
  7		enable_coverage :branch
  8	end
  9rescue LoadError
 10	nil
 11end
 12
 13require "em_promise"
 14require "fiber"
 15require "minitest/autorun"
 16require "rantly/minitest_extensions"
 17require "sentry-ruby"
 18require "webmock/minitest"
 19begin
 20	require "pry-rescue/minitest"
 21	require "pry-reload"
 22
 23	module Minitest
 24		class Test
 25			alias old_capture_exceptions capture_exceptions
 26			def capture_exceptions
 27				old_capture_exceptions do
 28					yield
 29				rescue Minitest::Skip => e
 30					failures << e
 31				end
 32			end
 33		end
 34	end
 35rescue LoadError
 36	# Just helpers for dev, no big deal if missing
 37	nil
 38end
 39
 40require "backend_sgx"
 41require "tel_selections"
 42
 43$VERBOSE = nil
 44Sentry.init
 45
 46def mksgx(customer_id="bogus", **kwargs)
 47	kwargs.delete(:sgx) || BackendSgx.new(
 48		jid: Blather::JID.new(CONFIG[:sgx]),
 49		creds: CONFIG[:creds],
 50		from_jid: ProxiedJID.proxy(
 51			"customer_#{customer_id}",
 52			CONFIG[:component][:jid]
 53		).__getobj__,
 54		ogm_url: NotLoaded.new("ogm_url"),
 55		fwd: NotLoaded.new("fwd"),
 56		transcription_enabled: NotLoaded.new("transcription_enabled"),
 57		registered?: NotLoaded.new("registered?"),
 58		**kwargs
 59	)
 60end
 61
 62def customer(
 63	customer_id="test",
 64	plan_name: nil,
 65	jid: Blather::JID.new("#{customer_id}@example.net"),
 66	expires_at: Time.now,
 67	auto_top_up_amount: 0,
 68	**kwargs
 69)
 70	Customer.extract(
 71		customer_id,
 72		jid,
 73		sgx: kwargs.delete(:sgx) || mksgx(customer_id),
 74		plan_name: plan_name,
 75		expires_at: expires_at,
 76		auto_top_up_amount: auto_top_up_amount,
 77		**kwargs
 78	)
 79end
 80
 81CONFIG = {
 82	sgx: "sgx",
 83	component: {
 84		jid: "component"
 85	},
 86	creds: {
 87		account: "test_bw_account",
 88		client_id: "test_bw_client_id",
 89		client_secret: "test_bw_client_secret",
 90		username: "test_bw_user",
 91		password: "test_bw_password"
 92	},
 93	sgx_creds: {
 94		route_value: {
 95			username: "test_sgx_user",
 96			password: "test_sgx_password",
 97			account: "test_sgx_account"
 98		}
 99	},
100	notify_from: "notify_from@example.org",
101	activation_amount: 1,
102	activation_amount_accept: 1,
103	plans: [
104		{
105			name: "test_usd",
106			currency: :USD,
107			monthly_price: 10000,
108			messages: :unlimited,
109			minutes: { included: 10440, price: 87 },
110			allow_register: true
111		},
112		{
113			name: "USD",
114			currency: :USD,
115			monthly_price: 10000,
116			messages: :unlimited,
117			minutes: { included: 10440, price: 87 },
118			allow_register: true
119		},
120		{
121			name: "test_bad_currency",
122			currency: :BAD
123		},
124		{
125			name: "test_usd_no_register",
126			currency: :USD,
127			monthly_price: 10000,
128			messages: :unlimited,
129			minutes: { included: 10440, price: 87 },
130			allow_register: false
131		},
132		{
133			name: "test_cad",
134			currency: :CAD,
135			monthly_price: 10000
136		},
137		{
138			name: "test_usd_old_billing",
139			currency: nil,
140			messages: :unlimited,
141			minutes: { included: 10440, price: 87 },
142			allow_register: false
143		}
144	],
145	braintree: {
146		merchant_accounts: {
147			USD: "merchant_usd"
148		}
149	},
150	sip: {
151		realm: "sip.example.com",
152		app: "sipappid"
153	},
154	xep0157: [
155		{ var: "support-addresses", value: "xmpp:tel@cheogram.com" }
156	],
157	credit_card_url: ->(*) { "http://creditcard.example.com?" },
158	electrum_notify_url: ->(*) { "http://notify.example.com" },
159	admin_notify: "admin_room@example.com",
160	sims: {
161		sim: {
162			USD: { price: 500, plan: "1GB" },
163			CAD: { price: 600, plan: "1GB" }
164		},
165		esim: {
166			USD: { price: 300, plan: "500MB" },
167			CAD: { price: 400, plan: "500MB" }
168		}
169	},
170	keep_area_codes: [
171		{ area_code: "556", premium_price: 10 },
172		{ area_code: "557", premium_price: nil }
173	],
174	keep_area_codes_in: {
175		account: "moveto",
176		site_id: "movetosite",
177		sip_peer_id: "movetopeer"
178	},
179	upstream_domain: "example.net",
180	approved_domains: {
181		"approved.example.com": nil,
182		"refer.example.com": "refer_to"
183	},
184	parented_domains: {
185		"parented.example.com" => {
186			customer_id: "1234",
187			plan_name: "test_usd"
188		}
189	},
190	offer_codes: {
191		pplus: "xmpp:pplus"
192	},
193	bandwidth_site: "test_site",
194	bandwidth_peer: "test_peer",
195	keepgo: { api_key: "keepgokey", access_token: "keepgotoken" },
196	snikket_hosting_api: "snikket.example.com",
197	onboarding_domain: "onboarding.example.com",
198	adr: "A Mailing Address",
199	interac: "interac@example.com",
200	support_link: ->(*) { "https://support.com" },
201	bulk_order_tokens: {
202		sometoken: { customer_id: "bulkcustomer", peer_id: "bulkpeer" },
203		lowtoken: { customer_id: "customerid_low", peer_id: "lowpeer" }
204	},
205	public_onboarding_url: "xmpp:example.com?register"
206}.freeze
207
208def panic(e)
209	raise e
210end
211
212LOG = Class.new {
213	def child(*)
214		Minitest::Mock.new
215	end
216
217	def debug(*); end
218
219	def info(*); end
220
221	def error(*); end
222}.new.freeze
223
224def log
225	LOG
226end
227
228BLATHER = Class.new {
229	def <<(*); end
230}.new.freeze
231
232def execute_command(
233	iq=Blather::Stanza::Iq::Command.new.tap { |i| i.from = "test@example.com" },
234	blather: BLATHER,
235	&blk
236)
237	Command::Execution.new(
238		Minitest::Mock.new,
239		blather,
240		:to_s.to_proc,
241		iq
242	).execute(&blk).sync
243end
244
245class NotLoaded < BasicObject
246	def inspect
247		"<NotLoaded #{@name}>"
248	end
249end
250
251class Matching
252	def initialize(&block)
253		@block = block
254	end
255
256	def ===(other)
257		@block.call(other)
258	end
259end
260
261class PromiseMock < Minitest::Mock
262	def then(succ=nil, _=nil)
263		if succ
264			succ.call(self)
265		else
266			yield self
267		end
268	end
269
270	def is_a?(_klass)
271		false
272	end
273end
274
275class FakeTelSelections
276	def initialize
277		@selections = {}
278	end
279
280	def set(jid, tel)
281		@selections[jid] = EMPromise.resolve(
282			TelSelections::HaveTel.new(tel.pending_value)
283		)
284	end
285
286	def delete(jid)
287		@selections.delete(jid)
288		EMPromise.resolve("OK")
289	end
290
291	def [](customer)
292		@selections.fetch(customer.jid) do
293			TelSelections::ChooseTel.new(
294				customer,
295				redis: FakeRedis.new,
296				db: FakeDB.new,
297				memcache: FakeMemcache.new
298			)
299		end
300	end
301end
302
303class FakeRedis
304	def initialize(values={})
305		@values = values
306	end
307
308	def set(key, value)
309		@values[key] = value
310		EMPromise.resolve("OK")
311	end
312
313	def setex(key, _expiry, value)
314		set(key, value)
315	end
316
317	def del(key)
318		@values.delete(key)
319		EMPromise.resolve("OK")
320	end
321
322	def mget(*keys)
323		EMPromise.all(keys.map(&method(:get)))
324	end
325
326	def get(key)
327		EMPromise.resolve(@values[key])
328	end
329
330	def getbit(key, bit)
331		get(key).then { |v| v.to_i.to_s(2)[bit].to_i }
332	end
333
334	def bitfield(key, *ops)
335		get(key).then do |v|
336			bits = v.to_i.to_s(2)
337			ops.each_slice(3).map do |(op, encoding, offset)|
338				raise "unsupported bitfield op" unless op == "GET"
339				raise "unsupported bitfield op" unless encoding == "u1"
340
341				bits[offset].to_i
342			end
343		end
344	end
345
346	def hget(key, field)
347		EMPromise.resolve(@values.dig(key, field))
348	end
349
350	def hexists(key, field)
351		hget(key, field).nil? ? 0 : 1
352	end
353
354	def hincrby(key, field, incrby)
355		@values[key] ||= {}
356		@values[key][field] ||= 0
357		@values[key][field] += incrby
358	end
359
360	def sadd(key, member)
361		@values[key] ||= Set.new
362		@values[key] << member
363	end
364
365	def srem(key, member)
366		@values[key]&.delete(member)
367	end
368
369	def scard(key)
370		@values[key]&.size || 0
371	end
372
373	def smembers(key)
374		@values[key]&.to_a || []
375	end
376
377	def sismember(key, value)
378		smembers(key).include?(value)
379	end
380
381	def expire(_, _); end
382
383	def exists(*keys)
384		EMPromise.resolve(
385			@values.select { |k, _| keys.include? k }.size
386		)
387	end
388
389	def lindex(key, index)
390		get(key).then { |v| v&.fetch(index) }
391	end
392
393	def incr(key)
394		get(key).then { |v|
395			n = v ? v + 1 : 0
396			set(key, n).then { n }
397		}
398	end
399
400	def reset!
401		@values = {}
402	end
403end
404
405class FakeDB
406	class MultiResult
407		def initialize(*args)
408			@results = args
409		end
410
411		def to_a
412			@results.shift
413		end
414	end
415
416	def initialize(items={})
417		@items = items
418	end
419
420	def transaction
421		yield
422	end
423
424	def exec(_, args)
425		@items.fetch(args, []).to_a
426	end
427
428	def query_defer(sql, args)
429		EMPromise.resolve(exec(sql, args))
430	end
431
432	def query_one(_, *args, field_names_as: :symbol, default: nil)
433		row = @items.fetch(args, []).to_a.first
434		row = row.transform_keys(&:to_sym) if row && field_names_as == :symbol
435		EMPromise.resolve(row || default)
436	end
437
438	def exec_defer(_, _)
439		EMPromise.resolve(nil)
440	end
441end
442
443class FakeMemcache
444	def initialize(data={})
445		@data = data
446	end
447
448	def set(k, v, _expires=nil)
449		raise "No spaces" if k =~ /\s/
450
451		@data[k] = v
452	end
453
454	def get(k)
455		yield @data[k]
456	end
457end
458
459class FakeLog
460	def initialize
461		@logs = []
462	end
463
464	def respond_to_missing?(*)
465		true
466	end
467
468	def method_missing(*args)
469		@logs << args
470	end
471end
472
473class FakeIBRRepo
474	def initialize(registrations={})
475		@registrations = registrations
476	end
477
478	def registered?(jid, from:)
479		@registrations.dig(jid.to_s, from.to_s) || false
480	end
481end
482
483class FakeTrustLevelRepo
484	def initialize(levels)
485		@levels = levels
486	end
487
488	def find(customer)
489		TrustLevel.for(customer: customer, manual: @levels[customer.customer_id])
490	end
491end
492
493module EventMachine
494	class << self
495		# Patch EM.add_timer to be instant in tests
496		alias old_add_timer add_timer
497		def add_timer(*args, &block)
498			args[0] = 0
499			old_add_timer(*args, &block)
500		end
501	end
502end
503
504module Minitest
505	class Test
506		def setup
507			oauth_body = {
508				access_token: "test_bw_oauth_token",
509				token_type: "Bearer",
510				expires_in: 3600
511			}.to_json
512
513			WebMock.stub_request(:post, "https://api.bandwidth.com/api/v1/oauth2/token")
514				.to_return(status: 200, body: oauth_body, headers: {
515					"Content-Type" => "application/json"
516				})
517			super
518		end
519
520		def self.property(m, &block)
521			define_method("test_#{m}") do
522				property_of(&block).check { |args| send(m, *args) }
523			end
524		end
525
526		def self.em(m)
527			alias_method "raw_#{m}", m
528			define_method(m) do
529				EM.run do
530					Fiber.new {
531						begin
532							send("raw_#{m}")
533						ensure
534							EM.stop
535						end
536					}.resume
537				end
538			end
539		end
540	end
541end
542
543require "lazy_object"
544require "porting_step_repo"
545
546class MockOutputs < PortingStepRepo::Outputs
547	def initialize(mock)
548		@mock = mock
549	end
550
551	def info(id, key, msg)
552		@mock.info(id, key, msg)
553	end
554
555	def warn(id, key, msg)
556		@mock.warn(id, key, msg)
557	end
558
559	def error(id, key, e_or_msg)
560		@mock.error(id, key, e_or_msg)
561	end
562
563	def to_customer(id, key, tel, msg)
564		@mock.to_customer(id, key, tel, msg)
565	end
566
567	def verify
568		@mock.verify
569	end
570end