nim-codex/tests/codex/testpurchasing.nim
markspanbroek 4175689745
Load purchase state from chain (#283)
* [purchasing] Simplify test

* [utils] Move StorageRequest.example up one level

* [purchasing] Load purchases from market

* [purchasing] load purchase states

* Implement myRequest() and getState() methods for OnChainMarket

* [proofs] Fix intermittently failing tests

Ensures that examples of proofs in tests are never of length 0;
these are considered invalid proofs by the smart contract logic.

* [contracts] Fix failing test

With the new solidity contracts update, a contract can only
be paid out after it started.

* [market] Add method to get request end time

* [purchasing] wait until purchase is finished

Purchase.wait() would previously wait until purchase
was started, now we wait until it is finished.

* [purchasing] Handle 'finished' and 'failed' states

* [marketplace] move to failed state once request fails

- Add support for subscribing to request failure events.
- Add supporting contract tests for subscribing to request failure events.
- Allow the PurchaseStarted state to move to PurchaseFailure once a request failure event is emitted
- Add supporting tests for moving from PurchaseStarted to PurchaseFailure
- Add state transition tests for PurchaseUnknown.

* [marketplace] Fix test with longer sleepAsync

* [integration] Add function to restart a codex node

* [purchasing] Set client address before requesting storage

To prevent the purchase id (which equals the request id)
from changing once it's been submitted.

* [contracts] Fix: OnChainMarket.getState()

Had the wrong method signature before

* [purchasing] Load purchases on node start

* [purchasing] Rename state 'PurchaseError' to 'PurchaseErrored'

Allows for an exception type called 'PurchaseError'

* [purchasing] Load purchases in background

No longer calls market.getRequest() for every purchase
on node start.

* [contracts] Add `$` for RequestId, SlotId and Nonce

To aid with debugging

* [purchasing] Add Purchasing.stop()

To ensure that all contract interactions have both a
start() and a stop() for

* [tests] Remove sleepAsync where possible

Use `eventually` loop instead, to make sure that we're
not waiting unnecessarily.

* [integration] Fix: handle non-json response in test

* [purchasing] Add purchase state to json

* [integration] Ensure that purchase is submitted before restart

Fixes test failure on slower CI

* [purchasing] re-implement `description` as method

Allows description to be set in the same module where the
state type is defined.

Co-authored-by: Eric Mastro <eric.mastro@gmail.com>

* [contracts] fix typo

Co-authored-by: Eric Mastro <eric.mastro@gmail.com>

* [market] Use more generic error type

Should we decide to change the provider type later

Co-authored-by: Eric Mastro <eric.mastro@gmail.com>

Co-authored-by: Eric Mastro <eric.mastro@gmail.com>
2022-11-08 08:10:17 +01:00

241 lines
9.2 KiB
Nim

import std/times
import pkg/asynctest
import pkg/chronos
import pkg/upraises
import pkg/stint
import pkg/codex/purchasing
import pkg/codex/purchasing/states/[finished, failed, error, started, submitted, unknown]
import ./helpers/mockmarket
import ./helpers/mockclock
import ./helpers/eventually
import ./examples
suite "Purchasing":
var purchasing: Purchasing
var market: MockMarket
var clock: MockClock
var request: StorageRequest
setup:
market = MockMarket.new()
clock = MockClock.new()
purchasing = Purchasing.new(market, clock)
request = StorageRequest(
ask: StorageAsk(
slots: uint8.example.uint64,
slotSize: uint32.example.u256,
duration: uint16.example.u256,
reward: uint8.example.u256
)
)
test "submits a storage request when asked":
discard await purchasing.purchase(request)
let submitted = market.requested[0]
check submitted.ask.slots == request.ask.slots
check submitted.ask.slotSize == request.ask.slotSize
check submitted.ask.duration == request.ask.duration
check submitted.ask.reward == request.ask.reward
test "remembers purchases":
let purchase1 = await purchasing.purchase(request)
let purchase2 = await purchasing.purchase(request)
check purchasing.getPurchase(purchase1.id) == some purchase1
check purchasing.getPurchase(purchase2.id) == some purchase2
test "has a default value for proof probability":
check purchasing.proofProbability != 0.u256
test "can change default value for proof probability":
purchasing.proofProbability = 42.u256
discard await purchasing.purchase(request)
check market.requested[0].ask.proofProbability == 42.u256
test "can override proof probability per request":
request.ask.proofProbability = 42.u256
discard await purchasing.purchase(request)
check market.requested[0].ask.proofProbability == 42.u256
test "has a default value for request expiration interval":
check purchasing.requestExpiryInterval != 0.u256
test "can change default value for request expiration interval":
purchasing.requestExpiryInterval = 42.u256
let start = getTime().toUnix()
discard await purchasing.purchase(request)
check market.requested[0].expiry == (start + 42).u256
test "can override expiry time per request":
let expiry = (getTime().toUnix() + 42).u256
request.expiry = expiry
discard await purchasing.purchase(request)
check market.requested[0].expiry == expiry
test "includes a random nonce in every storage request":
discard await purchasing.purchase(request)
discard await purchasing.purchase(request)
check market.requested[0].nonce != market.requested[1].nonce
test "sets client address in request":
discard await purchasing.purchase(request)
check market.requested[0].client == await market.getSigner()
test "succeeds when request is finished":
let purchase = await purchasing.purchase(request)
let request = market.requested[0]
let requestEnd = getTime().toUnix() + 42
market.requestEnds[request.id] = requestEnd
market.emitRequestFulfilled(request.id)
clock.set(requestEnd)
await purchase.wait()
check purchase.error.isNone
test "fails when request times out":
let purchase = await purchasing.purchase(request)
let request = market.requested[0]
clock.set(request.expiry.truncate(int64))
expect PurchaseTimeout:
await purchase.wait()
test "checks that funds were withdrawn when purchase times out":
let purchase = await purchasing.purchase(request)
let request = market.requested[0]
clock.set(request.expiry.truncate(int64))
expect PurchaseTimeout:
await purchase.wait()
check market.withdrawn == @[request.id]
suite "Purchasing state machine":
var purchasing: Purchasing
var market: MockMarket
var clock: MockClock
var request: StorageRequest
setup:
market = MockMarket.new()
clock = MockClock.new()
purchasing = Purchasing.new(market, clock)
request = StorageRequest(
ask: StorageAsk(
slots: uint8.example.uint64,
slotSize: uint32.example.u256,
duration: uint16.example.u256,
reward: uint8.example.u256
)
)
test "loads active purchases from market":
let me = await market.getSigner()
let request1, request2, request3 = StorageRequest.example
market.requested = @[request1, request2, request3]
market.activeRequests[me] = @[request1.id, request2.id]
await purchasing.load()
check isSome purchasing.getPurchase(PurchaseId(request1.id))
check isSome purchasing.getPurchase(PurchaseId(request2.id))
check isNone purchasing.getPurchase(PurchaseId(request3.id))
test "loads correct purchase.future state for purchases from market":
let me = await market.getSigner()
let request1, request2, request3, request4, request5 = StorageRequest.example
market.requested = @[request1, request2, request3, request4, request5]
market.activeRequests[me] = @[request1.id, request2.id, request3.id, request4.id, request5.id]
market.state[request1.id] = RequestState.New
market.state[request2.id] = RequestState.Started
market.state[request3.id] = RequestState.Cancelled
market.state[request4.id] = RequestState.Finished
market.state[request5.id] = RequestState.Failed
# ensure the started state doesn't error, giving a false positive test result
market.requestEnds[request2.id] = clock.now() - 1
await purchasing.load()
check purchasing.getPurchase(PurchaseId(request1.id)).?finished == false.some
check purchasing.getPurchase(PurchaseId(request2.id)).?finished == true.some
check purchasing.getPurchase(PurchaseId(request3.id)).?finished == true.some
check purchasing.getPurchase(PurchaseId(request4.id)).?finished == true.some
check purchasing.getPurchase(PurchaseId(request5.id)).?finished == true.some
check purchasing.getPurchase(PurchaseId(request5.id)).?error.isSome
test "moves to PurchaseSubmitted when request state is New":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requested = @[request]
market.state[request.id] = RequestState.New
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseSubmitted).isSome
test "moves to PurchaseStarted when request state is Started":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
market.requested = @[request]
market.state[request.id] = RequestState.Started
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseStarted).isSome
test "moves to PurchaseErrored when request state is Cancelled":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requested = @[request]
market.state[request.id] = RequestState.Cancelled
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseErrored).isSome
check purchase.error.?msg == "Purchase cancelled due to timeout".some
test "moves to PurchaseFinished when request state is Finished":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requested = @[request]
market.state[request.id] = RequestState.Finished
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseFinished).isSome
test "moves to PurchaseErrored when request state is Failed":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requested = @[request]
market.state[request.id] = RequestState.Failed
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseErrored).isSome
check purchase.error.?msg == "Purchase failed".some
test "moves to PurchaseErrored state once RequestFailed emitted":
let me = await market.getSigner()
let request = StorageRequest.example
market.requested = @[request]
market.activeRequests[me] = @[request.id]
market.state[request.id] = RequestState.Started
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
await purchasing.load()
# emit mock contract failure event
market.emitRequestFailed(request.id)
# must allow time for the callback to trigger the completion of the future
await sleepAsync(chronos.milliseconds(10))
# now check the result
let purchase = purchasing.getPurchase(PurchaseId(request.id))
let state = purchase.?state
check (state as PurchaseErrored).isSome
check (!purchase).error.?msg == "Purchase failed".some
test "moves to PurchaseFinished state once request finishes":
let me = await market.getSigner()
let request = StorageRequest.example
market.requested = @[request]
market.activeRequests[me] = @[request.id]
market.state[request.id] = RequestState.Started
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
await purchasing.load()
# advance the clock to the end of the request
clock.advance(request.ask.duration.truncate(int64))
# now check the result
proc getState: ?PurchaseState =
purchasing.getPurchase(PurchaseId(request.id)).?state as PurchaseState
check eventually (getState() as PurchaseFinished).isSome