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			messages: :unlimited,
137			minutes: { included: 10440, price: 87 },
138			allow_register: true
139		},
140		{
141			name: "test_usd_old_billing",
142			currency: nil,
143			messages: :unlimited,
144			minutes: { included: 10440, price: 87 },
145			allow_register: false
146		}
147	],
148	braintree: {
149		merchant_accounts: {
150			USD: "merchant_usd"
151		}
152	},
153	sip: {
154		realm: "sip.example.com",
155		app: "sipappid"
156	},
157	xep0157: [
158		{ var: "support-addresses", value: "xmpp:tel@cheogram.com" }
159	],
160	credit_card_url: ->(*) { "http://creditcard.example.com?" },
161	electrum_notify_url: ->(*) { "http://notify.example.com" },
162	admin_notify: "admin_room@example.com",
163	sims: {
164		annual: {
165			USD: 100,
166			CAD: 120
167		},
168		per_gb: {
169			USD: 100,
170			CAD: 120
171		},
172		sim: {
173			USD: 500,
174			CAD: 600
175		},
176		esim: {
177			USD: 300,
178			CAD: 400
179		}
180	},
181	keep_area_codes: [
182		{ area_code: "556", premium_price: 10 },
183		{ area_code: "557", premium_price: nil }
184	],
185	keep_area_codes_in: {
186		account: "moveto",
187		site_id: "movetosite",
188		sip_peer_id: "movetopeer"
189	},
190	upstream_domain: "example.net",
191	approved_domains: {
192		"approved.example.com": nil,
193		"refer.example.com": "refer_to"
194	},
195	parented_domains: {
196		"parented.example.com" => {
197			customer_id: "1234",
198			plan_name: "test_usd"
199		}
200	},
201	offer_codes: {
202		pplus: "xmpp:pplus"
203	},
204	bandwidth_site: "test_site",
205	bandwidth_peer: "test_peer",
206	keepgo: { api_key: "keepgokey", access_token: "keepgotoken" },
207	snikket_hosting_api: "snikket.example.com",
208	onboarding_domain: "onboarding.example.com",
209	adr: "A Mailing Address",
210	interac: "interac@example.com",
211	support_link: ->(*) { "https://support.com" },
212	bulk_order_tokens: {
213		sometoken: { customer_id: "bulkcustomer", peer_id: "bulkpeer" },
214		lowtoken: { customer_id: "customerid_low", peer_id: "lowpeer" }
215	},
216	public_onboarding_url: "xmpp:example.com?register"
217}.freeze
218
219def panic(e)
220	raise e
221end
222
223LOG = Class.new {
224	def child(*)
225		Minitest::Mock.new
226	end
227
228	def debug(*); end
229
230	def info(*); end
231
232	def error(*); end
233}.new.freeze
234
235def log
236	LOG
237end
238
239BLATHER = Class.new {
240	def <<(*); end
241}.new.freeze
242
243def execute_command(
244	iq=Blather::Stanza::Iq::Command.new.tap { |i| i.from = "test@example.com" },
245	blather: BLATHER,
246	&blk
247)
248	Command::Execution.new(
249		Minitest::Mock.new,
250		blather,
251		:to_s.to_proc,
252		iq
253	).execute(&blk).sync
254end
255
256class NotLoaded < BasicObject
257	def inspect
258		"<NotLoaded #{@name}>"
259	end
260end
261
262class Matching
263	def initialize(&block)
264		@block = block
265	end
266
267	def ===(other)
268		@block.call(other)
269	end
270end
271
272class PromiseMock < Minitest::Mock
273	def then(succ=nil, _=nil)
274		if succ
275			succ.call(self)
276		else
277			yield self
278		end
279	end
280
281	def is_a?(_klass)
282		false
283	end
284end
285
286class FakeTelSelections
287	def initialize
288		@selections = {}
289	end
290
291	def set(jid, tel)
292		@selections[jid] = EMPromise.resolve(
293			TelSelections::HaveTel.new(tel.pending_value)
294		)
295	end
296
297	def delete(jid)
298		@selections.delete(jid)
299		EMPromise.resolve("OK")
300	end
301
302	def [](customer)
303		@selections.fetch(customer.jid) do
304			TelSelections::ChooseTel.new(
305				customer,
306				redis: FakeRedis.new,
307				db: FakeDB.new,
308				memcache: FakeMemcache.new
309			)
310		end
311	end
312end
313
314class FakeRedis
315	def initialize(values={})
316		@values = values
317	end
318
319	def set(key, value)
320		@values[key] = value
321		EMPromise.resolve("OK")
322	end
323
324	def setex(key, _expiry, value)
325		set(key, value)
326	end
327
328	def del(key)
329		@values.delete(key)
330		EMPromise.resolve("OK")
331	end
332
333	def mget(*keys)
334		EMPromise.all(keys.map(&method(:get)))
335	end
336
337	def get(key)
338		EMPromise.resolve(@values[key])
339	end
340
341	def getbit(key, bit)
342		get(key).then { |v| v.to_i.to_s(2)[bit].to_i }
343	end
344
345	def bitfield(key, *ops)
346		get(key).then do |v|
347			bits = v.to_i.to_s(2)
348			ops.each_slice(3).map do |(op, encoding, offset)|
349				raise "unsupported bitfield op" unless op == "GET"
350				raise "unsupported bitfield op" unless encoding == "u1"
351
352				bits[offset].to_i
353			end
354		end
355	end
356
357	def hget(key, field)
358		EMPromise.resolve(@values.dig(key, field))
359	end
360
361	def hexists(key, field)
362		hget(key, field).nil? ? 0 : 1
363	end
364
365	def hincrby(key, field, incrby)
366		@values[key] ||= {}
367		@values[key][field] ||= 0
368		@values[key][field] += incrby
369	end
370
371	def sadd(key, member)
372		@values[key] ||= Set.new
373		@values[key] << member
374	end
375
376	def srem(key, member)
377		@values[key]&.delete(member)
378	end
379
380	def scard(key)
381		@values[key]&.size || 0
382	end
383
384	def smembers(key)
385		@values[key]&.to_a || []
386	end
387
388	def sismember(key, value)
389		smembers(key).include?(value)
390	end
391
392	def expire(_, _); end
393
394	def exists(*keys)
395		EMPromise.resolve(
396			@values.select { |k, _| keys.include? k }.size
397		)
398	end
399
400	def lindex(key, index)
401		get(key).then { |v| v&.fetch(index) }
402	end
403
404	def incr(key)
405		get(key).then { |v|
406			n = v ? v + 1 : 0
407			set(key, n).then { n }
408		}
409	end
410
411	def reset!
412		@values = {}
413	end
414end
415
416class FakeDB
417	class MultiResult
418		def initialize(*args)
419			@results = args
420		end
421
422		def to_a
423			@results.shift
424		end
425	end
426
427	def initialize(items={})
428		@items = items
429	end
430
431	def transaction
432		yield
433	end
434
435	def exec(_, args)
436		@items.fetch(args, []).to_a
437	end
438
439	def query_defer(sql, args)
440		EMPromise.resolve(exec(sql, args))
441	end
442
443	def query_one(_, *args, field_names_as: :symbol, default: nil)
444		row = @items.fetch(args, []).to_a.first
445		row = row.transform_keys(&:to_sym) if row && field_names_as == :symbol
446		EMPromise.resolve(row || default)
447	end
448
449	def exec_defer(_, _)
450		EMPromise.resolve(nil)
451	end
452end
453
454class FakeMemcache
455	def initialize(data={})
456		@data = data
457	end
458
459	def set(k, v, _expires=nil)
460		raise "No spaces" if k =~ /\s/
461
462		@data[k] = v
463	end
464
465	def get(k)
466		yield @data[k]
467	end
468end
469
470class FakeLog
471	def initialize
472		@logs = []
473	end
474
475	def respond_to_missing?(*)
476		true
477	end
478
479	def method_missing(*args)
480		@logs << args
481	end
482end
483
484class FakeIBRRepo
485	def initialize(registrations={})
486		@registrations = registrations
487	end
488
489	def registered?(jid, from:)
490		@registrations.dig(jid.to_s, from.to_s) || false
491	end
492end
493
494class FakeTrustLevelRepo
495	def initialize(levels)
496		@levels = levels
497	end
498
499	def find(customer)
500		TrustLevel.for(customer: customer, manual: @levels[customer.customer_id])
501	end
502end
503
504module EventMachine
505	class << self
506		# Patch EM.add_timer to be instant in tests
507		alias old_add_timer add_timer
508		def add_timer(*args, &block)
509			args[0] = 0
510			old_add_timer(*args, &block)
511		end
512	end
513end
514
515module Minitest
516	class Test
517		def stub_bw_oauth_token
518			oauth_body = {
519				access_token: "test_bw_oauth_token",
520				token_type: "Bearer",
521				expires_in: 3600
522			}.to_json
523
524			WebMock.stub_request(
525				:post, "https://api.bandwidth.com/api/v1/oauth2/token"
526			).to_return(
527				status: 200, body: oauth_body,
528				headers: { "Content-Type" => "application/json" }
529			)
530		end
531
532		def setup
533			stub_bw_oauth_token
534			super
535		end
536
537		def self.property(m, &block)
538			define_method("test_#{m}") do
539				property_of(&block).check { |args| send(m, *args) }
540			end
541		end
542
543		def self.em(m)
544			alias_method "raw_#{m}", m
545			define_method(m) do
546				EM.run do
547					Fiber.new {
548						begin
549							send("raw_#{m}")
550						ensure
551							EM.stop
552						end
553					}.resume
554				end
555			end
556		end
557	end
558end
559
560require "lazy_object"
561require "porting_step_repo"
562
563class MockOutputs < PortingStepRepo::Outputs
564	def initialize(mock)
565		@mock = mock
566	end
567
568	def info(id, key, msg)
569		@mock.info(id, key, msg)
570	end
571
572	def warn(id, key, msg)
573		@mock.warn(id, key, msg)
574	end
575
576	def error(id, key, e_or_msg)
577		@mock.error(id, key, e_or_msg)
578	end
579
580	def to_customer(id, key, tel, msg)
581		@mock.to_customer(id, key, tel, msg)
582	end
583
584	def verify
585		@mock.verify
586	end
587end