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 }