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 }