bitcoin-atm

bitcoin atm for pyc inc.

git clone https://9o.is/git/bitcoin-atm.git

MinionSpec.scala

(14169B)


      1 package inc.pyc.chimera
      2 
      3 import model._
      4 import minions._
      5 import akka.actor._
      6 import akka.testkit._
      7 import inc.pyc.currency._
      8 import inc.pyc.bitcoin._
      9 import inc.pyc.bill.acceptor._
     10 import Events._
     11 import awscala.dynamodbv2._
     12 import net.liftweb._
     13 import util._
     14 import mapper._
     15 import common.Empty
     16 import concurrent.duration._
     17 import org.joda.time._
     18 
     19 class MinionSpec extends BaseSpec(MockConfig.config) with SpecHelper {
     20   
     21   implicit val dynamoDB = DynamoDB.local()
     22   val expenditure = "Expenditure"
     23   val transaction = "Transaction"
     24 
     25   val vendor = new StandardDBVendor(
     26       "org.h2.Driver",
     27       "jdbc:h2:lift_proto.db;AUTO_SERVER=TRUE",
     28       Empty, Empty)
     29 
     30   def initializeLocalDB = {
     31     DB.defineConnectionManager(util.DefaultConnectionIdentifier, vendor)
     32     Schemifier.schemify(true, Schemifier.infoF _, CompletedTransaction)
     33     Schemifier.schemify(true, Schemifier.infoF _, IncompleteTransaction)
     34   }
     35   
     36   def shutdownLocalDB = {
     37     vendor.closeAllConnections_!
     38   }
     39   
     40   def testGeneral (f: TestActorRef[GeneralMinion] => Unit) {
     41     f(TestActorRef[GeneralMinion])
     42   }
     43     
     44   def testLocalDb (f: TestActorRef[LocalDBMinion] => Unit) {
     45     f(TestActorRef[LocalDBMinion])
     46   }
     47   
     48   def testExpenditure (f: TestActorRef[MockExpenditureMinion] => Unit) {
     49     f(TestActorRef[MockExpenditureMinion])
     50   }
     51   
     52   def testTransaction (f: TestActorRef[MockTransactionMinion] => Unit) {
     53     f(TestActorRef[MockTransactionMinion])
     54   }
     55   
     56   override def beforeAll = {
     57 
     58     val createdExpTableMeta: TableMeta = dynamoDB.createTable(
     59       name = expenditure,
     60       hashPK = "address" -> AttributeType.String,
     61       rangePK = "date" -> AttributeType.String,
     62       otherAttributes = Seq(),
     63       indexes = Seq())
     64 
     65     val createdTxTableMeta: TableMeta = dynamoDB.createTable(
     66       name = transaction,
     67       hashPK = "txid" -> AttributeType.String,
     68       rangePK = "date" -> AttributeType.String,
     69       otherAttributes = Seq(),
     70       indexes = Seq())
     71           
     72     var isExpActivated, isTxActivated = false
     73       while (!isExpActivated && !isTxActivated) {
     74         dynamoDB.describe(createdExpTableMeta.table).map { meta =>
     75           isExpActivated = meta.status.toString == "ACTIVE"
     76         }
     77         dynamoDB.describe(createdTxTableMeta.table).map { meta =>
     78           isTxActivated = meta.status.toString == "ACTIVE"
     79         }
     80         Thread.sleep(1000L)
     81         print(".")
     82       }
     83     
     84     initializeLocalDB
     85     super.beforeAll
     86   }
     87   
     88   override def afterAll = {
     89     IncompleteTransaction.findAll.foreach(_.delete_!)
     90     CompletedTransaction.findAll.foreach(_.delete_!)
     91     shutdownLocalDB
     92     dynamoDB.table(expenditure).get.destroy
     93     dynamoDB.table(transaction).get.destroy
     94     super.afterAll
     95   }
     96   
     97   "Minion" should {
     98     
     99     /*
    100     "log user in" in test {
    101       minion =>
    102         // TODO how to mock http rest interface
    103     }
    104     * 
    105     */
    106     
    107     "calculate buy limit in the past 24 hours" in {
    108         val address = BitcoinAddress("~address18~")
    109         val userinfo = UserInfo("test@example.com", 
    110             purchaseLimit = Some(3000)) // cannot pass $3,000
    111         val audit = AuditData(address, Some(userinfo))
    112         
    113         def tx(bills: List[Int]) = CompleteTx(IncompleteTx(
    114           audit.address.data,
    115           123.45,
    116           System.currency,
    117           bills,
    118           0, // ignored
    119           audit.userInfo), "~txid~") // txid ignored
    120 
    121         val table = dynamoDB.table(expenditure).get
    122         
    123         def save(tx: CompleteTx) =
    124           table.put(tx.address, tx.dateISO8601, "txid" -> tx.txid, 
    125               "paid" -> tx.bills.sum , "currency" -> tx.currency.toString)
    126               
    127              
    128         // transaction over the purchase limit 
    129         // seconds later from 24 hours ago, so tests should still pass
    130         val hours24ago = new DateTime(DateTimeZone.UTC).minusDays(1)
    131         save(tx(List(3001)).copy(date = hours24ago))
    132               
    133         // spent $1,000 -- left $2,000 -- limit is $3,000
    134         save(tx(List(100,100,100,100,100,100,100,100,100,100)))
    135         val minion1 = TestActorRef[MockExpenditureMinion]
    136         terminates (minion1) (minion1 ! audit)
    137         expectMsg(LeftToSpend(2000, 3000))
    138         
    139         // spent $2,000 -- left $1,000 -- limit is $3,000
    140         save(tx(List(100,100,100,100,100,100,100,100,100,100)))
    141         val minion2 = TestActorRef[MockExpenditureMinion]
    142         terminates(minion2) (minion2 ! audit)
    143         expectMsg(LeftToSpend(1000, 3000))
    144         
    145         // spent $950 -- left $50 -- limit is $3,000
    146         save(tx(List(100,100,100,100,100,100,100,100,100,50)))
    147         val minion3 = TestActorRef[MockExpenditureMinion]
    148         terminates(minion3) (minion3 ! audit)
    149         expectMsg(LeftToSpend(50, 3000))
    150         
    151         // spent $50 -- left $0 -- limit is $3,000
    152         save(tx(List(50)))
    153         val minion4 = TestActorRef[MockExpenditureMinion]
    154         terminates(minion4) (minion4 ! audit)
    155         expectMsg(NothingToSpend(3000))
    156     }
    157     
    158     "create a new transaction" in testLocalDb {
    159       minion =>
    160         val address     = BitcoinAddress("~address~")
    161         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    162         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    163         val price       = ticker.Price("123.45")
    164         val audit       = AuditData(address, Some(userinfo))
    165         val creator     = CreatorTx(audit, toSpend, price)
    166         
    167         terminates(minion) (minion ! creator)
    168         expectMsg(IncompleteTx(
    169           audit.address.data,
    170           price.double,
    171           System.currency,
    172           Nil,
    173           toSpend.left,
    174           audit.userInfo))
    175     }
    176     
    177     "open existing incomplete transaction" in testLocalDb {
    178       minion =>
    179         val address     = BitcoinAddress("~address~")
    180         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    181         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    182         val price       = ticker.Price("123.45")
    183         val audit       = AuditData(address, Some(userinfo))
    184         val creator     = CreatorTx(audit, toSpend, price)
    185         
    186         // saved tx will have $100
    187         val tx = IncompleteTx(
    188           audit.address.data,
    189           price.double,
    190           System.currency,
    191           List(100),
    192           toSpend.left,
    193           audit.userInfo)
    194           
    195         IncompleteTransaction.save(tx)
    196         
    197         terminates(minion) (minion ! creator)
    198         expectMsg(tx)
    199     }
    200     
    201     "accept bills under the spending limit" in testGeneral {
    202       minion =>
    203         val inserted    = Inserted(USD(100))
    204         val balance     = FiatBalance(10000)
    205         
    206         val address     = BitcoinAddress("~address~")
    207         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    208         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    209         val price       = ticker.Price("123.45")
    210         val audit       = AuditData(address, Some(userinfo))
    211         val creator     = CreatorTx(audit, toSpend, price)
    212         
    213         val tx = IncompleteTx(
    214           audit.address.data,
    215           price.double,
    216           System.currency,
    217           List(100),
    218           toSpend.left,
    219           audit.userInfo)
    220           
    221         val inspect = InspectBill(inserted, tx, balance)
    222         
    223         terminates(minion) (minion ! inspect)
    224         expectMsg(BillOkay)
    225     }
    226     
    227     "reject bills over the spending limit" in testGeneral {
    228       minion =>
    229         val inserted    = Inserted(USD(100))
    230         val balance     = FiatBalance(10000)
    231         
    232         val address     = BitcoinAddress("~address~")
    233         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    234         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    235         val price       = ticker.Price("123.45")
    236         val audit       = AuditData(address, Some(userinfo))
    237         val creator     = CreatorTx(audit, toSpend, price)
    238         
    239         val tx = IncompleteTx(
    240           audit.address.data,
    241           price.double,
    242           System.currency,
    243           List(100, 1),
    244           toSpend.left,
    245           audit.userInfo)
    246           
    247         val inspect = InspectBill(inserted, tx, balance)
    248         
    249         terminates(minion) (minion ! inspect)
    250         expectInvalidBill(Bad(Msg.billOverLimit))
    251     }
    252     
    253     "reject bills over the wallet's balance" in testGeneral {
    254       minion =>
    255         val inserted    = Inserted(USD(5))
    256         val balance     = FiatBalance(100)
    257         
    258         val address     = BitcoinAddress("~address~")
    259         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    260         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    261         val price       = ticker.Price("123.45")
    262         val audit       = AuditData(address, Some(userinfo))
    263         val creator     = CreatorTx(audit, toSpend, price)
    264         
    265         val tx = IncompleteTx(
    266           audit.address.data,
    267           price.double,
    268           System.currency,
    269           List(50, 10, 10, 10, 10, 5, 1), // $96
    270           toSpend.left,
    271           audit.userInfo)
    272           
    273         val inspect = InspectBill(inserted, tx, balance)
    274         
    275         terminates(minion) (minion ! inspect)
    276         expectInvalidBill(Bad(Msg.billOverBalance))
    277     }
    278     
    279     "save confirmed bill and modified transaction to disk" in testLocalDb {
    280       minion =>
    281         val confirmed   = Confirmed(USD(100))
    282         
    283         val address     = BitcoinAddress("~address2~")
    284         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    285         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    286         val price       = ticker.Price("123.45")
    287         val audit       = AuditData(address, Some(userinfo))
    288         val creator     = CreatorTx(audit, toSpend, price)
    289         
    290         val tx = IncompleteTx(
    291           audit.address.data,
    292           price.double,
    293           System.currency,
    294           List(50),
    295           toSpend.left,
    296           audit.userInfo)
    297         
    298         terminates(minion) (minion ! (confirmed, tx))
    299         expectMsgPF() {
    300           case CashSaved(newTx) =>
    301             newTx.bills.sorted should be (List(50,100))
    302         }
    303         IncompleteTransaction.find(address.data).isDefined should be (true)
    304     }
    305     
    306     "save local completed transaction" in testLocalDb {
    307       minion =>
    308         val address     = BitcoinAddress("~address3~")
    309         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    310         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    311         val price       = ticker.Price("123.45")
    312         val audit       = AuditData(address, Some(userinfo))
    313         val creator     = CreatorTx(audit, toSpend, price)
    314         
    315         val incTx = IncompleteTx(
    316           audit.address.data,
    317           price.double,
    318           System.currency,
    319           List(50),
    320           toSpend.left,
    321           audit.userInfo)
    322           
    323         val tx = CompleteTx(incTx, "~txid~")
    324           
    325         IncompleteTransaction.save(incTx)
    326         terminates(minion) (minion ! ("save", tx))
    327         IncompleteTransaction.find(incTx.address).isDefined should be (false)
    328         CompletedTransaction.find(tx.txid).isDefined should be (true)
    329     }
    330     
    331     "save transaction to DDB Expenditure table" in testExpenditure {
    332       minion =>
    333         val address     = BitcoinAddress("~address4~")
    334         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    335         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    336         val price       = ticker.Price("123.45")
    337         val audit       = AuditData(address, Some(userinfo))
    338         val creator     = CreatorTx(audit, toSpend, price)
    339         
    340         val incTx = IncompleteTx(
    341           audit.address.data,
    342           price.double,
    343           System.currency,
    344           List(50, 100, 10), // $160
    345           toSpend.left,
    346           audit.userInfo)
    347           
    348         val tx = CompleteTx(incTx, "~txid~")
    349         val table = dynamoDB.table(expenditure).get
    350         
    351         terminates(minion) (minion ! ("expenditure", tx))
    352         val result = table.query(Seq("address" -> Condition.eq(tx.address))).headOption
    353         result.isDefined should be (true)
    354         result.get.attributes.find(_.name == "txid").get.value.s.get should equal(tx.txid)
    355         result.get.attributes.find(_.name == "paid").get.value.n.get should equal("160")
    356         result.get.attributes.find(_.name == "currency").get.value.s.get should equal(tx.currency.toString)
    357     }
    358     
    359     "save transaction to DDB Transaction table" in testTransaction {
    360       minion =>
    361         val address     = BitcoinAddress("~address5~")
    362         val userinfo    = UserInfo("test@example.com", purchaseLimit = Some(3000))
    363         val toSpend     = LeftToSpend(200, userinfo.purchaseLimit.get)
    364         val price       = ticker.Price("123.45")
    365         val audit       = AuditData(address, Some(userinfo))
    366         val creator     = CreatorTx(audit, toSpend, price)
    367         
    368         val incTx = IncompleteTx(
    369           audit.address.data,
    370           price.double,
    371           System.currency,
    372           List(50, 100, 10), // $160
    373           toSpend.left,
    374           audit.userInfo)
    375           
    376         val tx = CompleteTx(incTx, "~txid~")
    377         val table = dynamoDB.table(transaction).get
    378         
    379         terminates(minion) (minion ! ("transaction", tx))
    380         val result = table.query(Seq("txid" -> Condition.eq(tx.txid))).headOption
    381         result.isDefined should be (true)
    382         result.get.attributes.find(_.name == "address").get.value.s.get should equal(tx.address)
    383         result.get.attributes.find(_.name == "price").get.value.n.get should equal(tx.price.toString)
    384         result.get.attributes.find(_.name == "currency").get.value.s.get should equal(tx.currency.toString)
    385         result.get.attributes.find(_.name == "chimera").get.value.s.get should equal(System.name)
    386         result.get.attributes.find(_.name == "bitcoin").get.value.n.get should equal((160 / tx.price) toString)
    387         
    388         // awscala library doesn't read number attributes correctly :/
    389         result.get.attributes.find(_.name == "bills").get.value.ss should equal(tx.bills.map(_.toString))
    390     }
    391   }
    392   
    393 }