test_pending_transaction_repo.rb

  1# frozen_string_literal: true
  2
  3require "pending_transaction_repo"
  4
  5class PendingTransactionRepo
  6	def setup_mocks
  7		@redis = Minitest::Mock.new
  8		@electrum = Minitest::Mock.new
  9	end
 10
 11	def override_filters(filters)
 12		@filters = filters
 13	end
 14end
 15
 16FakeElectrumTransaction = Struct.new(:tx_hash, :confirmations, :value) {
 17	def amount_for(_addr)
 18		value
 19	end
 20}
 21
 22class TestPendingTransactionRepo < Minitest::Test
 23	def test_empty_map
 24		repo = PendingTransactionRepo.new("key")
 25		repo.setup_mocks
 26		repo.redis.expect(
 27			:hgetall,
 28			[],
 29			["key"]
 30		)
 31		repo.map do |_pending, _customer_id|
 32			flunk "Shouldn't yield when empty"
 33		end
 34
 35		assert_mock repo.redis
 36		assert_mock repo.electrum
 37	end
 38
 39	def test_map
 40		repo = PendingTransactionRepo.new("key")
 41		repo.setup_mocks
 42		repo.override_filters([])
 43		repo.redis.expect(
 44			:hgetall,
 45			[["tx/addr", "1234"]],
 46			["key"]
 47		)
 48		repo.electrum.expect(
 49			:gettransaction,
 50			FakeElectrumTransaction.new("tx", 6, 0.5),
 51			["tx"]
 52		)
 53
 54		v = repo.map { |pending, customer_id|
 55			"#{pending.value} #{customer_id}"
 56		}
 57
 58		assert_equal ["0.5 1234"], v, "Should have returned result of block"
 59
 60		assert_mock repo.redis
 61		assert_mock repo.electrum
 62	end
 63
 64	def test_error_handler
 65		repo = PendingTransactionRepo.new("key")
 66		repo.setup_mocks
 67		repo.override_filters([])
 68		repo.redis.expect(
 69			:hgetall,
 70			[["tx/addr", "1234"], ["missing/addr", "1234"]],
 71			["key"]
 72		)
 73		def repo.electrum
 74			Class.new {
 75				def gettransaction(txid)
 76					if txid == "missing"
 77						raise Electrum::NoTransaction, "Couldn't find"
 78					end
 79
 80					FakeElectrumTransaction.new("tx", 6, 0.5)
 81				end
 82			}.new
 83		end
 84
 85		repo.error_handler do |e|
 86			case e
 87			when Electrum::NoTransaction
 88				true
 89			end
 90		end
 91
 92		v = repo.map { |pending, customer_id|
 93			"#{pending.value} #{customer_id}"
 94		}
 95
 96		assert_equal ["0.5 1234"], v, "Should have returned result of block"
 97
 98		assert_mock repo.redis
 99	end
100
101	def test_other_errors
102		repo = PendingTransactionRepo.new("key")
103		repo.setup_mocks
104		repo.override_filters([])
105		repo.redis.expect(
106			:hgetall,
107			[["tx/addr", "1234"], ["error/addr", "1234"]],
108			["key"]
109		)
110		def repo.electrum
111			Class.new {
112				def gettransaction(txid)
113					raise "Oh no" if txid == "error"
114
115					FakeElectrumTransaction.new("tx", 6, 0.5)
116				end
117			}.new
118		end
119
120		repo.error_handler do |e|
121			case e
122			when Electrum::NoTransaction
123				true
124			end
125		end
126
127		assert_raises(RuntimeError) do
128			repo.map { |pending, customer_id|
129				"#{pending.value} #{customer_id}"
130			}
131		end
132
133		assert_mock repo.redis
134	end
135
136	# This is basically the same as test_other_errors but uses the default
137	# error handler that should re-throw everything
138	def test_default_errors
139		repo = PendingTransactionRepo.new("key")
140		repo.setup_mocks
141		repo.override_filters([])
142		repo.redis.expect(
143			:hgetall,
144			[["tx/addr", "1234"], ["error/addr", "1234"]],
145			["key"]
146		)
147		def repo.electrum
148			Class.new {
149				def gettransaction(txid)
150					raise "Oh no" if txid == "error"
151
152					FakeElectrumTransaction.new("tx", 6, 0.5)
153				end
154			}.new
155		end
156
157		assert_raises(RuntimeError) do
158			repo.map { |pending, customer_id|
159				"#{pending.value} #{customer_id}"
160			}
161		end
162
163		assert_mock repo.redis
164	end
165
166	def test_remove_transaction
167		repo = PendingTransactionRepo.new("key")
168		repo.setup_mocks
169
170		pending = PendingTransactionRepo::PendingTransaction.new(
171			FakeElectrumTransaction.new("tx", 6, 0.5),
172			"addr"
173		)
174
175		repo.redis.expect(:hdel, nil, ["key", "tx/addr"])
176
177		repo.remove_transaction(pending)
178
179		assert_mock repo.redis
180		assert_mock repo.electrum
181	end
182
183	def test_mark_ignored
184		repo = PendingTransactionRepo.new("key", ignored_key: "ig")
185		repo.setup_mocks
186
187		pending = PendingTransactionRepo::PendingTransaction.new(
188			FakeElectrumTransaction.new("tx", 6, 0.5),
189			"addr"
190		)
191
192		repo.redis.expect(:sadd, nil, ["ig", "tx/addr"])
193
194		repo.mark_ignored(pending)
195
196		assert_mock repo.redis
197		assert_mock repo.electrum
198	end
199
200	def test_chunking
201		repo = PendingTransactionRepo.new("key")
202		repo.setup_mocks
203		mock_filter = Minitest::Mock.new
204		mock_filter.expect(
205			:filter_chunk, [["one/a", "1234"], ["two/a", "1234"]],
206			[[["one/a", "1234"], ["two/a", "1234"]]]
207		)
208		mock_filter.expect(
209			:filter_chunk, [["three/a", "1234"]],
210			[[["three/a", "1234"]]]
211		)
212		repo.override_filters([mock_filter])
213		repo.redis.expect(
214			:hgetall,
215			[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]],
216			["key"]
217		)
218		repo.electrum.expect(
219			:gettransaction,
220			FakeElectrumTransaction.new("one", 6, 0.5),
221			["one"]
222		)
223		repo.electrum.expect(
224			:gettransaction,
225			FakeElectrumTransaction.new("two", 6, 0.5),
226			["two"]
227		)
228		repo.electrum.expect(
229			:gettransaction,
230			FakeElectrumTransaction.new("three", 6, 0.5),
231			["three"]
232		)
233
234		v = repo.map(chunk_size: 2) { |pending, _customer_id|
235			pending.tx_hash
236		}
237
238		assert_equal(
239			["one", "two", "three"], v,
240			"Should have returned result of block"
241		)
242
243		assert_mock repo.redis
244		assert_mock repo.electrum
245		assert_mock mock_filter
246	end
247
248	def test_existing_transaction_filter
249		db_mock = Minitest::Mock.new
250		filter = PendingTransactionRepo::ExistingTransactionFilter.new(db_mock)
251
252		db_mock.expect(:escape_string, "one/a", ["one/a"])
253		db_mock.expect(:escape_string, "two/a", ["two/a"])
254		db_mock.expect(:escape_string, "three/b", ["three/b"])
255		# I've pretended this made a change here
256		db_mock.expect(:escape_string, "four/C", ["four/c"])
257
258		# I don't want to match the regex literally, that seems like a bit much
259		# so instead I've just matched the parameterized part
260		db_mock.expect(
261			:exec_params,
262			[
263				{ "transaction_id" => "one/a", "exists" => false },
264				{ "transaction_id" => "two/a", "exists" => true },
265				{ "transaction_id" => "three/b", "exists" => true },
266				{ "transaction_id" => "four/c", "exists" => false }
267			],
268			[/
269				\(VALUES
270				\s
271				\('one\/a'\),\('two\/a'\),
272				\('three\/b'\),\('four\/C'\)
273				\)
274			/x]
275		)
276
277		remaining = filter.filter_chunk([
278			["one/a", "1234"],
279			["two/a", "1234"],
280			["three/b", "4321"],
281			["four/c", "2323"]
282		])
283
284		assert_equal(
285			[["one/a", "1234"], ["four/c", "2323"]],
286			remaining,
287			"should only include unfiltered results"
288		)
289
290		assert_mock db_mock
291	end
292
293	def test_ignored_transaction_filter
294		redis = Object.new
295
296		def redis.pipelined
297			@stuff = []
298			yield
299			@stuff
300		end
301
302		def redis.sismember(key, txid)
303			raise unless key == "key"
304
305			@stuff << ["two/a", "three/b"].include?(txid)
306		end
307
308		filter = PendingTransactionRepo::IgnoredTransactionFilter.new(
309			redis, "key"
310		)
311
312		remaining = filter.filter_chunk([
313			["one/a", "1234"],
314			["two/a", "1234"],
315			["three/b", "4321"],
316			["four/c", "2323"]
317		])
318
319		assert_equal(
320			[["one/a", "1234"], ["four/c", "2323"]],
321			remaining,
322			"should only include unfiltered results"
323		)
324	end
325
326	def test_wrong_customer_filter
327		redis = Object.new
328
329		def redis.pipelined
330			@stuff = []
331			yield
332			@stuff
333		end
334
335		def redis.sismember(key, value)
336			store = {
337				"test_key_1234" => ["not_a"],
338				"test_key_4321" => ["b"],
339				"test_key_2323" => ["c"]
340			}
341			@stuff << store[key].include?(value)
342		end
343
344		filter = PendingTransactionRepo::WrongCustomerFilter.new(
345			redis, ->(customer_id) { "test_key_#{customer_id}" }
346		)
347
348		def filter.warn(s)
349			@warnings ||= []
350			@warnings << s
351		end
352
353		def filter.sneak_warnings
354			@warnings
355		end
356
357		remaining = filter.filter_chunk([
358			["one/a", "1234"],
359			["two/a", "1234"],
360			["three/b", "4321"],
361			["four/c", "2323"]
362		])
363
364		assert_equal(
365			[["three/b", "4321"], ["four/c", "2323"]],
366			remaining,
367			"should only include unfiltered results"
368		)
369
370		assert_equal(
371			[
372				"one/a doesn't match customer 1234",
373				"two/a doesn't match customer 1234"
374			],
375			filter.sneak_warnings,
376			"should have warned about busted results"
377		)
378	end
379
380	def test_filter_stack
381		repo = PendingTransactionRepo.new("key")
382		repo.setup_mocks
383		mock_filter_one = Minitest::Mock.new
384		mock_filter_one.expect(
385			:filter_chunk, [["one/a", "1234"], ["two/a", "1234"]],
386			[[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]]]
387		)
388
389		mock_filter_two = Minitest::Mock.new
390		mock_filter_two.expect(
391			:filter_chunk, [["one/a", "1234"]],
392			[[["one/a", "1234"], ["two/a", "1234"]]]
393		)
394
395		repo.override_filters([mock_filter_one, mock_filter_two])
396		repo.redis.expect(
397			:hgetall,
398			[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]],
399			["key"]
400		)
401		repo.redis.expect(
402			:hdel,
403			2,
404			["key", ["two/a", "three/a"]]
405		)
406		repo.electrum.expect(
407			:gettransaction,
408			FakeElectrumTransaction.new("one", 6, 0.5),
409			["one"]
410		)
411
412		v = repo.map { |pending, _customer_id|
413			pending.tx_hash
414		}
415
416		assert_equal(
417			["one"], v,
418			"Should have returned result of block"
419		)
420
421		assert_mock repo.redis
422		assert_mock repo.electrum
423		assert_mock mock_filter_one
424		assert_mock mock_filter_two
425	end
426
427	def test_filter_all
428		repo = PendingTransactionRepo.new("key")
429		repo.setup_mocks
430		mock_filter_one = Minitest::Mock.new
431		mock_filter_one.expect(
432			:filter_chunk, [],
433			[[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]]]
434		)
435
436		# Shouldn't be called at all
437		mock_filter_two = Minitest::Mock.new
438
439		repo.override_filters([mock_filter_one, mock_filter_two])
440		repo.redis.expect(
441			:hgetall,
442			[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]],
443			["key"]
444		)
445		repo.redis.expect(
446			:hdel,
447			3,
448			["key", ["one/a", "two/a", "three/a"]]
449		)
450
451		v = repo.map { |pending, _customer_id|
452			pending.tx_hash
453		}
454
455		assert_equal(
456			[], v,
457			"Should have returned result of block"
458		)
459
460		assert_mock repo.redis
461		assert_mock repo.electrum
462		assert_mock mock_filter_one
463		assert_mock mock_filter_two
464	end
465end