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