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