mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 10:47:32 +00:00
store: merge long polls
This commit is contained in:
commit
7786b4c6e4
10 files changed
+543
-26
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module StoreService.Names.Phases (Phases, RegistrationId (..), newPhases, publish, awaitChange, phasesCount) where
|
||||
module StoreService.Names.Phases (Phases, RegistrationId (..), newPhases, publish, awaitChange, phasesCount, phasesWaiting) where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (bracket)
|
||||
@@ -29,6 +29,10 @@ publish (Phases watches) rid version =
|
||||
phasesCount :: Phases -> IO Int
|
||||
phasesCount (Phases watches) = Map.size <$> readTVarIO watches
|
||||
|
||||
-- | The waiters across all registrations.
|
||||
phasesWaiting :: Phases -> STM Int
|
||||
phasesWaiting (Phases watches) = readTVar watches >>= fmap sum . mapM (\Watch {wRefs} -> readTVar wRefs) . Map.elems
|
||||
|
||||
subscribe :: Phases -> RegistrationId -> STM Watch
|
||||
subscribe (Phases watches) rid =
|
||||
readTVar watches >>= \ws -> case Map.lookup rid ws of
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module StoreService.Orders
|
||||
( RegistrationChanged (..),
|
||||
@@ -88,7 +89,7 @@ data Product = PBadge (Maybe ByteString) | PName NameRegistration
|
||||
data NameOutcome = NOWritten RegistrationId Int64 | NOLate RegistrationId
|
||||
|
||||
settleOrder :: DBStore -> Waiters -> Maybe Phases -> InvoiceId -> PaymentSignal -> UTCTime -> IO (Either Text InvoiceStatus)
|
||||
settleOrder st waiters phases invId@(InvoiceId invText) signal now' = do
|
||||
settleOrder st waiters phases invId signal now' = do
|
||||
outcome <- withTransaction st $ \db ->
|
||||
settlementInvoice db invId >>= \case
|
||||
Just InvoiceRow {irStatus}
|
||||
@@ -111,7 +112,7 @@ settleOrder st waiters phases invId@(InvoiceId invText) signal now' = do
|
||||
NOWritten rid v -> publishRegistration phases (rid, v)
|
||||
NOLate _ -> pure ()
|
||||
forM_ name $ \case
|
||||
NOLate (RegistrationId rid) -> logError ("names: invoice " <> invText <> " was paid after its order closed, so registration " <> rid <> " stays unpaid; refund the payment")
|
||||
NOLate rid -> latePayment invId rid
|
||||
NOWritten {} -> pure ()
|
||||
pure (Right status)
|
||||
where
|
||||
@@ -199,14 +200,30 @@ closeInvoiceOrder db invId now =
|
||||
Just InvoiceRow {irProduct = IPName rid} -> getRegistration db rid >>= maybe (pure Nothing) (\r -> closeOrder db r now)
|
||||
_ -> pure Nothing
|
||||
|
||||
latePayment :: InvoiceId -> RegistrationId -> IO ()
|
||||
latePayment (InvoiceId invText) (RegistrationId rid) =
|
||||
logError ("names: invoice " <> invText <> " was paid after its order closed, so registration " <> rid <> " stays unpaid; refund the payment")
|
||||
|
||||
-- | False when the invoice was no longer open. Throws RegistrationChanged, changing nothing, when its order changed meanwhile.
|
||||
-- The provider has taken the cancel, so a name invoice a payment landed on meanwhile expires too, and its payment is late.
|
||||
cancelOpenInvoice :: DBStore -> Waiters -> Maybe Phases -> InvoiceId -> UTCTime -> IO Bool
|
||||
cancelOpenInvoice st waiters phases invId now = do
|
||||
closed <- withTransaction st $ \db -> do
|
||||
cancelled <- updateInvoiceStatus db invId ISOpen ISExpired now
|
||||
if cancelled then Just <$> closeInvoiceOrder db invId now else pure Nothing
|
||||
forM_ closed $ \order -> atomically $ publish waiters invId ISExpired >> mapM_ (publishRegistration phases) order
|
||||
-- On Postgres, a settlement holding the row is waited for, and the read below sees its payment.
|
||||
lockInvoice db invId now
|
||||
settlementInvoice db invId >>= \case
|
||||
Just row@InvoiceRow {irStatus = ISOpen} -> do
|
||||
cancelled <- updateInvoiceStatus db invId ISOpen ISExpired now
|
||||
if cancelled then Just . (,lateOf row) <$> closeInvoiceOrder db invId now else pure Nothing
|
||||
_ -> pure Nothing
|
||||
forM_ closed $ \(order, late) -> do
|
||||
atomically $ publish waiters invId ISExpired >> mapM_ (publishRegistration phases) order
|
||||
mapM_ (latePayment invId) late
|
||||
pure (isJust closed)
|
||||
where
|
||||
lateOf InvoiceRow {irProduct, irPayment} = case irProduct of
|
||||
IPName rid | maybe False paymentHolds irPayment -> Just rid
|
||||
_ -> Nothing
|
||||
|
||||
-- | Of the given invoices, expires those still open, with no money received, and past the cutoff, closing their orders.
|
||||
-- Each in its own transaction, so an invoice that fails is logged and skipped, and the others still expire.
|
||||
|
||||
@@ -9,10 +9,11 @@ import Network.HTTP.Types (Method)
|
||||
import Network.Wai (Request, ResponseReceived, pathInfo)
|
||||
import StoreService.Names.Env (NamesEnv (..))
|
||||
import StoreService.Names.Orders (ordersEnv)
|
||||
import StoreService.Web.Http (HttpEnv, Limit, Respond, codeLimit, limited, only, previewLimit, recordsLimit, searchLimit)
|
||||
import StoreService.Web.Http (HttpEnv, Limit, Respond, codeLimit, holdMicros, limited, only, previewLimit, readLimit, recordsLimit, searchLimit)
|
||||
import StoreService.Web.Names.Codes (codeCheckHandler)
|
||||
import StoreService.Web.Names.Orders (codeOrderHandler)
|
||||
import StoreService.Web.Names.Records (recordsHandler)
|
||||
import StoreService.Web.Names.Registration (registrationHandler)
|
||||
import StoreService.Web.Names.Search (previewHandler, searchHandler)
|
||||
|
||||
data Route = Route
|
||||
@@ -29,6 +30,7 @@ namesRoutes env@NamesEnv {neConfig, neStore, neContracts, neProfiles, neRecords,
|
||||
["api", "link", "preview"] -> Just Route {rMethod = "POST", rLimit = previewLimit, rHandler = previewHandler neProfiles}
|
||||
-- below every fixed /api/name/<word> route, which it would otherwise match
|
||||
["api", "name", hash] -> Just Route {rMethod = "GET", rLimit = searchLimit, rHandler = searchHandler neConfig neStore neContracts neProfiles hash}
|
||||
["api", "registration", rid] -> Just Route {rMethod = "GET", rLimit = readLimit, rHandler = registrationHandler (ordersEnv env) holdMicros rid}
|
||||
_ -> Nothing
|
||||
|
||||
-- | Nothing for a path no route knows. The limit is taken only once the route and the method match.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module StoreService.Web.Names.Registration
|
||||
( registrationHandler,
|
||||
registrationAt,
|
||||
registrationVersion,
|
||||
lapseMoment,
|
||||
heldFor,
|
||||
versionParam,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (join)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (isDigit)
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (maybeToList)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime)
|
||||
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
|
||||
import Network.HTTP.Types (status200)
|
||||
import Network.Wai (Request, ResponseReceived, queryString)
|
||||
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction)
|
||||
import StoreService.Names.Orders (OrdersEnv (..))
|
||||
import StoreService.Names.Phases (RegistrationId (..), awaitChange)
|
||||
import StoreService.Names.Store (NamePhase (..), NameRegistration (..), getRegistration)
|
||||
import StoreService.Names.View (registrationView)
|
||||
import StoreService.Web.Http (Respond, jsonResponse, notFound)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
-- | GET /api/registration/:id, held while its version is seenVersion.
|
||||
registrationHandler :: OrdersEnv -> Int -> Text -> Request -> Respond -> IO ResponseReceived
|
||||
registrationHandler OrdersEnv {oeStore, oeConfig, oeContracts, oePhases, oeNow} hold ridText req respond =
|
||||
readNow >>= \case
|
||||
Nothing -> respond notFound
|
||||
Just (now, r@NameRegistration {nrVersion})
|
||||
| versionParam req == Just nrVersion -> do
|
||||
_ <- awaitChange oePhases rid (registrationVersion oeStore rid) nrVersion (heldFor hold now $ maybeToList $ lapseMoment r)
|
||||
readNow >>= maybe (respond notFound) answer
|
||||
| otherwise -> answer (now, r)
|
||||
where
|
||||
rid = RegistrationId ridText
|
||||
readNow = oeNow >>= \now -> fmap (now,) <$> withTransaction oeStore (`getRegistration` rid)
|
||||
answer (now, r) = respond . jsonResponse status200 =<< registrationView oeStore oeConfig oeContracts (registrationAt now r)
|
||||
|
||||
-- | Versions start at 1, so a registration deleted during a hold reads as changed.
|
||||
registrationVersion :: DBStore -> RegistrationId -> IO Int64
|
||||
registrationVersion st rid = maybe 0 nrVersion <$> withTransaction st (`getRegistration` rid)
|
||||
|
||||
notSignedInTime :: Text
|
||||
notSignedInTime = "The name was not signed in time"
|
||||
|
||||
-- | Past revealBefore with no reveal, the commitment can no longer register, so it reads failed.
|
||||
registrationAt :: UTCTime -> NameRegistration -> NameRegistration
|
||||
registrationAt now r = case lapseMoment r of
|
||||
Just lapse | now > lapse -> r {nrPhase = PFailed, nrError = Just notSignedInTime}
|
||||
_ -> r
|
||||
|
||||
lapseMoment :: NameRegistration -> Maybe UTCTime
|
||||
lapseMoment = \case
|
||||
NameRegistration {nrPhase = PCommitted, nrReveal = Nothing, nrRevealBefore = Just before} -> Just $ posixSecondsToUTCTime $ fromIntegral before
|
||||
_ -> Nothing
|
||||
|
||||
-- | The hold, cut to end just past the next moment at which the answer changes with time alone.
|
||||
heldFor :: Int -> UTCTime -> [UTCTime] -> Int
|
||||
heldFor hold now moments = fromInteger $ minimum $ toInteger hold : [ceiling (diffUTCTime moment now * microsPerSecond) + 1 | moment <- moments, moment >= now]
|
||||
where
|
||||
microsPerSecond = 1000000
|
||||
|
||||
-- | Nothing unless seenVersion is a decimal version, so any other value answers at once.
|
||||
versionParam :: Request -> Maybe Int64
|
||||
versionParam req = case join $ lookup "seenVersion" $ queryString req of
|
||||
Just s | not (B.null s) && B.all isDigit s -> readMaybe (B.unpack s) >>= inRange
|
||||
_ -> Nothing
|
||||
where
|
||||
inRange :: Integer -> Maybe Int64
|
||||
inRange v = if v <= toInteger (maxBound :: Int64) then Just (fromInteger v) else Nothing
|
||||
@@ -18,7 +18,8 @@ import StoreService.Catalog (PricedOffer (..), priceOffer)
|
||||
import StoreService.Config (ListenerConfig (..), ServiceConfig (..), StripeConfig (..))
|
||||
import StoreService.Names.Env (NamesEnv)
|
||||
import StoreService.Names.Orders (OrdersEnv (..), ordersEnv)
|
||||
import StoreService.Names.Store (getRegistration, textMethod)
|
||||
import StoreService.Names.Phases (RegistrationId, awaitChange)
|
||||
import StoreService.Names.Store (NamePhase (..), NameRegistration (..), getRegistration, textMethod)
|
||||
import StoreService.Orders (RegistrationChanged (..), cancelOpenInvoice)
|
||||
import StoreService.Poller (ReadHints, queueReadHint)
|
||||
import StoreService.Providers (OrderDraft (..), Provider (..), ProviderError (..), ProviderInvoice (..), WebhookError (..))
|
||||
@@ -27,9 +28,11 @@ import StoreService.Waiters (Seen, Waiters, awaitStatus)
|
||||
import StoreService.Web.Http
|
||||
import StoreService.Web.Names (namesApp, namesRoutes)
|
||||
import StoreService.Web.Names.Orders (nameInvoiceHandler, nameInvoiceView, registrationChangedResponse)
|
||||
import StoreService.Web.Names.Registration (heldFor, lapseMoment, registrationAt, registrationVersion, versionParam)
|
||||
import Control.Concurrent.Async (race)
|
||||
import qualified Control.Exception as E
|
||||
import StoreService.Log (logError, logInfo, logWarn)
|
||||
import Control.Monad (forM_, when)
|
||||
import Control.Monad (forM_, void, when)
|
||||
import Data.Aeson ((.=))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
@@ -38,7 +41,7 @@ import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Char (toLower)
|
||||
import Data.List (isPrefixOf)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Maybe (catMaybes, fromMaybe)
|
||||
import Data.String (fromString)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -221,38 +224,91 @@ readInvoiceHandler :: WebEnv -> InvoiceId -> Request -> Respond -> IO ResponseRe
|
||||
readInvoiceHandler env@WebEnv {weStore} invId req respond =
|
||||
getInvoice weStore invId >>= \case
|
||||
Nothing -> respond notFound
|
||||
Just row@InvoiceRow {irProduct = IPName rid} | Just orders <- weOrders env -> readNameInvoice env orders row rid req respond
|
||||
Just row -> case holdFor row of
|
||||
Nothing -> respondInvoice env row respond
|
||||
Just seen -> do
|
||||
-- Settlement publishes after it commits, so the row read below is at least as new as whatever woke us.
|
||||
_ <- awaitStatus (weWaiters env) invId readSeen seen (weHoldMicros env)
|
||||
getInvoice weStore invId >>= maybe (respond notFound) (\row' -> respondInvoice env row' respond)
|
||||
_ <- awaitStatus (weWaiters env) invId (readSeen env invId (pure id)) seen (weHoldMicros env)
|
||||
respondCurrent env invId respond
|
||||
where
|
||||
holdFor :: InvoiceRow -> Maybe Seen
|
||||
holdFor row@InvoiceRow {irStatus} = case waitParam req of
|
||||
Just seen | seen == irStatus, seen /= ISPaid, samePayment -> Just (irStatus, paymentMark row)
|
||||
Just seen | seen == irStatus, seen /= ISPaid, samePayment req row -> Just (seenOf row)
|
||||
_ -> Nothing
|
||||
where
|
||||
-- The counter a hold watches starts at zero, so a payment recorded before this request arrived cannot wake it.
|
||||
samePayment = case paidParam req of
|
||||
Nothing -> True
|
||||
Just seen -> seen == paymentMark row
|
||||
readSeen :: IO Seen
|
||||
readSeen = maybe (ISOpen, ("", False)) (\row -> (irStatus row, paymentMark row)) <$> getInvoice weStore invId
|
||||
|
||||
-- | A name invoice also holds while its registration's version is seenVersion, a paid one on that alone,
|
||||
-- except one paid after its order closed, which has nothing more to wait for.
|
||||
readNameInvoice :: WebEnv -> OrdersEnv -> InvoiceRow -> RegistrationId -> Request -> Respond -> IO ResponseReceived
|
||||
readNameInvoice env@WebEnv {weConfig, weWaiters, weHoldMicros} orders@OrdersEnv {oeStore, oeNow, oePhases} stored@InvoiceRow {irInvoiceId = invId} rid req respond =
|
||||
readName orders stored rid >>= \case
|
||||
Nothing -> respond notFound
|
||||
Just (now, row, r) -> case waitParam req of
|
||||
Just wait | holds wait row r -> hold now row r >> respondCurrent env invId respond
|
||||
_ -> respond . jsonResponse status200 =<< nameInvoiceView orders (confirmationsFor weConfig) row r
|
||||
where
|
||||
seenVersion = versionParam req
|
||||
holds wait row@InvoiceRow {irStatus} NameRegistration {nrPhase, nrVersion} =
|
||||
wait == irStatus
|
||||
&& not (irStatus == ISPaid && nrPhase == PUnpaid)
|
||||
&& samePayment req row
|
||||
&& maybe (irStatus /= ISPaid) (== nrVersion) seenVersion
|
||||
hold now row r = do
|
||||
let micros = heldFor weHoldMicros now $ catMaybes [expiryMoment row, lapseMoment r]
|
||||
onStatus = awaitStatus weWaiters invId (readSeen env invId (invoiceAt <$> oeNow)) (seenOf row) micros
|
||||
onVersion v = awaitChange oePhases rid (registrationVersion oeStore rid) v micros
|
||||
case (irStatus row, seenVersion) of
|
||||
(ISPaid, Just v) -> void $ onVersion v
|
||||
(_, Just v) -> void $ race onStatus (onVersion v)
|
||||
(_, Nothing) -> void onStatus
|
||||
|
||||
-- | The invoice and its registration as a read now shows them.
|
||||
readName :: OrdersEnv -> InvoiceRow -> RegistrationId -> IO (Maybe (UTCTime, InvoiceRow, NameRegistration))
|
||||
readName OrdersEnv {oeStore, oeNow} row rid = do
|
||||
now <- oeNow
|
||||
fmap (\r -> (now, invoiceAt now row, registrationAt now r)) <$> withTransaction oeStore (`getRegistration` rid)
|
||||
|
||||
-- | An open name invoice past its expiry with no money received reads expired before the sweep marks it.
|
||||
invoiceAt :: UTCTime -> InvoiceRow -> InvoiceRow
|
||||
invoiceAt now row = case expiryMoment row of
|
||||
Just expiry | now > expiry -> row {irStatus = ISExpired}
|
||||
_ -> row
|
||||
|
||||
-- | A funded open invoice stays open until it settles.
|
||||
expiryMoment :: InvoiceRow -> Maybe UTCTime
|
||||
expiryMoment InvoiceRow {irStatus, irExpiresAt, irPayment}
|
||||
| irStatus == ISOpen && not (maybe False paymentHolds irPayment) = Just irExpiresAt
|
||||
| otherwise = Nothing
|
||||
|
||||
-- | The counter a hold watches starts at zero, so a payment recorded before this request arrived cannot wake it.
|
||||
samePayment :: Request -> InvoiceRow -> Bool
|
||||
samePayment req row = maybe True (== paymentMark row) (paidParam req)
|
||||
|
||||
seenOf :: InvoiceRow -> Seen
|
||||
seenOf row@InvoiceRow {irStatus} = (irStatus, paymentMark row)
|
||||
|
||||
-- | What a hold compares, read as the answer would show it.
|
||||
readSeen :: WebEnv -> InvoiceId -> IO (InvoiceRow -> InvoiceRow) -> IO Seen
|
||||
readSeen WebEnv {weStore} invId reading = do
|
||||
readAs <- reading
|
||||
maybe (ISOpen, ("", False)) (seenOf . readAs) <$> getInvoice weStore invId
|
||||
|
||||
respondCurrent :: WebEnv -> InvoiceId -> Respond -> IO ResponseReceived
|
||||
respondCurrent env@WebEnv {weStore} invId respond = getInvoice weStore invId >>= maybe (respond notFound) (\row -> respondInvoice env row respond)
|
||||
|
||||
respondInvoice :: WebEnv -> InvoiceRow -> Respond -> IO ResponseReceived
|
||||
respondInvoice env row respond = invoiceViewOf env row >>= respond . either id (jsonResponse status200)
|
||||
|
||||
-- | A badge's view, or a name's with its registration.
|
||||
invoiceViewOf :: WebEnv -> InvoiceRow -> IO (Either Response J.Value)
|
||||
invoiceViewOf env@WebEnv {weConfig, weStore} row@InvoiceRow {irInvoiceId = InvoiceId invId, irProduct} = case irProduct of
|
||||
invoiceViewOf env@WebEnv {weConfig} row@InvoiceRow {irInvoiceId = InvoiceId invId, irProduct} = case irProduct of
|
||||
IPBadge badgeType months -> pure $ Right $ badgeInvoiceView (confirmationsFor weConfig) badgeType months row
|
||||
IPName rid -> case weOrders env of
|
||||
Nothing -> Left providerUnavailable <$ namesOff invId
|
||||
Just orders ->
|
||||
withTransaction weStore (`getRegistration` rid) >>= \case
|
||||
readName orders row rid >>= \case
|
||||
Nothing -> pure $ Left notFound
|
||||
Just r -> Right <$> nameInvoiceView orders (confirmationsFor weConfig) row r
|
||||
Just (_, row', r) -> Right <$> nameInvoiceView orders (confirmationsFor weConfig) row' r
|
||||
|
||||
namesOff :: Text -> IO ()
|
||||
namesOff invId = logError ("invoice " <> invId <> " is a name's, and this service sells no names")
|
||||
@@ -285,7 +341,7 @@ cancelInvoiceHandler env@WebEnv {weStore, weProviders} invId@(InvoiceId invText)
|
||||
now <- getCurrentTime
|
||||
E.try (cancelOpenInvoice weStore (weWaiters env) (oePhases <$> weOrders env) invId now) >>= \case
|
||||
Left (RegistrationChanged rid) -> maybe (pure providerUnavailable) (`registrationChangedResponse` rid) (weOrders env) >>= respond
|
||||
Right _ -> getInvoice weStore invId >>= maybe (respond notFound) (\row' -> respondInvoice env row' respond)
|
||||
Right _ -> respondCurrent env invId respond
|
||||
|
||||
paidParam :: Request -> Maybe (Text, Bool)
|
||||
paidParam req = case lookup "seenPaid" (queryString req) of
|
||||
|
||||
@@ -476,6 +476,7 @@ executable simplex-store-service
|
||||
StoreService.Web.Names.Codes
|
||||
StoreService.Web.Names.Orders
|
||||
StoreService.Web.Names.Records
|
||||
StoreService.Web.Names.Registration
|
||||
StoreService.Web.Names.Search
|
||||
StoreService.Web.Server
|
||||
Paths_simplex_chat
|
||||
@@ -769,6 +770,7 @@ test-suite simplex-chat-test
|
||||
Bots.StoreService.NamesLabelTests
|
||||
Bots.StoreService.NamesLinksTests
|
||||
Bots.StoreService.NamesOrdersTests
|
||||
Bots.StoreService.NamesPhasesTests
|
||||
Bots.StoreService.NamesProfilesTests
|
||||
Bots.StoreService.NamesRecordsTests
|
||||
Bots.StoreService.NamesSearchTests
|
||||
@@ -831,6 +833,7 @@ test-suite simplex-chat-test
|
||||
StoreService.Web.Names.Codes
|
||||
StoreService.Web.Names.Orders
|
||||
StoreService.Web.Names.Records
|
||||
StoreService.Web.Names.Registration
|
||||
StoreService.Web.Names.Search
|
||||
StoreService.Web.Server
|
||||
Paths_simplex_chat
|
||||
|
||||
@@ -2,7 +2,23 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Bots.StoreService.NamesOrdersTests (namesOrdersTests) where
|
||||
module Bots.StoreService.NamesOrdersTests
|
||||
( namesOrdersTests,
|
||||
Ctx (..),
|
||||
Order (..),
|
||||
withOrders,
|
||||
withOrdersCancelling,
|
||||
aliceOrder,
|
||||
commitmentOf,
|
||||
orderTime,
|
||||
placed,
|
||||
post,
|
||||
settleNow,
|
||||
settled,
|
||||
invoiceRegistration,
|
||||
registrationOf,
|
||||
)
|
||||
where
|
||||
|
||||
import Bots.StoreService.FakeBTCPay (FakeBTCPay (..), setInvoiceState, withFakeBTCPay)
|
||||
import Bots.StoreService.FakeEthNode
|
||||
@@ -123,6 +139,7 @@ namesOrdersTests = describe "StoreService names: orders" $ do
|
||||
it "alerts on a late payment as BTCPay reports it for an expired, cancelled or swept invoice, once" testLateBTCPayPayments
|
||||
it "alerts once on money read while open when the provider then closes the invoice, also after a restart" testOneLateAlert
|
||||
it "answers cancel with funded, not_open, paid when settlement won, and expired with the payment read meanwhile" testCancelAnswers
|
||||
it "alerts once when a cancel expires an invoice a payment landed on while the provider was told" testCancelAlertsFunded
|
||||
it "leaves a late payment of a cancelled invoice unpaid and alerts" testLatePayment
|
||||
it "queues an order settled while open and publishes its version" testSettlementQueues
|
||||
it "takes the invoice's row before recording a payment that moves no status" testSettlementLocksInvoice
|
||||
@@ -1067,6 +1084,21 @@ testCancelAnswers = withOrders $ \ctx -> do
|
||||
where
|
||||
cancelAt c (InvoiceId t) = post c ("/api/invoice/" <> T.unpack t <> "/cancel") ""
|
||||
|
||||
-- | Cancel takes the invoice row and decides on the payment it then reads, as settlement does.
|
||||
testCancelAlertsFunded :: IO ()
|
||||
testCancelAlertsFunded = withOrdersCancelling (SigFunded paid PaidInPart) $ \ctx -> do
|
||||
(invId, _) <- placed ctx aliceOrder
|
||||
logs <- capturingStderr $ do
|
||||
(status, v) <- post ctx ("/api/invoice/" <> T.unpack (invoiceText invId) <> "/cancel") ""
|
||||
(status, field "status" =<< v, field "cryptoAmountPaid" =<< v) `shouldBe` (200, Just "expired", Just "0.00078")
|
||||
length (T.breakOnAll (alertOf invId) logs) `shouldBe` 1
|
||||
capturingStderr (settleNow ctx invId (SigClosed paid) `shouldReturn` Right ISExpired) >>= (`shouldNotSatisfy` T.isInfixOf (alertOf invId))
|
||||
(nrPhase &&& nrLive) <$> invoiceRegistration ctx invId `shouldReturn` (PUnpaid, False)
|
||||
where
|
||||
paid = rcv 5000 (Just "0.00078")
|
||||
invoiceText (InvoiceId t) = t
|
||||
alertOf invId = "invoice " <> invoiceText invId <> " was paid after its order closed"
|
||||
|
||||
testNamesOff :: IO ()
|
||||
testNamesOff = withOrders $ \ctx -> do
|
||||
(invId@(InvoiceId t), _) <- placed ctx aliceOrder
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Bots.StoreService.NamesPhasesTests (namesPhasesTests) where
|
||||
|
||||
import Bots.StoreService.NamesOrdersTests
|
||||
import Bots.StoreService.NamesStoreTests (reveal)
|
||||
import Bots.StoreService.WebTests (bounded, jsonObject, rcv, statusOf, webGet)
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (Async, async, cancel, wait, waitCatchSTM)
|
||||
import qualified Control.Concurrent.Async as Async
|
||||
import Control.Concurrent.STM (STM, atomically, check, orElse)
|
||||
import Control.Monad (forM_)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Key as K
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import Data.IORef (readIORef, writeIORef)
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (fromJust, fromMaybe, isNothing)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime)
|
||||
import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)
|
||||
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 StoreService.Names.Orders (OrdersEnv (..))
|
||||
import StoreService.Names.Phases (RegistrationId (..), phasesWaiting, publish)
|
||||
import StoreService.Names.Store (NamePhase (..), NameRegistration (..), VersionWrite (..), getRegistration, updateRegistration)
|
||||
import StoreService.Providers (Funded (..), PaymentSignal (..))
|
||||
import StoreService.Store.Invoices (InvoiceRow (..), getInvoice)
|
||||
import StoreService.Waiters (publishPayment, waitingCountSTM)
|
||||
import StoreService.Web.Http (holdMicros, readLimit)
|
||||
import StoreService.Web.Names (Route (..), namesApp)
|
||||
import StoreService.Web.Names.Registration (registrationHandler)
|
||||
import StoreService.Web.Server (WebEnv (..), webApp)
|
||||
import System.Timeout (timeout)
|
||||
import Test.Hspec
|
||||
|
||||
namesPhasesTests :: Spec
|
||||
namesPhasesTests = describe "StoreService names: phases" $ do
|
||||
describe "changes" $ do
|
||||
it "wakes a held invoice poll and a held registration poll on each change: phase, label set and cleared, reveal times, error" testEachChangeWakes
|
||||
it "holds a paid name invoice on its registration's version alone, and answers at once without a valid version" testPaidHoldsOnVersion
|
||||
it "answers at once an invoice paid after its order closed" testLatePaidAnswersAtOnce
|
||||
it "wakes an open invoice poll by a version change, and holds it on its status without a valid version" testOpenWakesOnVersion
|
||||
describe "time" $ do
|
||||
it "wakes an unpaid invoice held on its status at its expiry, reading it expired" testExpiryWakes
|
||||
it "reads a funded open invoice past its expiry as open, and holds it" testFundedStaysOpen
|
||||
it "reads a committed registration with no reveal past revealBefore as failed, waking held registration and invoice polls" testLapseReadsFailed
|
||||
describe "registration poll" $ do
|
||||
it "answers at once without seenVersion, with another version, or with one that is not a number" testAnswersAtOnce
|
||||
it "answers unchanged after the hold" testHoldTimesOut
|
||||
it "answers an unknown registration 404" testUnknownRegistration
|
||||
it "leaves waitingCount to polls waiting on a payment" testWaitingCount
|
||||
|
||||
data Polls = Polls
|
||||
{ pCtx :: Ctx,
|
||||
-- | Sets the service's clock, which then runs on in real time, so a hold can end at a moment.
|
||||
pSetNow :: UTCTime -> IO ()
|
||||
}
|
||||
|
||||
-- | The registration route as namesRoutes registers it, beside the invoice routes, since a NamesEnv needs a running chat controller.
|
||||
withPolls :: Int -> (Polls -> IO a) -> IO a
|
||||
withPolls hold action = withOrders $ \ctx@Ctx {cClock, cOrders, cWeb} -> do
|
||||
started <- getCurrentTime
|
||||
let elapsed = (`diffUTCTime` started) <$> getCurrentTime
|
||||
running = addUTCTime <$> elapsed <*> readIORef cClock
|
||||
orders = cOrders {oeNow = running}
|
||||
web = cWeb {weOrders = Just orders}
|
||||
routes = \case
|
||||
["api", "registration", rid] -> Just Route {rMethod = "GET", rLimit = readLimit, rHandler = registrationHandler orders hold rid}
|
||||
_ -> Nothing
|
||||
app req respond = fromMaybe (webApp web req respond) (namesApp (weHttp web) routes req respond)
|
||||
pSetNow t = elapsed >>= \e -> writeIORef cClock (addUTCTime (negate e) t)
|
||||
mgr <- newManager defaultManagerSettings
|
||||
Warp.testWithApplication (pure app) $ \port ->
|
||||
action Polls {pCtx = ctx {cOrders = orders, cWeb = web, cClient = (mgr, "http://127.0.0.1:" <> show port)}, pSetNow}
|
||||
|
||||
-- | How long a held poll is watched for a wake that must not come.
|
||||
parkDelay :: Int
|
||||
parkDelay = 200000
|
||||
|
||||
-- | How long a poll may take to be held or to answer once woken; far below the hold.
|
||||
prompt :: Int
|
||||
prompt = 6000000
|
||||
|
||||
-- | How far ahead of a moment the clock is set, so a poll is held well before the moment wakes it.
|
||||
shortOfMoment :: NominalDiffTime
|
||||
shortOfMoment = 3
|
||||
|
||||
shortHold :: Int
|
||||
shortHold = 500000
|
||||
|
||||
get :: Ctx -> String -> IO (Int, J.Object)
|
||||
get Ctx {cClient} path = webGet cClient path >>= \r -> (statusOf r,) <$> jsonObject r
|
||||
|
||||
-- | The holds the server has taken: invoices held on a payment, and waiters on a registration's version.
|
||||
subscriptions :: Ctx -> STM (Int, Int)
|
||||
subscriptions Ctx {cWeb, cOrders} = (,) <$> waitingCountSTM (weWaiters cWeb) <*> phasesWaiting (oePhases cOrders)
|
||||
|
||||
-- | A poll once the server holds it, having taken these many more holds of each kind.
|
||||
held :: Ctx -> (Int, Int) -> String -> IO (Async (Int, J.Object))
|
||||
held ctx (payments, versions) path = do
|
||||
(payments0, versions0) <- atomically $ subscriptions ctx
|
||||
poll <- async $ get ctx path
|
||||
let holding = subscriptions ctx >>= check . (== (payments0 + payments, versions0 + versions))
|
||||
timeout prompt (atomically $ (Left <$> waitCatchSTM poll) `orElse` (Right <$> holding)) >>= \case
|
||||
Just (Right ()) -> pure poll
|
||||
Just (Left answered) -> fail $ path <> " was answered at once: " <> show answered
|
||||
Nothing -> cancel poll >> fail (path <> " was not held with " <> show (payments, versions) <> " more holds")
|
||||
|
||||
woken :: String -> Async (Int, J.Object) -> IO (Int, J.Object)
|
||||
woken what poll = timeout prompt (wait poll) >>= maybe (cancel poll >> fail (what <> " was not woken")) pure
|
||||
|
||||
answeredAtOnce :: Ctx -> String -> IO (Int, J.Object)
|
||||
answeredAtOnce ctx path = timeout prompt (get ctx path) >>= maybe (fail (path <> " was held")) pure
|
||||
|
||||
invoicePath :: InvoiceId -> String
|
||||
invoicePath (InvoiceId t) = "/api/invoice/" <> T.unpack t
|
||||
|
||||
registrationPath :: RegistrationId -> String
|
||||
registrationPath (RegistrationId t) = "/api/registration/" <> T.unpack t
|
||||
|
||||
-- | The query the page sends to follow an invoice, as it last saw it.
|
||||
following :: InvoiceStatus -> Int64 -> Text -> Bool -> String
|
||||
following status version = followingAs status (Just $ show version)
|
||||
|
||||
-- | With seenVersion as given, a number or not, or left out.
|
||||
followingAs :: InvoiceStatus -> Maybe String -> Text -> Bool -> String
|
||||
followingAs status version paid full =
|
||||
"?wait=" <> statusText <> maybe "" ("&seenVersion=" <>) version <> "&seenPaid=" <> T.unpack paid <> "&seenFull=" <> (if full then "1" else "0")
|
||||
where
|
||||
statusText = case status of
|
||||
ISOpen -> "open"
|
||||
ISPaid -> "paid"
|
||||
ISExpired -> "expired"
|
||||
|
||||
seenVersion :: Int64 -> String
|
||||
seenVersion v = "?seenVersion=" <> show v
|
||||
|
||||
-- | A write as the worker or a reveal makes it: a compare-and-set, then a publish after the commit.
|
||||
change :: Ctx -> RegistrationId -> (NameRegistration -> NameRegistration) -> IO Int64
|
||||
change Ctx {cStore, cOrders, cClock} rid f = do
|
||||
now <- readIORef cClock
|
||||
written <- withTransaction cStore $ \db -> getRegistration db rid >>= \r -> updateRegistration db (f $ fromJust r) now
|
||||
case written of
|
||||
Written v -> v <$ atomically (publish (oePhases cOrders) rid v)
|
||||
other -> fail $ "registration not written: " <> show other
|
||||
|
||||
key :: Text -> J.Object -> Maybe J.Value
|
||||
key k = KM.lookup (K.fromText k)
|
||||
|
||||
seconds :: UTCTime -> Integer
|
||||
seconds = floor . utcTimeToPOSIXSeconds
|
||||
|
||||
-- | The settled payment's marks: its crypto amount and paid in full.
|
||||
settledPaid :: Text
|
||||
settledPaid = "0.00156"
|
||||
|
||||
-- tests
|
||||
|
||||
-- | Paid and queued at version 2; each change below is one the worker, a reveal or a re-commit makes.
|
||||
testEachChangeWakes :: IO ()
|
||||
testEachChangeWakes = bounded "each change wakes" $ withPolls holdMicros $ \Polls {pCtx = ctx} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
settleNow ctx inv settled `shouldReturn` Right ISPaid
|
||||
NameRegistration {nrId} <- invoiceRegistration ctx inv
|
||||
let day = seconds orderTime + 86400
|
||||
changes :: [(String, NameRegistration -> NameRegistration, Text, J.Value)]
|
||||
changes =
|
||||
[ ("phase", \r -> r {nrPhase = PCommitting}, "phase", "committing"),
|
||||
("committed", \r -> r {nrPhase = PCommitted, nrRevealAfter = Just (fromInteger day), nrRevealBefore = Just (fromInteger $ day + 86400)}, "revealBefore", J.toJSON (day + 86400)),
|
||||
("label set", \r -> r {nrReveal = Just reveal}, "label", "alicebakery"),
|
||||
("label cleared", \r -> r {nrReveal = Nothing}, "label", J.Null),
|
||||
("reveal times after a re-commit", \r -> r {nrRevealAfter = Just (fromInteger $ day + 60), nrRevealBefore = Just (fromInteger $ day + 86460)}, "revealBefore", J.toJSON (day + 86460)),
|
||||
("error", \r -> r {nrPhase = PFailed, nrError = Just "The commit transaction failed"}, "error", "The commit transaction failed")
|
||||
]
|
||||
forM_ changes $ \(what, f, k, expected) -> do
|
||||
v <- nrVersion <$> invoiceRegistration ctx inv
|
||||
invoicePoll <- held ctx (0, 1) $ invoicePath inv <> following ISPaid v settledPaid True
|
||||
registrationPoll <- held ctx (0, 1) $ registrationPath nrId <> seenVersion v
|
||||
v' <- change ctx nrId f
|
||||
(status, o) <- woken (what <> ": invoice poll") invoicePoll
|
||||
(status, key "status" o, key "version" (registrationOf o), key k (registrationOf o)) `shouldBe` (200, Just "paid", Just (J.toJSON v'), Just expected)
|
||||
(status', r) <- woken (what <> ": registration poll") registrationPoll
|
||||
(status', key "version" r, key k r) `shouldBe` (200, Just (J.toJSON v'), Just expected)
|
||||
|
||||
testPaidHoldsOnVersion :: IO ()
|
||||
testPaidHoldsOnVersion = bounded "paid holds on version" $ withPolls holdMicros $ \Polls {pCtx = ctx@Ctx {cWeb}} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
settleNow ctx inv settled `shouldReturn` Right ISPaid
|
||||
NameRegistration {nrId, nrVersion} <- invoiceRegistration ctx inv
|
||||
-- without a version there is nothing a paid invoice could wait for
|
||||
forM_ [Nothing, Just "two"] $ \version ->
|
||||
(fst <$> answeredAtOnce ctx (invoicePath inv <> followingAs ISPaid version settledPaid True)) `shouldReturn` 200
|
||||
poll <- held ctx (0, 1) $ invoicePath inv <> following ISPaid nrVersion settledPaid True
|
||||
atomically $ publishPayment (weWaiters cWeb) inv
|
||||
threadDelay parkDelay
|
||||
Async.poll poll >>= (`shouldSatisfy` isNothing)
|
||||
v' <- change ctx nrId $ \r -> r {nrPhase = PCommitting}
|
||||
(_, o) <- woken "paid invoice poll" poll
|
||||
(key "version" (registrationOf o), key "phase" (registrationOf o)) `shouldBe` (Just (J.toJSON v'), Just "committing")
|
||||
-- marks other than the page saw answer at once
|
||||
(fst <$> answeredAtOnce ctx (invoicePath inv <> following ISPaid v' "0.001" True)) `shouldReturn` 200
|
||||
|
||||
testLatePaidAnswersAtOnce :: IO ()
|
||||
testLatePaidAnswersAtOnce = bounded "late paid" $ withPolls holdMicros $ \Polls {pCtx = ctx} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
fst <$> post ctx (invoicePath inv <> "/cancel") "" `shouldReturn` 200
|
||||
settleNow ctx inv settled `shouldReturn` Right ISPaid
|
||||
NameRegistration {nrVersion} <- invoiceRegistration ctx inv
|
||||
(status, o) <- answeredAtOnce ctx $ invoicePath inv <> following ISPaid nrVersion settledPaid True
|
||||
(status, key "status" o, key "phase" (registrationOf o)) `shouldBe` (200, Just "paid", Just "unpaid")
|
||||
|
||||
testOpenWakesOnVersion :: IO ()
|
||||
testOpenWakesOnVersion = bounded "open wakes on version" $ withPolls holdMicros $ \Polls {pCtx = ctx} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
NameRegistration {nrId, nrVersion} <- invoiceRegistration ctx inv
|
||||
-- a version the page has not seen answers at once
|
||||
(fst <$> answeredAtOnce ctx (invoicePath inv <> following ISOpen (nrVersion - 1) "" False)) `shouldReturn` 200
|
||||
poll <- held ctx (1, 1) $ invoicePath inv <> following ISOpen nrVersion "" False
|
||||
v' <- change ctx nrId $ \r -> r {nrError = Just "Checking the payment"}
|
||||
(_, o) <- woken "open invoice poll" poll
|
||||
(key "status" o, key "version" (registrationOf o)) `shouldBe` (Just "open", Just (J.toJSON v'))
|
||||
-- a seenVersion that is no version leaves the hold on the status and payment alone
|
||||
byStatus <- held ctx (1, 0) $ invoicePath inv <> followingAs ISOpen (Just "two") "" False
|
||||
settleNow ctx inv (SigFunded (rcv 5000 (Just "0.00078")) PaidInPart) `shouldReturn` Right ISOpen
|
||||
(_, paidInPart) <- woken "open invoice poll without a version" byStatus
|
||||
(key "status" paidInPart, key "cryptoAmountPaid" paidInPart) `shouldBe` (Just "open", Just "0.00078")
|
||||
|
||||
-- | The stub's crypto invoice expires 45 minutes after the order; the clock is set just short of it.
|
||||
testExpiryWakes :: IO ()
|
||||
testExpiryWakes = bounded "expiry wakes" $ withPolls holdMicros $ \Polls {pCtx = ctx, pSetNow} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
Just InvoiceRow {irExpiresAt} <- getInvoice (cStore ctx) inv
|
||||
NameRegistration {nrVersion} <- invoiceRegistration ctx inv
|
||||
pSetNow $ addUTCTime (negate shortOfMoment) irExpiresAt
|
||||
poll <- held ctx (1, 0) $ invoicePath inv <> followingAs ISOpen Nothing "" False
|
||||
(status, o) <- woken "unpaid invoice poll" poll
|
||||
(status, key "status" o, key "version" (registrationOf o)) `shouldBe` (200, Just "expired", Just (J.toJSON nrVersion))
|
||||
-- read so, not written: the sweep or the next order closes it
|
||||
(irStatus <$>) <$> getInvoice (cStore ctx) inv `shouldReturn` Just ISOpen
|
||||
(fst <$> answeredAtOnce ctx (invoicePath inv <> following ISOpen nrVersion "" False)) `shouldReturn` 200
|
||||
|
||||
testFundedStaysOpen :: IO ()
|
||||
testFundedStaysOpen = bounded "funded stays open" $ withPolls holdMicros $ \Polls {pCtx = ctx, pSetNow} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
settleNow ctx inv (SigFunded (rcv 5000 (Just "0.00078")) PaidInPart) `shouldReturn` Right ISOpen
|
||||
Just InvoiceRow {irExpiresAt} <- getInvoice (cStore ctx) inv
|
||||
NameRegistration {nrVersion} <- invoiceRegistration ctx inv
|
||||
pSetNow $ addUTCTime 60 irExpiresAt
|
||||
(status, o) <- get ctx (invoicePath inv)
|
||||
(status, key "status" o) `shouldBe` (200, Just "open")
|
||||
poll <- held ctx (1, 1) $ invoicePath inv <> following ISOpen nrVersion "0.00078" False
|
||||
cancel poll
|
||||
|
||||
testLapseReadsFailed :: IO ()
|
||||
testLapseReadsFailed = bounded "lapse" $ withPolls holdMicros $ \Polls {pCtx = ctx@Ctx {cOrders}, pSetNow} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
settleNow ctx inv settled `shouldReturn` Right ISPaid
|
||||
NameRegistration {nrId} <- invoiceRegistration ctx inv
|
||||
now <- oeNow cOrders
|
||||
let revealBefore = seconds now + 600
|
||||
lapsedAt = posixSecondsToUTCTime (fromInteger revealBefore)
|
||||
pSetNow $ addUTCTime (negate shortOfMoment) lapsedAt
|
||||
v <- change ctx nrId $ \r -> r {nrPhase = PCommitted, nrRevealAfter = Just (fromInteger $ revealBefore - 3600), nrRevealBefore = Just (fromInteger revealBefore)}
|
||||
(_, r) <- get ctx (registrationPath nrId)
|
||||
key "phase" r `shouldBe` Just "committed"
|
||||
invoicePoll <- held ctx (0, 1) $ invoicePath inv <> following ISPaid v settledPaid True
|
||||
poll <- held ctx (0, 1) $ registrationPath nrId <> seenVersion v
|
||||
(status, lapsed) <- woken "registration poll at the lapse" poll
|
||||
(status, key "phase" lapsed, key "error" lapsed, key "version" lapsed) `shouldBe` (200, Just "failed", Just "The name was not signed in time", Just (J.toJSON v))
|
||||
(_, o) <- woken "invoice poll at the lapse" invoicePoll
|
||||
(key "status" o, key "phase" (registrationOf o), key "error" (registrationOf o)) `shouldBe` (Just "paid", Just "failed", Just "The name was not signed in time")
|
||||
(nrPhase <$> invoiceRegistration ctx inv) `shouldReturn` PCommitted
|
||||
-- one with a reveal set stays committed past revealBefore
|
||||
revealed <- change ctx nrId $ \r' -> r' {nrReveal = Just reveal}
|
||||
pSetNow $ addUTCTime 3600 lapsedAt
|
||||
(_, r') <- get ctx (registrationPath nrId)
|
||||
(key "phase" r', key "label" r', key "version" r') `shouldBe` (Just "committed", Just "alicebakery", Just (J.toJSON revealed))
|
||||
|
||||
testAnswersAtOnce :: IO ()
|
||||
testAnswersAtOnce = bounded "at once" $ withPolls holdMicros $ \Polls {pCtx = ctx} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
NameRegistration {nrId = rid@(RegistrationId ridText), nrVersion} <- invoiceRegistration ctx inv
|
||||
forM_ ["", seenVersion (nrVersion + 1), seenVersion (nrVersion - 1), "?seenVersion=one", "?seenVersion=-1", "?seenVersion=99999999999999999999"] $ \q -> do
|
||||
(status, r) <- answeredAtOnce ctx (registrationPath rid <> q)
|
||||
(q, status, key "id" r, key "version" r, key "phase" r) `shouldBe` (q, 200, Just (J.String ridText), Just (J.toJSON nrVersion), Just "unpaid")
|
||||
|
||||
testHoldTimesOut :: IO ()
|
||||
testHoldTimesOut = bounded "hold times out" $ withPolls shortHold $ \Polls {pCtx = ctx} -> do
|
||||
(inv, _) <- placed ctx aliceOrder
|
||||
NameRegistration {nrId, nrVersion} <- invoiceRegistration ctx inv
|
||||
started <- getCurrentTime
|
||||
(status, r) <- get ctx $ registrationPath nrId <> seenVersion nrVersion
|
||||
waited <- (`diffUTCTime` started) <$> getCurrentTime
|
||||
(status, key "version" r) `shouldBe` (200, Just (J.toJSON nrVersion))
|
||||
waited `shouldSatisfy` (>= (fromIntegral shortHold / 1000000 :: NominalDiffTime))
|
||||
|
||||
testUnknownRegistration :: IO ()
|
||||
testUnknownRegistration = bounded "unknown" $ withPolls holdMicros $ \Polls {pCtx = ctx} -> do
|
||||
(status, o) <- answeredAtOnce ctx (registrationPath (RegistrationId "t2Jk8RmQ4vXw9NcP3yLb6g") <> seenVersion 1)
|
||||
(status, o) `shouldBe` (404, KM.fromList [("error", "not_found")])
|
||||
|
||||
-- | The poller reads providers sooner while a page waits on a payment; a hold on a version waits on none.
|
||||
testWaitingCount :: IO ()
|
||||
testWaitingCount = bounded "waiting count" $ withPolls holdMicros $ \Polls {pCtx = ctx} -> do
|
||||
(paid, _) <- placed ctx aliceOrder
|
||||
settleNow ctx paid settled `shouldReturn` Right ISPaid
|
||||
NameRegistration {nrId, nrVersion} <- invoiceRegistration ctx paid
|
||||
polls <- mapM (held ctx (0, 1)) [registrationPath nrId <> seenVersion nrVersion, invoicePath paid <> following ISPaid nrVersion settledPaid True]
|
||||
atomically (subscriptions ctx) `shouldReturn` (0, 2)
|
||||
(open, _) <- placed ctx aliceOrder {oLabel = "dynamis", oPrice = 20000, oCommitment = commitmentOf 2}
|
||||
NameRegistration {nrVersion = openVersion} <- invoiceRegistration ctx open
|
||||
openPoll <- held ctx (1, 1) $ invoicePath open <> following ISOpen openVersion "" False
|
||||
atomically (subscriptions ctx) `shouldReturn` (1, 3)
|
||||
mapM_ cancel (openPoll : polls)
|
||||
@@ -3,7 +3,7 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Bots.StoreService.NamesStoreTests (namesStoreTests, issue, order, ownerA, ownerB, placeCode, placePaid, t0, usable) where
|
||||
module Bots.StoreService.NamesStoreTests (namesStoreTests, issue, order, ownerA, ownerB, placeCode, placePaid, reveal, t0, usable) where
|
||||
|
||||
import Bots.StoreService.WebTests (columnsOf, digestFixture, runMigrations, withServiceStore)
|
||||
import Control.Arrow ((&&&))
|
||||
|
||||
@@ -16,6 +16,7 @@ import Bots.StoreService.NamesContractsTests
|
||||
import Bots.StoreService.NamesLabelTests
|
||||
import Bots.StoreService.NamesLinksTests
|
||||
import Bots.StoreService.NamesOrdersTests
|
||||
import Bots.StoreService.NamesPhasesTests
|
||||
import Bots.StoreService.NamesProfilesTests
|
||||
import Bots.StoreService.NamesRecordsTests
|
||||
import Bots.StoreService.NamesSearchTests
|
||||
@@ -106,6 +107,7 @@ main = do
|
||||
namesLabelTests
|
||||
namesLinksTests
|
||||
namesOrdersTests
|
||||
namesPhasesTests
|
||||
namesProfilesTests
|
||||
namesRecordsTests
|
||||
namesSearchTests
|
||||
|
||||
Reference in new issue
Block a user