bitcoin-atm
bitcoin atm for pyc inc.
git clone https://9o.is/git/bitcoin-atm.git
OverlordSpec.scala
(15165B)
1 package inc.pyc.chimera
2
3 import akka.actor._
4 import akka.testkit._
5 import lycia._
6 import inc.pyc.bitcoin._
7 import inc.pyc.currency._
8 import inc.pyc.bill._
9 import acceptor._
10 import Commands._
11 import Events._
12 import States.Disconnected
13 import concurrent.duration._
14 import scala.language.reflectiveCalls
15
16 class OverlordSpec extends BaseSpec(MockConfig.config) with SpecHelper {
17
18 val comet = new MockComet(system)
19 comet.subcribe(
20 Topic.goto,
21 Topic.transactionUpdate,
22 Topic.redirectTo,
23 Topic.newPrice)
24
25 val network = TestProbe()
26 val wallet = TestProbe()
27 val ticker = TestProbe()
28 val watcher = TestProbe()
29 val minion = TestProbe()
30 val acceptor = TestProbe()
31 val overlord = TestFSMRef(
32 new MockOverlord(
33 acceptor.ref,
34 minion.ref,
35 watcher.ref,
36 wallet.ref,
37 ticker.ref,
38 network.ref))
39
40 // test data
41 val qr = QrCode("~qrdata~")
42 val address = BitcoinAddress("~valid~")
43 val auditData = AuditData(address, None)
44 val toSpend = LeftToSpend(500, 1000)
45 val price = Price("123.45")
46 val txCreated = CreatorTx(auditData, toSpend, price)
47 val userInfo = UserInfo("test@example.com", None, None, None, Nil)
48
49 val tx = IncompleteTx(
50 address.data,
51 123.45,
52 System.currency,
53 Nil,
54 toSpend.left,
55 auditData.userInfo)
56
57 val txFinal = CompleteTx(tx, "~txid~")
58
59
60 def state(s: State, d: Data = NullData)(func: => Unit) {
61 overlord.setState(s,d)
62 func
63 }
64
65 overlord ! Start
66
67 "Overlord" should {
68
69 "begin uninitialized" in {
70 overlord.stateName should be (Uninitialized)
71 overlord.stateData should be (NullData)
72 }
73
74 "subscribe to bill acceptor's state transitions when initializing" in {
75 acceptor.expectMsg(FSM.SubscribeTransitionCallBack(overlord))
76 }
77
78 "listen to bill acceptor's serial port when initializing" in {
79 acceptor.expectMsg(Listen)
80 }
81
82 "ping the network when initializing" in {
83 network.expectMsg("ping")
84 }
85
86 "set price ticker service provider when initializing" in {
87 ticker.expectMsg(ChangeBitcoinService(Lycia.servicePriceTicker))
88 }
89
90 "set bitcoin wallet service provider when initializing" in {
91 wallet.expectMsg(ChangeBitcoinService(Lycia.serviceWallet))
92 }
93
94 "set price ticker percentage over market value when initializing" in {
95 ticker.expectMsg(Lycia.percentProfit)
96 }
97
98 "tick the price ticker when initializing" in {
99 ticker.expectMsg(Tick)
100 }
101
102 "get the price per bitcoin when a gossip message is received" in {
103 overlord ! GossipPrice
104 ticker.expectMsgPF(){
105 case GetPrice => overlord ! Price("123.45")
106 }
107 comet.expectMsg("123.45")
108 }
109
110 "initialize bitcoin wallet balance when initializing" in {
111 wallet.expectMsg(GetBalance)
112 }
113
114 "be malfunctioning if initialization does not idle" in
115 state (Uninitialized) {
116 overlord ! FSM.StateTimeout
117 comet.expectMsg("/malfunction")
118 overlord.stateName should be (Malfunctioning)
119 overlord.stateData should be (Reason(Msg.unableToStart))
120 }
121
122 "warn about unknown message" in {
123 def test = warn ("unhandled") {
124 overlord ! "unknown"
125 }
126
127 test
128 overlord.setState(CashInsert)
129 test
130 overlord.setState(Idle)
131 test
132 }
133
134 "idle when bill acceptor is ready" in state (Uninitialized) {
135 overlord ! Ready
136 overlord.stateName should be (Idle)
137 overlord.stateData should be (NullData)
138 }
139
140 "uninhibit all bills when bill acceptor is ready" in {
141 acceptor.expectMsg(UnInhibit)
142 }
143
144 "be ready to scan qr code when session is started" in
145 state (Idle) {
146 overlord ! Start
147 overlord.stateName should be (QrScan)
148 overlord.stateData should be (NullData)
149 }
150
151 "go back to idling if qr code is not scanned" in state (QrScan) {
152 overlord.isStateTimerActive should be (true)
153 overlord ! FSM.StateTimeout
154 comet.expectMsg(Goto(Idle,""))
155 overlord.stateName should be (Idle)
156 overlord.stateData should be (NullData)
157 }
158
159 "be able to scan qr code and validate bitcoin address" in
160 state (QrScan) {
161 overlord ! qr
162 wallet.expectMsg(ValidateAddress(qr.qr))
163 overlord.stateName should be (QrValidate)
164 overlord.stateData should be (NullData)
165 }
166
167 "go back to scan qr code again if validation takes too long" in
168 state (QrValidate) {
169 overlord.isStateTimerActive should be (true)
170 overlord ! FSM.StateTimeout
171 comet.expectMsg(Goto(QrScan, Msg.unableValidateQr))
172 overlord.stateName should be (QrScan)
173 overlord.stateData should be (NullData)
174 }
175
176 "send minion to log user in when qr code is valid" in
177 state (QrValidate) {
178 overlord ! address
179 minion.expectMsg(address)
180 overlord.stateName should be (UserLogin)
181 overlord.stateData should be (address)
182 }
183
184 "go back to scan qr code if scanned code is invalid" in
185 state (QrValidate) {
186 overlord ! InvalidBitcoinAddress
187 comet.expectMsg(Goto(QrScan, Msg.invalidScannedQr))
188 overlord.stateName should be (QrScan)
189 overlord.stateData should be (NullData)
190 }
191
192 "go back to scan qr code again if logging user in takes too long" in
193 state (UserLogin, address) {
194 overlord.isStateTimerActive should be (true)
195 overlord ! FSM.StateTimeout
196 comet.expectMsg(Goto(QrScan, Msg.unableValidateQr))
197 overlord.stateName should be (QrScan)
198 overlord.stateData should be (NullData)
199 }
200
201 "send minion to audit user data if it exists" in
202 state (UserLogin, address) {
203 overlord ! UserData(None)
204 minion.expectMsg(auditData)
205 overlord.stateName should be (HistoryAudit)
206 overlord.stateData should be (auditData)
207 }
208
209 "go back to scan qr code again if auditting user takes too long" in
210 state (HistoryAudit, auditData) {
211 overlord.isStateTimerActive should be (true)
212 overlord ! FSM.StateTimeout
213 comet.expectMsg(Goto(QrScan, Msg.unableValidateQr))
214 overlord.stateName should be (QrScan)
215 overlord.stateData should be (NullData)
216 }
217
218 "go back to idle if user has reached bitcoin buying quota" in
219 state (HistoryAudit, auditData) {
220 overlord ! NothingToSpend(1000)
221 comet.expectMsg(Goto(Idle, Msg.buyLimitQuota format 1000))
222 overlord.stateName should be (Idle)
223 overlord.stateData should be (NullData)
224 }
225
226 "send minion to create a transaction if user has not passed quota" in
227 state (HistoryAudit, auditData) {
228 overlord ! toSpend
229 minion.expectMsg(txCreated)
230 overlord.stateName should be (TxCreate)
231 overlord.stateData should be (txCreated)
232 }
233
234 "go back to scan qr code again if creating transaction takes too long" in
235 state (TxCreate, txCreated) {
236 overlord.isStateTimerActive should be (true)
237 overlord ! FSM.StateTimeout
238 comet.expectMsg(Goto(QrScan, Msg.unableValidateQr))
239 overlord.stateName should be (QrScan)
240 overlord.stateData should be (NullData)
241 }
242
243 "be ready for user to insert cash when transaction is created" in
244 state (TxCreate, txCreated) {
245 overlord ! tx
246 acceptor.expectMsg(Inhibit)
247 comet.expectMsg(tx)
248 comet.expectMsg(Goto(CashInsert, ""))
249 overlord.stateName should be (CashInsert)
250 overlord.stateData should be (tx)
251 }
252
253 "go back to idling if there is no activity during cash insert state" in
254 state (CashInsert, tx) {
255 overlord.isStateTimerActive should be (true)
256 overlord ! FSM.StateTimeout
257 comet.expectMsg(Goto(Idle,""))
258 overlord.stateName should be (Idle)
259 overlord.stateData should be (NullData)
260 }
261
262 "send minion to check if inserted bill does not pass the quota" in
263 state (CashInsert, tx) {
264 overlord ! Inserted(USD(10))
265 minion.expectMsg((Inserted(USD(10)), tx))
266 overlord.stateName should be (CashValidate)
267 overlord.stateData should be (tx)
268 }
269
270 "be able to buy bitcoin after user is done inserting cash" in
271 state (CashInsert, tx) {
272 overlord ! Buy
273 acceptor.expectMsg(UnInhibit)
274 // TODO wallet should expect a message here
275 overlord.stateName should be (Sending)
276 overlord.stateData should be (tx)
277 }
278
279 "go back to inserting cash if cash validation takes too long" in
280 state (CashValidate, tx) {
281 overlord.isStateTimerActive should be (true)
282 overlord ! FSM.StateTimeout
283 acceptor.expectMsg(Return)
284 overlord.stateName should be (CashInsert)
285 overlord.stateData should be (tx)
286 }
287
288 "return bill if it passes the buying quota" in
289 state (CashValidate, tx) {
290 overlord ! InvalidBill
291 acceptor.expectMsg(Return)
292 overlord.stateName should be (CashInsert)
293 overlord.stateData should be (tx)
294 }
295
296 "stack bill if it does not pass the buying quota" in
297 state (CashValidate, tx) {
298 overlord ! BillOkay
299 acceptor.expectMsg(Stack)
300 overlord.stateName should be (CashValidate)
301 overlord.stateData should be (tx)
302 }
303
304 "wait until bill acceptor confirms the bill has been stacked" in
305 state (CashValidate, tx) {
306 val confirmation = Confirmed(USD(10))
307 overlord ! confirmation
308 minion.expectMsg((confirmation, tx))
309 overlord.stateName should be (CashValidate)
310 overlord.stateData should be (tx)
311 }
312
313 "be ready for next bill when updated transaction is saved to disk" in
314 state (CashValidate, tx) {
315 overlord ! CashSaved(tx)
316 comet.expectMsg(tx)
317 overlord.stateName should be (CashInsert)
318 overlord.stateData should be (tx)
319 }
320
321 "assume something is wrong and report error when sending bitcoins takes too long" in
322 state (Sending, tx) {
323 error ("Manual Assistance Required") {
324 overlord.isStateTimerActive should be (true)
325 overlord ! FSM.StateTimeout
326 comet.expectMsg(Goto(ErrorState, Msg.errorSending))
327 overlord.stateName should be (ErrorState)
328 overlord.stateData should be (tx)
329 }
330 }
331
332 "automatically send an email when sending bitcoins fails and user is logged in" in
333 state (Sending, tx.copy(userInfo = Some(userInfo))) {
334 overlord ! FSM.StateTimeout
335 // TODO test that minion gets email
336 comet.expectMsg(Goto(ErrorState, Msg.errorSending))
337 }
338
339 "receive finalized transaction when bitcoins are sent" in
340 state (Sending, tx) {
341 overlord ! txFinal
342 minion.expectMsg(("save", txFinal))
343 minion.expectMsg(("expenditure", txFinal))
344 minion.expectMsg(("transaction", txFinal))
345 comet.expectMsg(Goto(Receipt,""))
346 overlord.stateName should be (Receipt)
347 overlord.stateData should be (txFinal)
348 }
349
350 "idle if there is no activity in error state" in
351 state (ErrorState) {
352 overlord.isStateTimerActive should be (true)
353 overlord ! FSM.StateTimeout
354 comet.expectMsg(Goto(Idle,""))
355 overlord.stateName should be (Idle)
356 overlord.stateData should be (NullData)
357 }
358
359 "allow client to reset error state timer" in
360 state (ErrorState, tx) {
361 overlord ! Wait
362 overlord.stateName should be (ErrorState)
363 overlord.stateData should be (tx)
364 }
365
366 "be able to sms user to notify that we know about the error" in
367 state (ErrorState, tx) {
368 val number = "3051234567"
369 overlord ! Phone(number, true)
370 // TODO test that minion gets sms
371 overlord.stateName should be (Idle)
372 overlord.stateData should be (NullData)
373 }
374
375 "be able to email user to notify that we know about the error" in
376 state (ErrorState, tx) {
377 val address = "test@example.com"
378 overlord ! Email(address)
379 // TODO test that minion gets email
380 overlord.stateName should be (Idle)
381 overlord.stateData should be (NullData)
382 }
383
384 "allow client to reset receipt state timer" in
385 state (Receipt, txFinal) {
386 overlord ! Wait
387 overlord.stateName should be (Receipt)
388 overlord.stateData should be (txFinal)
389 }
390
391 "be able to email user to send receipt" in
392 state (Receipt, txFinal) {
393 val address = "test@example.com"
394 overlord ! Email(address)
395 // TODO test that minion gets email
396 // TODO test that email is saved
397 overlord.stateName should be (Idle)
398 overlord.stateData should be (NullData)
399 }
400
401 "be able to finalize transaction without sending a receipt" in
402 state (Receipt, txFinal) {
403 overlord ! Continue
404 overlord.stateName should be (Idle)
405 overlord.stateData should be (NullData)
406 }
407
408 "be malfunctioning when bill acceptor is disconnected" in
409 state (Idle, address) {
410 error ("Bill acceptor not operating") {
411 overlord ! Disconnected
412 overlord.isTimerActive("poll-acceptor") should be (true)
413 comet.expectMsg("/malfunction")
414 overlord.stateName should be (Malfunctioning)
415 overlord.stateData should be (Reason(Msg.hardwareMalfunction))
416 }
417 }
418
419 "be able to recover from malfunctioning bill acceptor" in
420 state (Malfunctioning) {
421 info ("Bill acceptor is operating again") {
422 overlord ! Listen
423 acceptor.expectMsg(Listen)
424 overlord ! Ready
425 overlord.isTimerActive("poll-acceptor") should be (false)
426 comet.expectMsg("/index")
427 overlord.stateName should be (Idle)
428 overlord.stateData should be (NullData)
429 }
430 }
431
432 "be malfunctioning when network is unreachable" in
433 state (Idle, auditData) {
434 error ("Network unreachable") {
435 overlord ! NetworkOutage
436 comet.expectMsg("/malfunction")
437 overlord.stateName should be (Malfunctioning)
438 overlord.stateData should be (Reason(Msg.networkUnreachable))
439 }
440 }
441
442 "be able to recover from unreachable network" in
443 state (Malfunctioning) {
444 info ("Network reestablished") {
445 overlord ! NetworkEstablished
446 comet.expectMsg("/index")
447 overlord.stateName should be (Idle)
448 overlord.stateData should be (NullData)
449 }
450 }
451
452 "forward bill acceptor's current state and transitions to state watcher" in {
453 val currentState = FSM.CurrentState(acceptor.ref, "mystate")
454 val transition = FSM.Transition(acceptor.ref, "oldstate", "mystate")
455 overlord ! currentState
456 watcher.expectMsg(currentState)
457 overlord ! transition
458 watcher.expectMsg(transition)
459 }
460 }
461 }