mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 17:17:39 +00:00
827 lines
43 KiB
Haskell
827 lines
43 KiB
Haskell
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
|
|
module Bots.StoreService.NamesOwnerTests
|
|
( namesOwnerTests,
|
|
answerExpiry,
|
|
answerFree,
|
|
answerOwner,
|
|
answerUnowned,
|
|
byB,
|
|
codeIn,
|
|
committedPhase,
|
|
dynamisOrder,
|
|
failedPhase,
|
|
findBody,
|
|
labelBytes,
|
|
moveBody,
|
|
moveTo,
|
|
ownerOfSelector,
|
|
paidIn,
|
|
postFind,
|
|
postMove,
|
|
rewrite,
|
|
sentRegister,
|
|
stored,
|
|
)
|
|
where
|
|
|
|
import Bots.StoreService.ConfigTests (termsVersion)
|
|
import Bots.StoreService.FakeEthNode
|
|
import Bots.StoreService.NamesContractsTests (registrarOf, unhex)
|
|
import Bots.StoreService.NamesOrdersTests
|
|
import Bots.StoreService.NamesStoreTests (btc, insertInvoiceRow, order, ownerA, paidOrder, t0)
|
|
import Bots.StoreService.WebTests (capturingStderr, digestFixture, jsonObject, statusOf, webGet)
|
|
import Control.Applicative ((<|>))
|
|
import Control.Arrow ((&&&))
|
|
import Control.Concurrent (threadDelay)
|
|
import Control.Concurrent.Async (Async, async, cancel, wait)
|
|
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
|
|
import Control.Concurrent.STM (atomically, check)
|
|
import Control.Monad (forM, forM_, void)
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Aeson.KeyMap as KM
|
|
import Data.ByteString (ByteString)
|
|
import Data.IORef (readIORef)
|
|
import Data.Foldable (toList)
|
|
import Data.List (find, sort, sortOn)
|
|
import Data.Maybe (fromJust, fromMaybe)
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.Time.Clock (UTCTime, addUTCTime)
|
|
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
|
import Data.Word (Word64, Word8)
|
|
import Network.HTTP.Client (defaultManagerSettings, newManager)
|
|
import qualified Network.Wai.Handler.Warp as Warp
|
|
import Simplex.Chat.PaymentService.Types (InvoiceId (..), InvoiceStatus (..))
|
|
import Simplex.Messaging.Agent.Store.Common (withTransaction)
|
|
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
|
import Simplex.Messaging.Crypto.Secp256k1 (Secp256k1PrivateKey, signRecoverable)
|
|
import Simplex.Messaging.Eth.Address (Address)
|
|
import Simplex.Messaging.Eth.EIP712 (serializeSignature)
|
|
import Simplex.Messaging.SimplexName (LabelHash (..), labelHash)
|
|
import Simplex.Messaging.Util (tshow)
|
|
import StoreService.Config (NamesConfig (..))
|
|
import StoreService.Names.Abi (addressWord, hex, uint256)
|
|
import StoreService.Names.Chain (TxHash (..))
|
|
import StoreService.Names.Orders (OrdersEnv (..), signedRequestSeconds)
|
|
import StoreService.Names.Phases (RegistrationId (..), phasesWaiting)
|
|
import StoreService.Names.Signatures (OrderFields (..), findMessage, parseAnyCaseAddress, personalDigest, retargetMessage)
|
|
import StoreService.Names.Store hiding (findLimit)
|
|
import StoreService.Names.Turns (withLabelTurn)
|
|
import StoreService.Store (RevokeResult (..))
|
|
import StoreService.Web.Http (createLimit, findLimit, holdMicros, readLimit)
|
|
import StoreService.Web.Names (Route (..), namesApp)
|
|
import StoreService.Web.Names.Owner (findHandler, retargetHandler)
|
|
import StoreService.Web.Names.Registration (registrationHandler)
|
|
import StoreService.Web.Server (WebEnv (..), webApp)
|
|
import System.Timeout (timeout)
|
|
import Test.Hspec
|
|
|
|
namesOwnerTests :: Spec
|
|
namesOwnerTests = describe "StoreService names: owner" $ do
|
|
describe "find" $ do
|
|
it "lists the owner's orders newest first, a moved, an unpaid and a lapsed one read failed included, at most 50, and 404 for none" testFind
|
|
it "refuses another signer's signature, a stale timestamp and a malformed body" testFindRefused
|
|
describe "another name" $ do
|
|
it "refuses a move from registered, queued, taken, and committed without a refused reveal" testMoveRefusedPhases
|
|
it "moves a failed order, a lapsed one, and a committed one whose links were refused, to the same name with other links too" testMoveAccepted
|
|
it "answers a body equal to the current fields with the view unchanged, counting no move, whatever its timestamp" testMoveRepeat
|
|
it "answers a repeat after the terms version changed with the view unchanged, counting no second move" testMoveRepeatAfterBump
|
|
it "moves an order by a body differing only in terms, storing them" testMoveTermsOnly
|
|
it "answers terms_changed for an old version or the wrong variant, listed or not, a body differing only in terms included, and moves under the community terms, storing them" testMoveTermsChanged
|
|
it "checks the terms after the timestamp, /retry's first check, the existing orders and the duration, before the chain" testMoveTermsCheckOrder
|
|
it "answers move_limit before checking the terms" testMoveLimitBeforeTerms
|
|
it "answers the fourth move move_limit with the current view, changing nothing" testMoveLimit
|
|
it "refuses a move meeting another live order of the wallet's or the commitment with a bare order_exists, before the price" testMoveMeetsOrder
|
|
it "runs the order checks on the new fields: invalid_name, the duration, name_taken, name_check_unavailable" testMoveOrderChecks
|
|
it "registers a failed order whose register went through, by its mined register or by ownerOf, answering registration_changed and spending a code" testMoveLanded
|
|
it "answers name_check_unavailable when the register check cannot read the chain, changing nothing" testMoveLandedUnread
|
|
it "spends a released code still unused when its failed order turns out registered, and alerts once when it was spent elsewhere meanwhile" testMoveLandedReleasedCode
|
|
it "answers a dearer name price_too_high with its price, and moves to a cheaper one at its price, crediting the difference to its balance" testMovePrice
|
|
it "credits nothing for a cheaper move whose compare-and-set loses" testCheaperMoveLostRace
|
|
it "keeps fromBalance through a move to a cheaper name" testMoveKeepsFromBalance
|
|
it "checks a code order's code again: code_not_covering for a shorter name, a held code kept though revoked and expired since" testMoveCodeOrder
|
|
it "reserves a released code again while unused, else answers code_used, code_reserved, code_invalid or code_expired" testMoveReleasedCode
|
|
it "refuses sealed over 11095 characters, a body over 16384 bytes and malformed fields, accepting the longest" testMoveSizes
|
|
it "refuses another signer's signature and a stale timestamp, and answers an unknown order 404" testMoveRefusedSigner
|
|
it "answers registration_changed with the current view when the order changed during the checks, writing nothing" testMoveLostRace
|
|
it "moves A to B while another order moves B to A, in the turns' hash order, without deadlock" testMovesCross
|
|
describe "polls" $
|
|
it "wakes a held registration poll by another name" testPollsWoken
|
|
|
|
-- fixtures
|
|
|
|
-- | The owner routes and the registration poll as namesRoutes registers them, with the code orders, since a NamesEnv needs a running chat controller.
|
|
withOwner :: (Ctx -> IO a) -> IO a
|
|
withOwner = withOwnerAs id
|
|
|
|
withOwnerAs :: (NamesConfig -> NamesConfig) -> (Ctx -> IO a) -> IO a
|
|
withOwnerAs change action = withOrders $ \ctx -> do
|
|
let ctx' = configured change ctx
|
|
forM_ ["alicebakery", "dynamis"] $ answerUnowned ctx'
|
|
serveOwner ctx' action
|
|
|
|
-- | The owner routes over the context's settings.
|
|
serveOwner :: Ctx -> (Ctx -> IO a) -> IO a
|
|
serveOwner ctx@Ctx {cOrders = orders, cWeb = web} action = do
|
|
let routes path = ownerRoutes orders path <|> codeRoutes orders (weProviders web) (weWaiters web) path
|
|
app req respond = fromMaybe (webApp web req respond) (namesApp (weHttp web) routes req respond)
|
|
mgr <- newManager defaultManagerSettings
|
|
Warp.testWithApplication (pure app) $ \port -> action ctx {cClient = (mgr, "http://127.0.0.1:" <> show port)}
|
|
|
|
ownerRoutes :: OrdersEnv -> [Text] -> Maybe Route
|
|
ownerRoutes orders = \case
|
|
["api", "registration", "find"] -> Just Route {rMethod = "POST", rLimit = findLimit, rHandler = findHandler orders}
|
|
["api", "registration", rid] -> Just Route {rMethod = "GET", rLimit = readLimit, rHandler = registrationHandler orders holdMicros rid}
|
|
["api", "registration", rid, "name"] -> Just Route {rMethod = "POST", rLimit = createLimit, rHandler = retargetHandler orders rid}
|
|
_ -> Nothing
|
|
|
|
signWith :: Ctx -> Secp256k1PrivateKey -> ByteString -> IO Text
|
|
signWith Ctx {cRandom} key msg = hex . serializeSignature . either (error . show) id <$> signRecoverable cRandom key (personalDigest msg)
|
|
|
|
clockSeconds :: Ctx -> IO Word64
|
|
clockSeconds Ctx {cClock} = seconds <$> readIORef cClock
|
|
|
|
seconds :: UTCTime -> Word64
|
|
seconds = floor . utcTimeToPOSIXSeconds
|
|
|
|
signedWindow :: Word64
|
|
signedWindow = fromInteger signedRequestSeconds
|
|
|
|
byB :: Order -> Order
|
|
byB o = o {oOwner = ownerBText, oKey = keyB}
|
|
|
|
dynamisOrder :: Order
|
|
dynamisOrder = aliceOrder {oLabel = "dynamis", oCommitment = commitmentOf 3, oSealed = "sealed-dynamis-3", oPrice = 20000}
|
|
|
|
-- viem's selectors of nameExpires(uint256), reservedNames(bytes32) and ownerOf(uint256), as in the calls of the contracts tests
|
|
nameExpiresSelector, reservedNamesSelector, ownerOfSelector :: ByteString
|
|
nameExpiresSelector = unhex "d6e4fa86"
|
|
reservedNamesSelector = unhex "389c0748"
|
|
ownerOfSelector = unhex "6352211e"
|
|
|
|
labelBytes :: Text -> ByteString
|
|
labelBytes label = lh
|
|
where
|
|
LabelHash lh = labelHash label
|
|
|
|
-- | ownerOf of a name nobody holds: the registrar's bare require, a revert without data.
|
|
answerUnowned :: Ctx -> Text -> IO ()
|
|
answerUnowned Ctx {cNode} label = answerCall cNode (registrarOf cNode) (ownerOfSelector <> labelBytes label) $ Reverts ""
|
|
|
|
answerOwner :: Ctx -> Text -> Address -> IO ()
|
|
answerOwner Ctx {cNode} label owner = answerCall cNode (registrarOf cNode) (ownerOfSelector <> labelBytes label) $ Returns $ addressWord owner
|
|
|
|
answerExpiry :: Ctx -> Text -> Word64 -> IO ()
|
|
answerExpiry Ctx {cNode} label expiry = answerCall cNode (registrarOf cNode) (nameExpiresSelector <> labelBytes label) $ Returns $ uint256 expiry
|
|
|
|
-- | A register transaction of the order as the sender saves it, then moved to this outcome.
|
|
sentRegister :: Ctx -> NameRegistration -> Word8 -> TxStatus -> IO TxHash
|
|
sentRegister Ctx {cStore} NameRegistration {nrId} n status = withTransaction cStore $ \db -> do
|
|
let h = TxHash $ digestFixture n
|
|
void $ insertTransaction db (TxRegister nrId) NewTransaction {ntxRaw = "raw", ntxHash = h, ntxNonce = fromIntegral n} orderTime
|
|
recordTransactionOutcome db h TSPending status (Just 120) `shouldReturn` True
|
|
pure h
|
|
|
|
-- | The chain answers the name as available and held by nobody.
|
|
answerFree :: Ctx -> Text -> IO ()
|
|
answerFree ctx label = answerReserved ctx label 0 >> answerUnowned ctx label
|
|
|
|
answerReserved :: Ctx -> Text -> Int -> IO ()
|
|
answerReserved Ctx {cNode} label reason = do
|
|
answerCall cNode (registrarOf cNode) (nameExpiresSelector <> lh) $ Returns $ uint256 (0 :: Int)
|
|
answerCall cNode fakeController (reservedNamesSelector <> lh) $ Returns $ uint256 reason
|
|
where
|
|
LabelHash lh = labelHash label
|
|
|
|
-- | A paid order, settled, then written as the worker would write it.
|
|
paidIn :: Ctx -> Order -> (NameRegistration -> NameRegistration) -> IO NameRegistration
|
|
paidIn ctx o change = do
|
|
(invId, _) <- placed ctx o
|
|
settleNow ctx invId settled `shouldReturn` Right ISPaid
|
|
r <- invoiceRegistration ctx invId
|
|
rewrite ctx r change
|
|
|
|
-- | A code order of a code of this length, then written as the worker would write it.
|
|
codeIn :: Ctx -> Int -> Order -> (NameRegistration -> NameRegistration) -> IO (NameRegistration, PrepaidCode)
|
|
codeIn ctx len o change = do
|
|
(text, code) <- issueCode ctx len 2 id
|
|
r <- registrationById ctx =<< registered ctx text o
|
|
(,code) <$> rewrite ctx r change
|
|
|
|
-- | A write outside the turns, as the worker's, at the stored version.
|
|
rewrite :: Ctx -> NameRegistration -> (NameRegistration -> NameRegistration) -> IO NameRegistration
|
|
rewrite Ctx {cStore, cClock} NameRegistration {nrId} change = do
|
|
t <- readIORef cClock
|
|
withTransaction cStore $ \db -> do
|
|
current <- fromJust <$> getRegistration db nrId
|
|
updateRegistration db (change current) t >>= \case
|
|
Written _ -> fromJust <$> getRegistration db nrId
|
|
refused -> error $ "not written: " <> show refused
|
|
|
|
-- | The amount and credited of the order's balance.
|
|
balanceOf :: Ctx -> NameRegistration -> IO (Maybe (Int, Int))
|
|
balanceOf Ctx {cStore} NameRegistration {nrId} = fmap (\Balance {blAmount, blCredited} -> (blAmount, blCredited)) <$> withTransaction cStore (\db -> getBalance db nrId orderTime)
|
|
|
|
stored :: Ctx -> NameRegistration -> IO NameRegistration
|
|
stored ctx = registrationById ctx . nrId
|
|
|
|
failedPhase, takenPhase, registeredPhase, committedPhase, linksRefusedPhase :: NameRegistration -> NameRegistration
|
|
failedPhase r = r {nrPhase = PFailed, nrFailedAt = Just orderTime, nrError = Just "The name could not be registered"}
|
|
takenPhase r = r {nrPhase = PTaken}
|
|
registeredPhase r = r {nrPhase = PRegistered, nrExpiresAt = Just (seconds orderTime + 3 * 31536000)}
|
|
committedPhase r =
|
|
r
|
|
{ nrPhase = PCommitted,
|
|
nrCommitTx = Just (TxHash $ digestFixture 8),
|
|
nrCommitBlock = Just 100,
|
|
nrRevealAfter = Just (seconds orderTime + 60),
|
|
nrRevealBefore = Just (seconds orderTime + 86400)
|
|
}
|
|
linksRefusedPhase r = (committedPhase r) {nrLinksRefusedAt = Just orderTime}
|
|
|
|
-- | Another name's fields, with the page's duration for 2 years at the service's clock.
|
|
moveTo :: Ctx -> Text -> Int -> IO OrderFields
|
|
moveTo Ctx {cClock} label n = do
|
|
t <- readIORef cClock
|
|
pure OrderFields {ofLabelHash = labelHash label, ofLength = T.length label, ofDuration = yearsDuration 2 t, ofCommitment = commitmentOf n, ofSealed = "sealed-" <> label <> "-" <> tshow n, ofAgreedTerms = termsVersion}
|
|
|
|
moveBody :: Ctx -> Secp256k1PrivateKey -> RegistrationId -> OrderFields -> IO J.Object
|
|
moveBody ctx key rid fields = clockSeconds ctx >>= moveBodyAt ctx key rid fields
|
|
|
|
moveBodyAt :: Ctx -> Secp256k1PrivateKey -> RegistrationId -> OrderFields -> Word64 -> IO J.Object
|
|
moveBodyAt ctx key rid fields@OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed, ofAgreedTerms} timestamp = do
|
|
sig <- signWith ctx key $ retargetMessage rid fields timestamp
|
|
pure $
|
|
KM.fromList
|
|
[ ("labelHash", J.String $ hex lh),
|
|
("length", J.toJSON ofLength),
|
|
("duration", J.toJSON ofDuration),
|
|
("commitment", J.String $ hex ofCommitment),
|
|
("sealed", J.String ofSealed),
|
|
("terms", J.String ofAgreedTerms),
|
|
("timestamp", J.toJSON timestamp),
|
|
("signature", J.String sig)
|
|
]
|
|
|
|
postMove :: Ctx -> RegistrationId -> J.Object -> IO (Int, Maybe J.Object)
|
|
postMove ctx rid = post ctx (registrationPath rid <> "/name") . J.encode
|
|
|
|
move :: Ctx -> Secp256k1PrivateKey -> NameRegistration -> OrderFields -> IO (Int, Maybe J.Object)
|
|
move ctx key NameRegistration {nrId} fields = moveBody ctx key nrId fields >>= postMove ctx nrId
|
|
|
|
-- | The view of an accepted move, failing the test otherwise.
|
|
moved :: Ctx -> Secp256k1PrivateKey -> NameRegistration -> OrderFields -> IO J.Object
|
|
moved ctx key r fields =
|
|
move ctx key r fields >>= \case
|
|
(200, Just v) -> pure v
|
|
other -> error $ "move refused: " <> show other
|
|
|
|
registrationPath :: RegistrationId -> String
|
|
registrationPath (RegistrationId rid) = "/api/registration/" <> T.unpack rid
|
|
|
|
getView :: Ctx -> NameRegistration -> IO J.Object
|
|
getView ctx NameRegistration {nrId} = webGet (cClient ctx) (registrationPath nrId) >>= jsonObject
|
|
|
|
findBody :: Ctx -> Secp256k1PrivateKey -> Text -> Word64 -> IO J.Object
|
|
findBody ctx key owner timestamp = do
|
|
sig <- signWith ctx key $ findMessage (either (error . show) id $ parseAnyCaseAddress owner) timestamp
|
|
pure $ KM.fromList [("owner", J.String owner), ("timestamp", J.toJSON timestamp), ("signature", J.String sig)]
|
|
|
|
postFind :: Ctx -> J.Object -> IO (Int, Maybe J.Object)
|
|
postFind ctx = post ctx "/api/registration/find" . J.encode
|
|
|
|
currentFields :: NameRegistration -> OrderFields
|
|
currentFields NameRegistration {nrLabelHash, nrLength, nrDuration, nrCommitment, nrSealed, nrAgreedTerms} =
|
|
OrderFields {ofLabelHash = nrLabelHash, ofLength = nrLength, ofDuration = nrDuration, ofCommitment = nrCommitment, ofSealed = nrSealed, ofAgreedTerms = nrAgreedTerms}
|
|
|
|
fieldsIn :: J.Object -> [(Text, J.Value)] -> Expectation
|
|
fieldsIn v = mapM_ (\(k, value) -> (k, field k v) `shouldBe` (k, Just value))
|
|
|
|
refusedWithView :: Text -> J.Object -> (Int, Maybe J.Object)
|
|
refusedWithView tag view = (409, Just $ KM.fromList [("error", J.String tag), ("registration", J.Object view)])
|
|
|
|
labelHashText :: Text -> J.Value
|
|
labelHashText label = J.String $ hex lh
|
|
where
|
|
LabelHash lh = labelHash label
|
|
|
|
codeRow :: Ctx -> PrepaidCode -> IO PrepaidCode
|
|
codeRow Ctx {cStore} PrepaidCode {pcId} = withTransaction cStore $ \db -> fromJust <$> getPrepaidCodeById db pcId
|
|
|
|
-- | Bounded, since a deadlock would otherwise hang the suite.
|
|
within :: String -> IO a -> IO a
|
|
within what action = timeout 10000000 action >>= maybe (error $ what <> " did not finish") pure
|
|
|
|
-- | A registration poll at the order's version, once the server holds it.
|
|
held :: Ctx -> NameRegistration -> IO (Async (Int, J.Object))
|
|
held ctx@Ctx {cOrders} NameRegistration {nrId, nrVersion} = do
|
|
waiting <- atomically $ phasesWaiting (oePhases cOrders)
|
|
poll <- async $ webGet (cClient ctx) (registrationPath nrId <> "?seenVersion=" <> show nrVersion) >>= \resp -> (statusOf resp,) <$> jsonObject resp
|
|
within "the hold" $ atomically $ phasesWaiting (oePhases cOrders) >>= check . (> waiting)
|
|
pure poll
|
|
|
|
woken :: Async (Int, J.Object) -> IO (Int, J.Object)
|
|
woken poll = timeout 5000000 (wait poll) >>= maybe (cancel poll >> error "the poll was not woken") pure
|
|
|
|
-- | Long enough for a request just sent to reach its turns.
|
|
parkDelay :: Int
|
|
parkDelay = 200000
|
|
|
|
-- tests
|
|
|
|
testFind :: IO ()
|
|
testFind = withOwner $ \ctx@Ctx {cStore} -> do
|
|
forM_ [1 .. 50 :: Int] $ \i -> do
|
|
(_, code) <- issueCode ctx 8 2 id
|
|
let new = NewRegistration {nrgId = oldOrder i, nrgOwner = ownerA, nrgOrder = order ("alicebakery" <> tshow i) (fromIntegral i), nrgYears = 2}
|
|
withTransaction cStore (\db -> createCodeRegistration db new code (addUTCTime (fromIntegral i) t0)) `shouldReturn` Right ()
|
|
answerFree ctx "alicebakers"
|
|
answerUnowned ctx "alicebakery50"
|
|
movedOrder <- registrationById ctx (oldOrder 50) >>= \r -> rewrite ctx r failedPhase
|
|
to <- moveTo ctx "alicebakers" 10
|
|
void $ moved ctx keyA movedOrder to
|
|
(InvoiceId invId, _) <- placed ctx aliceOrder
|
|
paid <- invoiceRegistration ctx (InvoiceId invId)
|
|
advance ctx 1
|
|
lapsed <- paidIn ctx dynamisOrder $ \r -> (committedPhase r) {nrRevealBefore = Just (seconds orderTime)}
|
|
findBody ctx keyA (T.toLower ownerAText) (seconds orderTime) >>= postFind ctx >>= \case
|
|
(200, Just v) | Just (J.Array found) <- KM.lookup "registrations" v -> do
|
|
let entries = map objectEntry $ toList found
|
|
map (field "registrationId") entries `shouldBe` map (Just . idText) (nrId lapsed : nrId paid : [oldOrder i | i <- [50, 49 .. 3]])
|
|
case entries of
|
|
lapsedEntry : unpaidEntry : moveEntry : _ -> do
|
|
(field "phase" lapsedEntry, nrPhase lapsed) `shouldBe` (Just "failed", PCommitted)
|
|
sort (KM.keys unpaidEntry) `shouldBe` sort ["registrationId", "invoiceId", "phase", "createdAt", "labelHash", "sealed", "balanceSource", "heldBy"]
|
|
unpaidEntry
|
|
`shouldBe` KM.fromList
|
|
[ ("registrationId", idText $ nrId paid),
|
|
("invoiceId", J.String invId),
|
|
("phase", "unpaid"),
|
|
("createdAt", J.toJSON orderTime),
|
|
("labelHash", labelHashText "alicebakery"),
|
|
("sealed", "sealed-alicebakery-1"),
|
|
("balanceSource", J.Null),
|
|
("heldBy", J.Null)
|
|
]
|
|
moveEntry
|
|
`shouldBe` KM.fromList
|
|
[ ("registrationId", "old50"),
|
|
("invoiceId", J.Null),
|
|
("phase", "queued"),
|
|
("createdAt", J.toJSON $ addUTCTime 50 t0),
|
|
("labelHash", labelHashText "alicebakers"),
|
|
("sealed", "sealed-alicebakers-10"),
|
|
("balanceSource", J.Null),
|
|
("heldBy", J.Null)
|
|
]
|
|
_ -> expectationFailure "fewer than three entries"
|
|
other -> expectationFailure $ "find refused: " <> show other
|
|
(findBody ctx keyB ownerBText (seconds orderTime) >>= postFind ctx) `shouldReturn` refusedWith 404 "not_found"
|
|
where
|
|
oldOrder i = RegistrationId $ "old" <> tshow (i :: Int)
|
|
idText (RegistrationId t) = J.String t
|
|
objectEntry = \case
|
|
J.Object o -> o
|
|
other -> error $ "not an entry: " <> show other
|
|
|
|
testFindRefused :: IO ()
|
|
testFindRefused = withOwner $ \ctx -> do
|
|
void $ placed ctx aliceOrder
|
|
let ts = seconds orderTime
|
|
(findBody ctx keyB ownerAText ts >>= postFind ctx) `shouldReturn` refusedWith 400 "bad_request"
|
|
(findBody ctx keyA ownerAText (ts - signedWindow - 1) >>= postFind ctx) `shouldReturn` refusedWith 400 "bad_request"
|
|
(findBody ctx keyA ownerAText (ts + signedWindow + 1) >>= postFind ctx) `shouldReturn` refusedWith 400 "bad_request"
|
|
body <- findBody ctx keyA ownerAText ts
|
|
postFind ctx (KM.insert "owner" "0x014f9c09" body) `shouldReturn` refusedWith 400 "bad_request"
|
|
postFind ctx (KM.delete "signature" body) `shouldReturn` refusedWith 400 "bad_request"
|
|
post ctx "/api/registration/find" "[]" `shouldReturn` refusedWith 400 "bad_request"
|
|
findBody ctx keyA ownerAText (ts - signedWindow) >>= postFind ctx >>= (`shouldBe` 200) . fst
|
|
|
|
testMoveRefusedPhases :: IO ()
|
|
testMoveRefusedPhases = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
to <- moveTo ctx "alicebakers" 10
|
|
registeredOrder <- paidIn ctx aliceOrder registeredPhase
|
|
committedOrder <- paidIn ctx aliceOrder {oCommitment = commitmentOf 2, oTimestamp = seconds orderTime + 1} committedPhase
|
|
queuedOrder <- paidIn ctx dynamisOrder id
|
|
takenOrder <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 4} takenPhase
|
|
forM_ [registeredOrder, committedOrder, queuedOrder, takenOrder] $ \r -> do
|
|
(nrPhase r,) <$> move ctx keyA r to `shouldReturn` (nrPhase r, refusedWith 400 "bad_request")
|
|
stored ctx r `shouldReturn` r
|
|
|
|
testMoveAccepted :: IO ()
|
|
testMoveAccepted = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
failedOrder <- paidIn ctx aliceOrder failedPhase
|
|
to <- moveTo ctx "alicebakers" 10
|
|
v <- moved ctx keyA failedOrder to
|
|
fieldsIn v
|
|
[ ("phase", "queued"),
|
|
("labelHash", labelHashText "alicebakers"),
|
|
("commitment", J.String $ hex $ ofCommitment to),
|
|
("sealed", J.String $ ofSealed to),
|
|
("duration", J.toJSON $ ofDuration to),
|
|
("price", J.Number 10000),
|
|
("moves", J.Number 1),
|
|
("version", J.toJSON $ nrVersion failedOrder + 1),
|
|
("linksRefused", J.Bool False),
|
|
("label", J.Null),
|
|
("commitTx", J.Null),
|
|
("revealAfter", J.Null),
|
|
("revealBefore", J.Null),
|
|
("error", J.Null)
|
|
]
|
|
written <- stored ctx failedOrder
|
|
(nrPhase written, nrLive written, nrMoves written, nrFailedAt written, nrOrderHash written) `shouldBe` (PQueued, True, 1, Nothing, nrOrderHash failedOrder)
|
|
-- the same name with other links, after a reveal refused for its links
|
|
refusedOrder <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 2} linksRefusedPhase
|
|
sameName <- moveTo ctx "alicebakery" 11
|
|
v2 <- moved ctx keyB refusedOrder sameName
|
|
fieldsIn v2 [("phase", "queued"), ("labelHash", labelHashText "alicebakery"), ("linksRefused", J.Bool False), ("commitTx", J.Null), ("revealAfter", J.Null), ("moves", J.Number 1)]
|
|
(nrLinksRefusedAt &&& nrCommitTx) <$> stored ctx refusedOrder `shouldReturn` (Nothing, Nothing)
|
|
-- a committed order with no reveal past revealBefore reads failed
|
|
lapsed <- paidIn ctx dynamisOrder $ \r -> (committedPhase r) {nrRevealBefore = Just (seconds orderTime - 1)}
|
|
back <- moveTo ctx "alicebakery" 12
|
|
v3 <- moved ctx keyA lapsed back
|
|
fieldsIn v3 [("phase", "queued"), ("error", J.Null), ("price", J.Number 10000)]
|
|
|
|
testMoveRepeat :: IO ()
|
|
testMoveRepeat = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
-- the current fields from failed: nothing moves
|
|
(status, v) <- move ctx keyA r (currentFields r)
|
|
(status, field "phase" =<< v, field "moves" =<< v) `shouldBe` (200, Just "failed", Just (J.Number 0))
|
|
stored ctx r `shouldReturn` r
|
|
to <- moveTo ctx "alicebakers" 10
|
|
body <- moveBody ctx keyA (nrId r) to
|
|
first' <- postMove ctx (nrId r) body
|
|
fst first' `shouldBe` 200
|
|
movedOnce <- stored ctx r
|
|
advance ctx 3600
|
|
postMove ctx (nrId r) body `shouldReturn` first'
|
|
resigned <- moveBody ctx keyA (nrId r) to
|
|
postMove ctx (nrId r) resigned `shouldReturn` first'
|
|
stored ctx r `shouldReturn` movedOnce
|
|
nrMoves movedOnce `shouldBe` 1
|
|
|
|
testMoveRepeatAfterBump :: IO ()
|
|
testMoveRepeatAfterBump = withOwner $ \ctx0 -> do
|
|
answerFree ctx0 "alicebakers"
|
|
r <- paidIn ctx0 aliceOrder failedPhase
|
|
body <- moveBody ctx0 keyA (nrId r) =<< moveTo ctx0 "alicebakers" 10
|
|
first' <- postMove ctx0 (nrId r) body
|
|
fst first' `shouldBe` 200
|
|
movedOnce <- stored ctx0 r
|
|
serveOwner (configured bumpedTerms ctx0) $ \ctx -> do
|
|
advance ctx 60
|
|
postMove ctx (nrId r) body `shouldReturn` first'
|
|
stored ctx r `shouldReturn` movedOnce
|
|
(nrMoves &&& nrAgreedTerms) movedOnce `shouldBe` (1, termsVersion)
|
|
|
|
testMoveTermsOnly :: IO ()
|
|
testMoveTermsOnly = withOwner $ \ctx -> do
|
|
r <- paidIn ctx aliceOrder $ \p -> (failedPhase p) {nrAgreedTerms = "2026-10-01"}
|
|
(status, v) <- move ctx keyA r (currentFields r) {ofAgreedTerms = termsVersion}
|
|
(status, field "phase" =<< v, field "moves" =<< v) `shouldBe` (200, Just "queued", Just (J.Number 1))
|
|
(nrAgreedTerms &&& nrMoves) <$> stored ctx r `shouldReturn` (termsVersion, 1)
|
|
|
|
testMoveTermsChanged :: IO ()
|
|
testMoveTermsChanged = withOwnerAs (listName "alicebakers") $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
forM_ [olderTerms, communityTerms termsVersion] $ \t ->
|
|
move ctx keyA r (currentFields r) {ofCommitment = commitmentOf 11, ofAgreedTerms = t} `shouldReturn` termsChanged termsVersion Nothing
|
|
move ctx keyA r (currentFields r) {ofAgreedTerms = olderTerms} `shouldReturn` termsChanged termsVersion Nothing
|
|
to <- moveTo ctx "alicebakers" 10
|
|
forM_ [termsVersion, communityTerms olderTerms] $ \t ->
|
|
move ctx keyA r to {ofAgreedTerms = t} `shouldReturn` termsChanged termsVersion (Just bakers)
|
|
stored ctx r `shouldReturn` r
|
|
v <- moved ctx keyA r to {ofAgreedTerms = communityTerms termsVersion}
|
|
fieldsIn v [("terms", J.String $ communityTerms termsVersion), ("community", J.object [("for", J.String bakers)])]
|
|
nrAgreedTerms <$> stored ctx r `shouldReturn` communityTerms termsVersion
|
|
|
|
testMoveTermsCheckOrder :: IO ()
|
|
testMoveTermsCheckOrder = withOwner $ \ctx@Ctx {cNode} -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
to <- (\f -> f {ofAgreedTerms = olderTerms}) <$> moveTo ctx "alicebakers" 10
|
|
ts <- clockSeconds ctx
|
|
(moveBodyAt ctx keyA (nrId r) to (ts - signedWindow - 1) >>= postMove ctx (nrId r)) `shouldReturn` refusedWith 400 "bad_request"
|
|
setFault cNode "eth_call" HttpFailure
|
|
move ctx keyA r to `shouldReturn` refusedWith 503 "name_check_unavailable"
|
|
clearFault cNode "eth_call"
|
|
void $ placed ctx dynamisOrder
|
|
toDynamis <- moveTo ctx "dynamis" 10
|
|
move ctx keyA r toDynamis {ofAgreedTerms = olderTerms} `shouldReturn` refusedWith 409 "order_exists"
|
|
move ctx keyA r to {ofDuration = ofDuration to + 3601} `shouldReturn` refusedWith 400 "bad_request"
|
|
answerReserved ctx "alicebakers" 1
|
|
move ctx keyA r to `shouldReturn` termsChanged termsVersion Nothing
|
|
stored ctx r `shouldReturn` r
|
|
answerExpiry ctx "alicebakery" (seconds orderTime + 2 * 31536000)
|
|
answerOwner ctx "alicebakery" ownerA
|
|
answer <- move ctx keyA r to
|
|
view <- getView ctx r
|
|
answer `shouldBe` refusedWithView "registration_changed" view
|
|
field "phase" view `shouldBe` Just "registered"
|
|
|
|
testMoveLimitBeforeTerms :: IO ()
|
|
testMoveLimitBeforeTerms = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder $ \p -> (failedPhase p) {nrMoves = 3}
|
|
to <- moveTo ctx "alicebakers" 10
|
|
view <- getView ctx r
|
|
move ctx keyA r to {ofAgreedTerms = olderTerms} `shouldReturn` refusedWithView "move_limit" view
|
|
stored ctx r `shouldReturn` r
|
|
|
|
testMoveLimit :: IO ()
|
|
testMoveLimit = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
forM_ (zip [1 :: Int ..] ["alicebakers", "alicebakery", "alicebakers"]) $ \(n, label) -> do
|
|
to <- moveTo ctx label (10 + n)
|
|
v <- moved ctx keyA r to
|
|
field "moves" v `shouldBe` Just (J.toJSON n)
|
|
rewrite ctx r failedPhase
|
|
limited <- stored ctx r
|
|
to <- moveTo ctx "alicebakery" 20
|
|
view <- getView ctx limited
|
|
move ctx keyA limited to `shouldReturn` refusedWithView "move_limit" view
|
|
(field "moves" view, field "phase" view) `shouldBe` (Just (J.Number 3), Just "failed")
|
|
stored ctx r `shouldReturn` limited
|
|
|
|
testMoveMeetsOrder :: IO ()
|
|
testMoveMeetsOrder = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
void $ placed ctx dynamisOrder
|
|
toDynamis <- moveTo ctx "dynamis" 10
|
|
move ctx keyA r toDynamis `shouldReturn` refusedWith 409 "order_exists"
|
|
void $ placed ctx (byB aliceOrder) {oLabel = "alicebakers", oCommitment = commitmentOf 4}
|
|
toForeign <- moveTo ctx "alicebakers" 4
|
|
move ctx keyA r toForeign `shouldReturn` refusedWith 409 "order_exists"
|
|
stored ctx r `shouldReturn` r
|
|
|
|
testMoveOrderChecks :: IO ()
|
|
testMoveOrderChecks = withOwner $ \ctx@Ctx {cNode} -> do
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
to <- moveTo ctx "alicebakers" 10
|
|
move ctx keyA r to {ofLength = 5} `shouldReturn` refusedWith 400 "invalid_name"
|
|
move ctx keyA r to {ofLength = 64} `shouldReturn` refusedWith 400 "invalid_name"
|
|
answerFree ctx "alicebakers"
|
|
move ctx keyA r to {ofDuration = ofDuration to + 3601} `shouldReturn` refusedWith 400 "bad_request"
|
|
answerReserved ctx "alicebakers" 1
|
|
move ctx keyA r to `shouldReturn` refusedWith 409 "name_taken"
|
|
setFault cNode "eth_call" HttpFailure
|
|
move ctx keyA r to `shouldReturn` refusedWith 503 "name_check_unavailable"
|
|
stored ctx r `shouldReturn` r
|
|
|
|
testMoveLanded :: IO ()
|
|
testMoveLanded = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
minedOrder <- paidIn ctx aliceOrder failedPhase
|
|
(ownedOrder, code) <- codeIn ctx 7 dynamisOrder failedPhase
|
|
notLanded <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 2} failedPhase
|
|
let expiry = seconds orderTime + 2 * 31536000
|
|
forM_ ["alicebakery", "dynamis"] $ \label -> answerExpiry ctx label expiry
|
|
TxHash minedHash <- sentRegister ctx minedOrder 1 TSMined
|
|
answerOwner ctx "dynamis" (nrOwner ownedOrder)
|
|
void $ sentRegister ctx notLanded 2 TSReverted
|
|
answerOwner ctx "alicebakery" (nrOwner minedOrder)
|
|
forM_ [(minedOrder, Just minedHash), (ownedOrder, Nothing)] $ \(r, hash) -> do
|
|
poll <- held ctx r
|
|
to <- moveTo ctx "alicebakers" 10
|
|
answer <- move ctx keyA r to
|
|
view <- getView ctx r
|
|
answer `shouldBe` refusedWithView "registration_changed" view
|
|
woken poll `shouldReturn` (200, view)
|
|
fieldsIn view [("phase", "registered"), ("expiresAt", J.toJSON expiry), ("moves", J.Number 0), ("error", J.Null), ("registerTx", maybe J.Null (J.String . hex) hash)]
|
|
written <- stored ctx r
|
|
(nrPhase written, nrLive written, nrVersion written, nrLabelHash written) `shouldBe` (PRegistered, False, nrVersion r + 1, nrLabelHash r)
|
|
pcSpentAt <$> codeRow ctx code `shouldReturn` Just orderTime
|
|
to <- moveTo ctx "alicebakers" 11
|
|
v <- moved ctx keyB notLanded to
|
|
fieldsIn v [("phase", "queued"), ("moves", J.Number 1)]
|
|
|
|
testMoveLandedReleasedCode :: IO ()
|
|
testMoveLandedReleasedCode = withOwner $ \ctx@Ctx {cStore} -> do
|
|
answerFree ctx "alicebakers"
|
|
spender <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 20} id
|
|
(unused, unusedCode) <- codeIn ctx 7 dynamisOrder failedPhase
|
|
(spentElsewhere, spentCode) <- codeIn ctx 8 aliceOrder failedPhase
|
|
let expiry = seconds orderTime + 2 * 31536000
|
|
forM_ [("dynamis", unused), ("alicebakery", spentElsewhere)] $ \(label, r) -> do
|
|
answerExpiry ctx label expiry
|
|
answerOwner ctx label (nrOwner r)
|
|
forM_ [(unused, unusedCode), (spentElsewhere, spentCode)] $ \(r, code) ->
|
|
withTransaction cStore (\db -> releasePrepaidCode db (pcId code) (nrId r)) `shouldReturn` True
|
|
withTransaction cStore $ \db -> do
|
|
reservePrepaidCode db (pcId spentCode) (nrId spender) orderTime `shouldReturn` True
|
|
spendPrepaidCode db (pcId spentCode) (nrId spender) orderTime `shouldReturn` True
|
|
spenderBefore <- stored ctx spender
|
|
spentBefore <- codeRow ctx spentCode
|
|
to <- moveTo ctx "alicebakers" 10
|
|
fst <$> move ctx keyA unused to `shouldReturn` 409
|
|
nrPhase <$> stored ctx unused `shouldReturn` PRegistered
|
|
(pcReservedBy &&& pcSpentAt) <$> codeRow ctx unusedCode `shouldReturn` (Just (nrId unused), Just orderTime)
|
|
to' <- moveTo ctx "alicebakers" 11
|
|
logs <- capturingStderr $ fst <$> move ctx keyA spentElsewhere to' `shouldReturn` 409
|
|
nrPhase <$> stored ctx spentElsewhere `shouldReturn` PRegistered
|
|
length (filter (\l -> "[ERROR " `T.isInfixOf` l && hex (pcCodeHash spentCode) `T.isInfixOf` l && idText (nrId spentElsewhere) `T.isInfixOf` l) $ T.lines logs) `shouldBe` 1
|
|
codeRow ctx spentCode `shouldReturn` spentBefore
|
|
stored ctx spender `shouldReturn` spenderBefore
|
|
where
|
|
idText (RegistrationId t) = t
|
|
|
|
testMoveLandedUnread :: IO ()
|
|
testMoveLandedUnread = withOwner $ \ctx@Ctx {cNode} -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
-- a revert with data is a failed read, not an unowned name
|
|
answerCall cNode (registrarOf cNode) (ownerOfSelector <> labelBytes "alicebakery") $ Reverts $ unhex "37ed32e8"
|
|
to <- moveTo ctx "alicebakers" 10
|
|
move ctx keyA r to `shouldReturn` refusedWith 503 "name_check_unavailable"
|
|
stored ctx r `shouldReturn` r
|
|
|
|
testMovePrice :: IO ()
|
|
testMovePrice = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
cheap <- paidIn ctx aliceOrder failedPhase
|
|
toDynamis <- moveTo ctx "dynamis" 10
|
|
move ctx keyA cheap toDynamis `shouldReturn` (409, Just $ KM.fromList [("error", "price_too_high"), ("price", J.Number 20000)])
|
|
stored ctx cheap `shouldReturn` cheap
|
|
balanceOf ctx cheap `shouldReturn` Nothing
|
|
dear <- paidIn ctx (byB dynamisOrder) failedPhase
|
|
toCheaper <- moveTo ctx "alicebakers" 11
|
|
v <- moved ctx keyB dear toCheaper
|
|
(field "price" v, field "phase" v, field "fromBalance" v) `shouldBe` (Just (J.Number 10000), Just "queued", Just (J.Number 0))
|
|
field "balance" v `shouldBe` Just (J.object ["amount" J..= (10000 :: Int), "held" J..= False, "heldUntil" J..= J.Null, "refunded" J..= (0 :: Int)])
|
|
nrPrice <$> stored ctx dear `shouldReturn` 10000
|
|
balanceOf ctx dear `shouldReturn` Just (10000, 10000)
|
|
|
|
testCheaperMoveLostRace :: IO ()
|
|
testCheaperMoveLostRace = withOwner $ \ctx@Ctx {cNode} -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx dynamisOrder failedPhase
|
|
to <- moveTo ctx "alicebakers" 10
|
|
-- held at the register check's ownerOf read, inside the turns
|
|
(moving, gate) <- pausedAt cNode "eth_call" $ move ctx keyA r to
|
|
changed <- rewrite ctx r id
|
|
releaseGate gate
|
|
fst <$> within "the move" (wait moving) `shouldReturn` 409
|
|
stored ctx r `shouldReturn` changed
|
|
balanceOf ctx r `shouldReturn` Nothing
|
|
|
|
-- | dynamis for 20000, 5000 of it from another order's balance, moved to alicebakers for 10000.
|
|
testMoveKeepsFromBalance :: IO ()
|
|
testMoveKeepsFromBalance = withOwner $ \ctx@Ctx {cStore} -> do
|
|
answerFree ctx "alicebakers"
|
|
paying <- paidIn ctx (byB aliceOrder) failedPhase
|
|
withTransaction cStore $ \db -> creditBalance db (nrId paying) 5000 orderTime
|
|
let fields = OrderFields {ofLabelHash = labelHash "dynamis", ofLength = 7, ofDuration = yearsDuration 2 orderTime, ofCommitment = commitmentOf 3, ofSealed = "sealed-dynamis-3", ofAgreedTerms = termsVersion}
|
|
new = NewRegistration {nrgId = RegistrationId "diff", nrgOwner = ownerA, nrgOrder = fields, nrgYears = 2}
|
|
withTransaction cStore $ \db -> do
|
|
insertInvoiceRow db "inv-diff" (addUTCTime 3600 orderTime) "open"
|
|
createPaidRegistration db new (OTBalance (nrId paying) 20000 5000 (Just btc)) (paidOrder "inv-diff") {poPrice = 20000} orderTime `shouldReturn` Right ()
|
|
settleNow ctx (InvoiceId "inv-diff") settled `shouldReturn` Right ISPaid
|
|
r <- registrationById ctx (RegistrationId "diff") >>= \d -> rewrite ctx d failedPhase
|
|
to <- moveTo ctx "alicebakers" 11
|
|
v <- moved ctx keyA r to
|
|
(field "price" v, field "fromBalance" v) `shouldBe` (Just (J.Number 10000), Just (J.Number 5000))
|
|
balanceOf ctx r `shouldReturn` Just (10000, 10000)
|
|
balanceOf ctx paying `shouldReturn` Just (0, 5000)
|
|
|
|
testMoveCodeOrder :: IO ()
|
|
testMoveCodeOrder = withOwner $ \ctx@Ctx {cStore} -> do
|
|
answerFree ctx "alicebakers"
|
|
(r, code@PrepaidCode {pcCodeHash}) <- codeIn ctx 8 aliceOrder failedPhase
|
|
toShorter <- moveTo ctx "dynamis" 10
|
|
move ctx keyA r toShorter `shouldReturn` refusedWith 409 "code_not_covering"
|
|
stored ctx r `shouldReturn` r
|
|
withTransaction cStore (\db -> revokePrepaidCode db pcCodeHash orderTime) `shouldReturn` Revoked (pcId code)
|
|
withTransaction cStore $ \db -> DB.execute db "UPDATE sx_badge_service_name_prepaid_codes SET expires_at = ? WHERE code_hash = ?" (orderTime, DB.Binary pcCodeHash)
|
|
to <- moveTo ctx "alicebakers" 11
|
|
v <- moved ctx keyA r to
|
|
(field "phase" v, field "price" v) `shouldBe` (Just "queued", Just (J.Number 0))
|
|
(pcReservedBy &&& pcSpentAt) <$> codeRow ctx code `shouldReturn` (Just (nrId r), Nothing)
|
|
|
|
testMoveReleasedCode :: IO ()
|
|
testMoveReleasedCode = withOwner $ \ctx@Ctx {cStore} -> do
|
|
answerFree ctx "alicebakers"
|
|
spender <- paidIn ctx (byB aliceOrder) {oCommitment = commitmentOf 20} id
|
|
holder <- paidIn ctx (byB dynamisOrder) {oCommitment = commitmentOf 21} id
|
|
let refusals :: [(Text, PrepaidCode -> DB.Connection -> IO (), (Int, Maybe J.Object))]
|
|
refusals =
|
|
[ ("spent", \PrepaidCode {pcId} db -> reservePrepaidCode db pcId (nrId spender) orderTime >> void (spendPrepaidCode db pcId (nrId spender) orderTime), refusedWith 409 "code_used"),
|
|
("held", \PrepaidCode {pcId} db -> void $ reservePrepaidCode db pcId (nrId holder) orderTime, refusedWith 409 "code_reserved"),
|
|
("revoked", \PrepaidCode {pcCodeHash} db -> void $ revokePrepaidCode db pcCodeHash orderTime, refusedWith 400 "code_invalid"),
|
|
("expired", \PrepaidCode {pcCodeHash} db -> DB.execute db "UPDATE sx_badge_service_name_prepaid_codes SET expires_at = ? WHERE code_hash = ?" (orderTime, DB.Binary pcCodeHash), refusedWith 409 "code_expired")
|
|
]
|
|
forM_ (zip [1 :: Int ..] refusals) $ \(n, (what, change, refusal)) -> do
|
|
let label = "codeorder" <> tshow n
|
|
answerFree ctx label
|
|
(r, code) <- codeIn ctx 8 aliceOrder {oLabel = label, oCommitment = commitmentOf (30 + n)} failedPhase
|
|
withTransaction cStore (\db -> releasePrepaidCode db (pcId code) (nrId r)) `shouldReturn` True
|
|
withTransaction cStore $ change code
|
|
to <- moveTo ctx "alicebakers" (40 + n)
|
|
(what,) <$> move ctx keyA r to `shouldReturn` (what, refusal)
|
|
stored ctx r `shouldReturn` r
|
|
(r, code) <- codeIn ctx 8 aliceOrder failedPhase
|
|
withTransaction cStore (\db -> releasePrepaidCode db (pcId code) (nrId r)) `shouldReturn` True
|
|
to <- moveTo ctx "alicebakers" 50
|
|
void $ moved ctx keyA r to
|
|
pcReservedBy <$> codeRow ctx code `shouldReturn` Just (nrId r)
|
|
|
|
testMoveSizes :: IO ()
|
|
testMoveSizes = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
to <- moveTo ctx "alicebakers" 10
|
|
let longest = to {ofSealed = T.replicate 11095 "s"}
|
|
body <- moveBody ctx keyA (nrId r) longest
|
|
postMove ctx (nrId r) (KM.insert "owner" (J.String $ T.replicate 5300 "0") body) `shouldReturn` refusedWith 400 "bad_request"
|
|
move ctx keyA r to {ofSealed = T.replicate 11096 "s"} `shouldReturn` refusedWith 400 "bad_request"
|
|
valid <- moveBody ctx keyA (nrId r) to
|
|
forM_ [KM.insert "labelHash" (J.String $ T.toUpper $ hex $ (\(LabelHash h) -> h) $ ofLabelHash to) valid, KM.delete "signature" valid, KM.insert "length" "11" valid, KM.delete "terms" valid] $ \malformed ->
|
|
postMove ctx (nrId r) malformed `shouldReturn` refusedWith 400 "bad_request"
|
|
forM_ malformedTerms $ \t -> move ctx keyA r to {ofAgreedTerms = t} `shouldReturn` refusedWith 400 "bad_request"
|
|
post ctx (registrationPath (nrId r) <> "/name") "[]" `shouldReturn` refusedWith 400 "bad_request"
|
|
stored ctx r `shouldReturn` r
|
|
-- an owner in the body is ignored
|
|
(status, v) <- postMove ctx (nrId r) (KM.insert "owner" (J.String ownerBText) body)
|
|
(status, field "sealed" =<< v, field "owner" =<< v) `shouldBe` (200, Just (J.String $ ofSealed longest), Just (J.String ownerAText))
|
|
|
|
testMoveRefusedSigner :: IO ()
|
|
testMoveRefusedSigner = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
to <- moveTo ctx "alicebakers" 10
|
|
move ctx keyB r to `shouldReturn` refusedWith 400 "bad_request"
|
|
ts <- clockSeconds ctx
|
|
(moveBodyAt ctx keyA (nrId r) to (ts - signedWindow - 1) >>= postMove ctx (nrId r)) `shouldReturn` refusedWith 400 "bad_request"
|
|
stored ctx r `shouldReturn` r
|
|
let unknown = RegistrationId "t2Jk8RmQ4vXw9NcP3yLb6g"
|
|
(moveBody ctx keyA unknown to >>= postMove ctx unknown) `shouldReturn` refusedWith 404 "not_found"
|
|
|
|
testMoveLostRace :: IO ()
|
|
testMoveLostRace = withOwner $ \ctx@Ctx {cNode} -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
to <- moveTo ctx "alicebakers" 10
|
|
-- held at the register check's ownerOf read, inside the turns
|
|
(moving, gate) <- pausedAt cNode "eth_call" $ move ctx keyA r to
|
|
changed <- rewrite ctx r id
|
|
releaseGate gate
|
|
answer <- within "the move" $ wait moving
|
|
view <- getView ctx changed
|
|
answer `shouldBe` refusedWithView "registration_changed" view
|
|
field "version" view `shouldBe` Just (J.toJSON $ nrVersion r + 1)
|
|
stored ctx r `shouldReturn` changed
|
|
|
|
-- | Each round holds the higher hash's turn while both orders start, the one moving from the lower hash first in odd
|
|
-- rounds and last in even ones, so that without the hash order some round has each order holding one turn and waiting
|
|
-- for the other's.
|
|
testMovesCross :: IO ()
|
|
testMovesCross = withOwnerAs (\cfg -> cfg {nMaxOrderMoves = 100}) $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
let (low, high) = case sortOn hashOf ["alicebakery", "alicebakers"] of
|
|
[l, h] -> (l, h)
|
|
_ -> error "two names"
|
|
a <- paidIn ctx aliceOrder {oLabel = low, oCommitment = commitmentOf 60} failedPhase
|
|
b <- paidIn ctx (byB aliceOrder) {oLabel = high, oCommitment = commitmentOf 61} failedPhase
|
|
forM_ [1 .. 6 :: Int] $ \n -> do
|
|
rs <- mapM (stored ctx) [a, b]
|
|
let onName label = fromJust $ find ((== labelHash label) . nrLabelHash) rs
|
|
keyOf r = if nrOwner r == nrOwner a then keyA else keyB
|
|
bodyTo r label k = moveTo ctx label (100 + 2 * n + k) >>= moveBody ctx (keyOf r) (nrId r)
|
|
(fromLow, fromHigh) = (onName low, onName high)
|
|
lowMove <- (fromLow,) <$> bodyTo fromLow high 0
|
|
highMove <- (fromHigh,) <$> bodyTo fromHigh low 1
|
|
holding <- newEmptyMVar
|
|
release <- newEmptyMVar
|
|
holder <- async $ withLabelTurn (oeTurns $ cOrders ctx) (labelHash high) $ putMVar holding () >> takeMVar release
|
|
takeMVar holding
|
|
let started = if odd n then [lowMove, highMove] else [highMove, lowMove]
|
|
moves <- forM started $ \(r, body) -> async (postMove ctx (nrId r) body) <* threadDelay parkDelay
|
|
putMVar release ()
|
|
wait holder
|
|
outcomes <- within ("round " <> show n) $ mapM wait moves
|
|
map fst outcomes `shouldBe` [200, 200]
|
|
forM_ [a, b] $ \r -> rewrite ctx r failedPhase
|
|
where
|
|
hashOf label = (\(LabelHash h) -> h) (labelHash label)
|
|
|
|
testPollsWoken :: IO ()
|
|
testPollsWoken = withOwner $ \ctx -> do
|
|
answerFree ctx "alicebakers"
|
|
r <- paidIn ctx aliceOrder failedPhase
|
|
poll <- held ctx r
|
|
to <- moveTo ctx "alicebakers" 10
|
|
void $ moved ctx keyA r to
|
|
(status, v) <- woken poll
|
|
(status, field "version" v, field "phase" v) `shouldBe` (200, Just (J.toJSON $ nrVersion r + 1), Just "queued")
|