mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 10:47:32 +00:00
893 lines
48 KiB
Haskell
893 lines
48 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Bots.StoreService.NamesStoreTests (namesStoreTests, at, btc, insertInvoiceRow, invoiceStatus, issue, newReg, order, ownerA, ownerB, paidOrder, placeCode, placePaid, price, reg, reveal, signature, t0, usable, write) where
|
|
|
|
import Bots.StoreService.WebTests (columnsOf, digestFixture, runMigrations, withServiceStore)
|
|
import Control.Arrow ((&&&))
|
|
import Control.Monad (forM_, void)
|
|
import Data.Int (Int64)
|
|
import Data.List (sort)
|
|
import Data.Maybe (fromJust, mapMaybe)
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.Time.Calendar (fromGregorian)
|
|
import Data.Time.Clock (UTCTime (..), addUTCTime)
|
|
import Data.Word (Word8)
|
|
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..))
|
|
import Simplex.Chat.PaymentService.Types (CardProvider (..), CryptoCurrency (..), InvoiceId (..), ServicePaymentMethod (..))
|
|
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction)
|
|
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
|
import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationsToRun (..), toDownMigration)
|
|
import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature)
|
|
import Simplex.Messaging.Eth.Address (Address, unAddress)
|
|
import Simplex.Messaging.SimplexName (LabelHash (..), labelHash)
|
|
import StoreService.Names.Chain (TxHash (..))
|
|
import StoreService.Names.Phases (RegistrationId (..))
|
|
import StoreService.Names.Signatures (OrderFields (..), parseAnyCaseAddress, parseSignature)
|
|
import StoreService.Names.Store
|
|
import StoreService.Store (RevokeResult (..))
|
|
import Test.Hspec
|
|
|
|
#if defined(dbPostgres)
|
|
import Database.PostgreSQL.Simple (Only (..), (:.) (..))
|
|
import Database.PostgreSQL.Simple.Errors (ConstraintViolation (..), constraintViolation)
|
|
import StoreService.Store.Postgres.Migrations (storeServiceSchemaMigrations)
|
|
#else
|
|
import Database.SQLite.Simple (Error (ErrorConstraint), Only (..), SQLError (..), (:.) (..))
|
|
import StoreService.Store.SQLite.Migrations (storeServiceSchemaMigrations)
|
|
#endif
|
|
|
|
namesStoreTests :: Spec
|
|
namesStoreTests = describe "StoreService names: store" $ do
|
|
describe "migration" $ do
|
|
it "creates the names tables with the planned columns" testNamesColumns
|
|
it "rolls back and re-applies only the names migrations, with rows in them, leaving the badge tables and paid invoices as they were" testNamesDownUp
|
|
it "turns a code_issued order taken at price 0 and not live at its next version, dropping issued_code_id with its index and foreign key, which the down migration restores" testDropTakenCodes
|
|
it "makes taken orders not live and gives a taken paid order a balance of its price, none to a code order or a price of 0, and rolls back the balance tables and columns" testNameBalancesMigration
|
|
describe "order hash" $ do
|
|
it "is equal for the same order fields, and differs when any one field differs" testOrderHashFields
|
|
it "keeps field boundaries, so moving a byte between two fields changes it" testOrderHashBoundaries
|
|
describe "registrations" $ do
|
|
it "creates a paid order unpaid and live at version 1, linked to its invoice" testCreatePaid
|
|
it "creates a code order queued with its code reserved" testCreateCode
|
|
it "refuses a second live order for one owner and label hash, or for one commitment" testLiveUnique
|
|
it "leaves no registration when the code cannot be reserved" testCodeNotReservable
|
|
it "finds an exact resend by its order hash, live or not" testOrderHashLookup
|
|
it "tells an overdue live order apart, whatever the poller marked, unless paid" testLiveOrdersOverdue
|
|
it "closes an overdue order so a new one from the same wallet fits in the same transaction" testCloseOverdue
|
|
it "closes nothing from a stale version" testCloseOverdueStale
|
|
it "bumps the version on every write and changes nothing for a stale one" testVersionBumps
|
|
it "writes and reads back every changeable field, and clears the reveal" testRoundTrip
|
|
it "keeps live in step with the phase" testLiveFollowsPhase
|
|
it "is live while an order can still progress to a registration for its name" testOrderLive
|
|
it "lets a wallet order a name again once its registration is done" testReorderAfterRegistered
|
|
it "refuses to queue a closed order, also when a newer live order holds its name" testLatePayment
|
|
it "refuses a write that would meet another live order, writing nothing" testWriteMeetsLiveOrder
|
|
it "retargets to the new fields, clearing the reveal, transactions, reveal times and refusal" testRetarget
|
|
it "finds an owner's orders newest first, at most 50" testFind
|
|
it "orders registrations made in the same second by their id" testSameSecondOrder
|
|
it "tells a label being registered here" testLabelRegistering
|
|
it "lists the registrations a worker moves on" testActive
|
|
it "lists unpaid orders whose invoice expired before the cutoff without a payment, and deletes one with its invoice" testUnpaidCleanup
|
|
it "lists code orders failed before the cutoff that still hold their code" testFailedCodeOrders
|
|
describe "prepaid codes" $ do
|
|
it "issues one code per hash" testIssueCode
|
|
it "reserves only an unused code, releases and spends it only for its holder" testReserveReleaseSpend
|
|
it "refuses an unpaid, revoked or expired code" testReserveRefused
|
|
it "lets a registration hold one code" testOneCodePerRegistration
|
|
it "revokes an unspent code, reserved or not, never a spent one" testRevoke
|
|
describe "transactions" $ do
|
|
it "keeps a signed transaction pending until its outcome, and lists them by owner" testTransactions
|
|
it "moves a transaction only from the status its caller read" testTransactionFromStatus
|
|
describe "record edits" $ do
|
|
it "holds one pending or mined edit per owner and nonce, freed by a revert" testEditNonceUnique
|
|
it "settles a pending edit once, with its mined hash" testSettleEdit
|
|
it "round-trips a clear edit" testClearEdit
|
|
it "gives the times of a name's relayed edits in the window" testRelayedEditTimes
|
|
|
|
t0 :: UTCTime
|
|
t0 = UTCTime (fromGregorian 2026 10 21) 0
|
|
|
|
at :: Int -> UTCTime
|
|
at minutes = addUTCTime (fromIntegral $ minutes * 60) t0
|
|
|
|
ownerA :: Address
|
|
ownerA = address "0x014f9c09CcEeDEF1B713110d04D3a07d5B5D9b73"
|
|
|
|
ownerB :: Address
|
|
ownerB = address "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"
|
|
|
|
address :: Text -> Address
|
|
address = either (error . show) id . parseAnyCaseAddress
|
|
|
|
-- r all 0x11, s all 0x22 (a low s), v 27
|
|
signature :: RecoverableSignature
|
|
signature = either (error . show) id $ parseSignature $ "0x" <> T.replicate 32 "11" <> T.replicate 32 "22" <> "1b"
|
|
|
|
twoYears :: Int
|
|
twoYears = 2
|
|
|
|
order :: Text -> Word8 -> OrderFields
|
|
order label n =
|
|
OrderFields
|
|
{ ofLabelHash = labelHash label,
|
|
ofLength = T.length label,
|
|
ofDuration = 69292800,
|
|
ofCommitment = digestFixture n,
|
|
ofSealed = "sealed-" <> label <> "-" <> T.pack (show n)
|
|
}
|
|
|
|
newReg :: Text -> Address -> OrderFields -> NewRegistration
|
|
newReg rid nrgOwner nrgOrder = NewRegistration {nrgId = RegistrationId rid, nrgOwner, nrgOrder, nrgYears = twoYears}
|
|
|
|
btc :: ServicePaymentMethod
|
|
btc = SPMCrypto CCBtc
|
|
|
|
price :: Int
|
|
price = 20000
|
|
|
|
paidOrder :: Text -> PaidOrder
|
|
paidOrder invId = PaidOrder {poMethod = btc, poPrice = price, poInvoiceId = InvoiceId invId, poProviderRef = "ref-" <> invId}
|
|
|
|
insertInvoiceRow :: DB.Connection -> Text -> UTCTime -> Text -> IO ()
|
|
insertInvoiceRow db invId expiresAt status =
|
|
DB.execute
|
|
db
|
|
"INSERT INTO sx_badge_service_invoices (invoice_id, provider, price, amount, currency, payment_address, payment_crypto_currency, payment_crypto_amount, expires_at, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
|
|
((invId, "crypto" :: Text, price, price, "usd" :: Text, "bc1qexampleaddress" :: Text, "btc" :: Text, "0.00312" :: Text) :. (expiresAt, status, t0, t0))
|
|
|
|
-- | A paid order whose invoice, named after the registration, expires at the given time.
|
|
placePaid :: DBStore -> Text -> Address -> OrderFields -> UTCTime -> IO (Either RegistrationRefused ())
|
|
placePaid st rid owner o expiresAt = withTransaction st $ \db -> do
|
|
insertInvoiceRow db ("inv-" <> rid) expiresAt "open"
|
|
createPaidRegistration db (newReg rid owner o) (OTInvoice btc price) (paidOrder $ "inv-" <> rid) t0
|
|
|
|
issue :: DBStore -> Word8 -> NewPrepaidCode -> IO PrepaidCode
|
|
issue st n c = withTransaction st $ \db -> do
|
|
Right codeId <- insertPrepaidCode db c {npcCodeHash = digestFixture n} t0
|
|
fromJust <$> getPrepaidCodeById db codeId
|
|
|
|
usable :: NewPrepaidCode
|
|
usable = NewPrepaidCode {npcCodeHash = "", npcMinLength = 8, npcYears = twoYears, npcPaymentStatus = CPSPaid, npcExpiresAt = Nothing}
|
|
|
|
placeCode :: DBStore -> Text -> Address -> OrderFields -> PrepaidCode -> IO (Either RegistrationRefused ())
|
|
placeCode st rid owner o code = withTransaction st $ \db -> createCodeRegistration db (newReg rid owner o) code t0
|
|
|
|
reg :: DBStore -> Text -> IO NameRegistration
|
|
reg st rid = withTransaction st $ \db -> fromJust <$> getRegistration db (RegistrationId rid)
|
|
|
|
write :: DBStore -> NameRegistration -> IO VersionWrite
|
|
write st r = withTransaction st $ \db -> updateRegistration db r (at 1)
|
|
|
|
namesTables :: [Text]
|
|
namesTables = map ("sx_badge_service_name_" <>) ["prepaid_codes", "registrations", "invoices", "record_edits", "transactions", "balances", "late_credits"]
|
|
|
|
-- | Added to the badge tables' payments by the names migrations.
|
|
balancePaymentColumns :: [Text]
|
|
balancePaymentColumns = ["confirmed_amount", "received_units", "confirmed_units", "invoice_rate"]
|
|
|
|
badgeTables :: [Text]
|
|
badgeTables = map ("sx_badge_service_" <>) ["invoices", "payments", "badge_prices", "badge_offers", "badge_purchases", "badge_ledger", "badge_issuances", "badge_codes", "badge_code_invoices", "group"]
|
|
|
|
testNamesColumns :: IO ()
|
|
testNamesColumns = withServiceStore $ \st -> do
|
|
let has table cols = columnsOf st table >>= \cs -> filter (`notElem` cs) cols `shouldBe` []
|
|
has "sx_badge_service_name_prepaid_codes" ["name_prepaid_code_id", "code_hash", "min_length", "years", "code_payment_status", "reserved_by", "reserved_at", "spent_at", "revoked_at", "expires_at", "created_at"]
|
|
has
|
|
"sx_badge_service_name_registrations"
|
|
[ "registration_id", "label_hash", "length", "label", "secret", "contact_link", "channel_link", "intent_nonce", "intent_deadline", "intent_signature",
|
|
"sealed", "owner", "commitment", "phase", "commit_tx", "commit_block", "register_tx", "register_block", "reveal_after", "reveal_before",
|
|
"years", "duration", "expires_at", "price", "name_prepaid_code_id", "registered_at", "failed_at", "error", "created_at",
|
|
"updated_at", "version", "live", "order_hash", "method", "moves", "links_refused_at", "balance_source", "balance_used"
|
|
]
|
|
has "sx_badge_service_name_invoices" ["invoice_id", "registration_id", "provider_ref", "late_alerted_at", "late_alerted_units", "late_support"]
|
|
has "sx_badge_service_name_balances" ["registration_id", "amount", "credited", "refunded", "reserved_by", "refunded_at", "created_at", "updated_at"]
|
|
has "sx_badge_service_name_late_credits" ["invoice_id", "credit_no", "registration_id", "units", "invoice_rate", "current_rate", "rate_used", "cents", "created_at"]
|
|
has "sx_badge_service_payments" balancePaymentColumns
|
|
has "sx_badge_service_name_record_edits" ["label_hash", "owner", "nonce", "record_key", "record_value", "deadline", "signature", "body_hash", "state", "revert", "mined_tx", "created_at"]
|
|
has "sx_badge_service_name_transactions" ["registration_id", "name_record_edit_id", "kind", "raw", "tx_hash", "registrar_nonce", "sent_at", "mined_block", "status"]
|
|
|
|
testNamesDownUp :: IO ()
|
|
testNamesDownUp = withServiceStore $ \st -> do
|
|
let names = dropWhile ((/= "20261010_names") . name) storeServiceSchemaMigrations
|
|
badgeColumns = mapM (fmap sort . columnsOf st) badgeTables
|
|
map name names `shouldBe` ["20261010_names", "20261022_drop_taken_codes", "20261022_name_balances", "20261022_name_late_wait"]
|
|
badgeBefore <- badgeColumns
|
|
code <- issue st 1 usable
|
|
placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right ()
|
|
placePaid st "r2" ownerB (order "bobsbakery" 2) (at 60) `shouldReturn` Right ()
|
|
placePaid st "r3" ownerB (order "carolsbakery" 3) (at 60) `shouldReturn` Right ()
|
|
withTransaction st $ \db -> insertPayment db "inv-r3"
|
|
runMigrations st $ MTRDown (mapMaybe toDownMigration names)
|
|
mapM (columnsOf st) namesTables `shouldReturn` map (const []) namesTables
|
|
mapM (invoiceCount st) ["inv-r2", "inv-r3"] `shouldReturn` [0, 1]
|
|
badgeColumns `shouldReturn` map (filter (`notElem` balancePaymentColumns)) badgeBefore
|
|
runMigrations st $ MTRUp names
|
|
mapM (fmap null . columnsOf st) namesTables `shouldReturn` map (const False) namesTables
|
|
badgeColumns `shouldReturn` badgeBefore
|
|
|
|
testDropTakenCodes :: IO ()
|
|
testDropTakenCodes = withServiceStore $ \st -> do
|
|
let dropCodes = [m | m@Migration {name = "20261022_drop_taken_codes"} <- storeServiceSchemaMigrations]
|
|
issuedCode = (,) <$> (elem "issued_code_id" <$> columnsOf st "sx_badge_service_name_registrations") <*> indexExists st "sx_badge_service_idx_name_registrations_issued_code"
|
|
worth r = (nrPhase r, nrPrice r, nrLive r, nrVersion r)
|
|
length dropCodes `shouldBe` 1
|
|
runMigrations st $ MTRDown (mapMaybe toDownMigration dropCodes)
|
|
issuedCode `shouldReturn` (True, True)
|
|
code <- issue st 1 usable
|
|
placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right ()
|
|
placePaid st "r2" ownerB (order "bobsbakery" 2) (at 60) `shouldReturn` Right ()
|
|
withTransaction st $ \db -> DB.execute db "UPDATE sx_badge_service_name_registrations SET phase = 'code_issued', issued_code_id = ? WHERE registration_id = 'r1'" (Only $ pcId code)
|
|
r2 <- reg st "r2"
|
|
write st r2 {nrPhase = PTaken} `shouldReturn` Written 2
|
|
taken <- reg st "r2"
|
|
runMigrations st $ MTRUp dropCodes
|
|
issuedCode `shouldReturn` (False, False)
|
|
worth <$> reg st "r1" `shouldReturn` (PTaken, 0, False, 2)
|
|
reg st "r2" `shouldReturn` taken
|
|
withTransaction st (\db -> getPrepaidCodeById db (pcId code)) `shouldReturn` Just code
|
|
runMigrations st $ MTRDown (mapMaybe toDownMigration dropCodes)
|
|
issuedCode `shouldReturn` (True, True)
|
|
worth <$> reg st "r1" `shouldReturn` (PTaken, 0, False, 2)
|
|
withTransaction st (\db -> DB.execute db "UPDATE sx_badge_service_name_registrations SET issued_code_id = ? WHERE registration_id = 'r1'" (Only $ pcId code + 1)) `shouldThrow` foreignKeyViolation
|
|
|
|
testNameBalancesMigration :: IO ()
|
|
testNameBalancesMigration = withServiceStore $ \st -> do
|
|
let balances = [m | m@Migration {name = "20261022_name_balances"} <- storeServiceSchemaMigrations]
|
|
applied :: IO (Bool, Bool, Bool)
|
|
applied = (,,) <$> (not . null <$> columnsOf st "sx_badge_service_name_balances") <*> (elem "balance_source" <$> columnsOf st "sx_badge_service_name_registrations") <*> (elem "confirmed_units" <$> columnsOf st "sx_badge_service_payments")
|
|
live :: Text -> IO Bool
|
|
live rid = withTransaction st $ \db -> DB.query db "SELECT live FROM sx_badge_service_name_registrations WHERE registration_id = ?" (Only rid) >>= \case
|
|
[Only (DB.BI l)] -> pure l
|
|
rows -> fail $ "registration " <> show rid <> ": " <> show (length rows) <> " rows"
|
|
balanceOf :: Text -> IO (Maybe (Int, Int, Int, Maybe BalanceHold))
|
|
balanceOf rid = withTransaction st $ \db -> fmap (\Balance {blAmount, blCredited, blRefunded, blHold} -> (blAmount, blCredited, blRefunded, blHold)) <$> getBalance db (RegistrationId rid) t0
|
|
length balances `shouldBe` 1
|
|
runMigrations st $ MTRDown (mapMaybe toDownMigration balances)
|
|
applied `shouldReturn` (False, False, False)
|
|
code <- issue st 1 usable
|
|
withTransaction st $ \db -> do
|
|
insertInvoiceRow db "inv-paid" (at 60) "paid"
|
|
insertOldRegistration db "paid" 1 "taken" price Nothing (Just "inv-paid")
|
|
insertOldRegistration db "code" 2 "taken" 0 (Just $ pcId code) Nothing
|
|
insertInvoiceRow db "inv-converted" (at 60) "paid"
|
|
insertOldRegistration db "converted" 3 "taken" 0 Nothing (Just "inv-converted")
|
|
insertInvoiceRow db "inv-queued" (at 60) "paid"
|
|
insertOldRegistration db "queued" 4 "queued" price Nothing (Just "inv-queued")
|
|
runMigrations st $ MTRUp balances
|
|
applied `shouldReturn` (True, True, True)
|
|
mapM live ["paid", "code", "converted", "queued"] `shouldReturn` [False, False, False, True]
|
|
mapM balanceOf ["paid", "code", "converted", "queued"] `shouldReturn` [Just (price, price, 0, Nothing), Nothing, Nothing, Nothing]
|
|
runMigrations st $ MTRDown (mapMaybe toDownMigration balances)
|
|
applied `shouldReturn` (False, False, False)
|
|
mapM live ["paid", "queued"] `shouldReturn` [False, True]
|
|
runMigrations st $ MTRUp balances
|
|
balanceOf "paid" `shouldReturn` Just (price, price, 0, Nothing)
|
|
|
|
-- | A registration row as the code before the balances wrote it, paid by the invoice or the code given.
|
|
insertOldRegistration :: DB.Connection -> Text -> Word8 -> Text -> Int -> Maybe Int64 -> Maybe Text -> IO ()
|
|
insertOldRegistration db rid n phase cents codeId invId = do
|
|
let OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed} = order ("alicebakery" <> rid) n
|
|
DB.execute
|
|
db
|
|
( "INSERT INTO sx_badge_service_name_registrations (registration_id, version, label_hash, length, duration, owner, commitment, sealed, phase, years,"
|
|
<> " price, method, name_prepaid_code_id, live, order_hash, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?)"
|
|
)
|
|
( (rid, 1 :: Int, DB.Binary lh, ofLength, fromIntegral ofDuration :: Int64, DB.Binary $ unAddress ownerA, DB.Binary ofCommitment, ofSealed, phase, twoYears)
|
|
:. (cents, ("btc" :: Text) <$ invId, codeId, DB.BI True, DB.Binary $ digestFixture n, t0, t0)
|
|
)
|
|
mapM_ (\i -> DB.execute db "INSERT INTO sx_badge_service_name_invoices (invoice_id, registration_id, provider_ref, created_at) VALUES (?,?,?,?)" (i, rid, "ref-" <> i, t0)) invId
|
|
|
|
testOrderHashFields :: IO ()
|
|
testOrderHashFields = do
|
|
let o = order "alicebakery" 1
|
|
paid = OTInvoice btc price
|
|
base = orderHash ownerA o paid
|
|
variants =
|
|
[ ("labelHash" :: Text, orderHash ownerA o {ofLabelHash = labelHash "alicebakers"} paid),
|
|
("length", orderHash ownerA o {ofLength = 12} paid),
|
|
("duration", orderHash ownerA o {ofDuration = 69292801} paid),
|
|
("owner", orderHash ownerB o paid),
|
|
("commitment", orderHash ownerA o {ofCommitment = digestFixture 2} paid),
|
|
("sealed", orderHash ownerA o {ofSealed = ofSealed o <> "x"} paid),
|
|
("method", orderHash ownerA o (OTInvoice (SPMCard CPStripe) price)),
|
|
("price", orderHash ownerA o (OTInvoice btc (price + 1))),
|
|
("code instead of invoice", orderHash ownerA o (OTCode $ digestFixture 3)),
|
|
("another code", orderHash ownerA o (OTCode $ digestFixture 4))
|
|
]
|
|
orderHash ownerA (order "alicebakery" 1) paid `shouldBe` base
|
|
forM_ variants $ \(field, h) -> (field, h == base) `shouldBe` (field, False)
|
|
orderHash ownerA o (OTCode $ digestFixture 3) `shouldNotBe` orderHash ownerA o (OTCode $ digestFixture 4)
|
|
|
|
testOrderHashBoundaries :: IO ()
|
|
testOrderHashBoundaries = do
|
|
let o = order "alicebakery" 1
|
|
orderHash ownerA o {ofSealed = "ab"} (OTCode "c") `shouldNotBe` orderHash ownerA o {ofSealed = "a"} (OTCode "bc")
|
|
|
|
testCreatePaid :: IO ()
|
|
testCreatePaid = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
placePaid st "r1" ownerA o (at 60) `shouldReturn` Right ()
|
|
r <- reg st "r1"
|
|
r
|
|
`shouldBe` NameRegistration
|
|
{ nrId = RegistrationId "r1",
|
|
nrVersion = 1,
|
|
nrLabelHash = ofLabelHash o,
|
|
nrLength = 11,
|
|
nrDuration = 69292800,
|
|
nrOwner = ownerA,
|
|
nrCommitment = ofCommitment o,
|
|
nrSealed = ofSealed o,
|
|
nrPhase = PUnpaid,
|
|
nrYears = twoYears,
|
|
nrPrice = price,
|
|
nrPayment = RPInvoice btc (InvoiceId "inv-r1"),
|
|
nrBalanceSource = Nothing,
|
|
nrBalanceUsed = 0,
|
|
nrReveal = Nothing,
|
|
nrCommitTx = Nothing,
|
|
nrCommitBlock = Nothing,
|
|
nrRegisterTx = Nothing,
|
|
nrRegisterBlock = Nothing,
|
|
nrRevealAfter = Nothing,
|
|
nrRevealBefore = Nothing,
|
|
nrExpiresAt = Nothing,
|
|
nrRegisteredAt = Nothing,
|
|
nrFailedAt = Nothing,
|
|
nrError = Nothing,
|
|
nrLive = True,
|
|
nrOrderHash = orderHash ownerA o (OTInvoice btc price),
|
|
nrMoves = 0,
|
|
nrLinksRefusedAt = Nothing,
|
|
nrCreatedAt = t0,
|
|
nrUpdatedAt = t0
|
|
}
|
|
|
|
testCreateCode :: IO ()
|
|
testCreateCode = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
code@PrepaidCode {pcId, pcCodeHash} <- issue st 1 usable
|
|
placeCode st "r1" ownerA o code `shouldReturn` Right ()
|
|
r <- reg st "r1"
|
|
(nrPhase r, nrLive r, nrPrice r, nrPayment r, nrVersion r) `shouldBe` (PQueued, True, 0, RPCode pcId, 1)
|
|
nrOrderHash r `shouldBe` orderHash ownerA o (OTCode pcCodeHash)
|
|
held <- withTransaction st $ \db -> getPrepaidCode db pcCodeHash
|
|
(pcReservedBy =<< held, pcReservedAt =<< held) `shouldBe` (Just (RegistrationId "r1"), Just t0)
|
|
|
|
testLiveUnique :: IO ()
|
|
testLiveUnique = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
placePaid st "r1" ownerA o (at 60) `shouldReturn` Right ()
|
|
placePaid st "r2" ownerA o {ofCommitment = digestFixture 2} (at 60) `shouldReturn` Left RRLiveOrder
|
|
placePaid st "r3" ownerB o (at 60) `shouldReturn` Left RRLiveOrder
|
|
code <- issue st 9 usable
|
|
placeCode st "r4" ownerA (order "alicebakery" 3) code `shouldReturn` Left RRLiveOrder
|
|
placePaid st "r5" ownerB (order "alicebakery" 5) (at 60) `shouldReturn` Right ()
|
|
existing <- withTransaction st $ \db -> mapM (getRegistration db . RegistrationId) ["r2", "r3", "r4"]
|
|
existing `shouldBe` [Nothing, Nothing, Nothing]
|
|
free <- withTransaction st $ \db -> getPrepaidCodeById db (pcId code)
|
|
(pcReservedBy =<< free) `shouldBe` Nothing
|
|
|
|
testCodeNotReservable :: IO ()
|
|
testCodeNotReservable = withServiceStore $ \st -> do
|
|
code <- issue st 1 usable
|
|
placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right ()
|
|
placeCode st "r2" ownerB (order "bobsbakery" 2) code `shouldReturn` Left RRCodeNotReservable
|
|
withTransaction st (\db -> getRegistration db (RegistrationId "r2")) `shouldReturn` Nothing
|
|
held <- withTransaction st $ \db -> getPrepaidCodeById db (pcId code)
|
|
(pcReservedBy =<< held) `shouldBe` Just (RegistrationId "r1")
|
|
|
|
testOrderHashLookup :: IO ()
|
|
testOrderHashLookup = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
placePaid st "r1" ownerA o (at 1) `shouldReturn` Right ()
|
|
r <- reg st "r1"
|
|
closed <- withTransaction st $ \db -> closeOverdue db r (at 2)
|
|
closed `shouldBe` Just 2
|
|
found <- withTransaction st $ \db -> getRegistrationByOrderHash db (orderHash ownerA o (OTInvoice btc price))
|
|
(nrId <$> found, nrLive <$> found) `shouldBe` (Just (RegistrationId "r1"), Just False)
|
|
withTransaction st (\db -> getRegistrationByOrderHash db (orderHash ownerA o (OTInvoice btc (price + 1)))) `shouldReturn` Nothing
|
|
|
|
testLiveOrdersOverdue :: IO ()
|
|
testLiveOrdersOverdue = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
live now = withTransaction st $ \db -> map (\LiveOrder {loRegistration, loOverdue} -> (nrId loRegistration, loOverdue)) <$> getLiveOrders db (ofCommitment o) ownerA (ofLabelHash o) now
|
|
setStatus status = withTransaction st $ \db -> DB.execute db "UPDATE sx_badge_service_invoices SET status = ? WHERE invoice_id = ?" (status :: Text, "inv-r1" :: Text)
|
|
placePaid st "r1" ownerA o (at 60) `shouldReturn` Right ()
|
|
placePaid st "r2" ownerB (order "alicebakery" 2) (at 60) `shouldReturn` Right ()
|
|
live (at 30) `shouldReturn` [(RegistrationId "r1", False)]
|
|
live (at 61) `shouldReturn` [(RegistrationId "r1", True)]
|
|
withTransaction st (\db -> map (nrId . loRegistration) <$> getLiveOrders db (ofCommitment o) ownerB (labelHash "zzzzzzzz") (at 30))
|
|
`shouldReturn` [RegistrationId "r1"]
|
|
setStatus "expired"
|
|
live (at 61) `shouldReturn` [(RegistrationId "r1", True)]
|
|
setStatus "paid"
|
|
live (at 61) `shouldReturn` [(RegistrationId "r1", False)]
|
|
|
|
testCloseOverdue :: IO ()
|
|
testCloseOverdue = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
o' = o {ofCommitment = digestFixture 2, ofSealed = "new"}
|
|
placePaid st "r1" ownerA o (at 60) `shouldReturn` Right ()
|
|
placed <- withTransaction st $ \db -> do
|
|
[LiveOrder {loRegistration, loOverdue = True}] <- getLiveOrders db (ofCommitment o') ownerA (ofLabelHash o') (at 61)
|
|
void $ closeOverdue db loRegistration (at 61)
|
|
insertInvoiceRow db "inv-r2" (at 120) "open"
|
|
createPaidRegistration db (newReg "r2" ownerA o') (OTInvoice btc price) (paidOrder "inv-r2") (at 61)
|
|
placed `shouldBe` Right ()
|
|
old <- reg st "r1"
|
|
(nrLive old, nrPhase old, nrVersion old) `shouldBe` (False, PUnpaid, 2)
|
|
invoiceStatus st "inv-r1" `shouldReturn` "expired"
|
|
invoiceStatus st "inv-r2" `shouldReturn` "open"
|
|
|
|
testCloseOverdueStale :: IO ()
|
|
testCloseOverdueStale = withServiceStore $ \st -> do
|
|
placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right ()
|
|
r <- reg st "r1"
|
|
write st r {nrPhase = PQueued} `shouldReturn` Written 2
|
|
withTransaction st (\db -> closeOverdue db r (at 61)) `shouldReturn` Nothing
|
|
invoiceStatus st "inv-r1" `shouldReturn` "open"
|
|
(nrLive &&& nrPhase) <$> reg st "r1" `shouldReturn` (True, PQueued)
|
|
|
|
invoiceStatus :: DBStore -> Text -> IO Text
|
|
invoiceStatus st invId = withTransaction st $ \db ->
|
|
DB.query db "SELECT status FROM sx_badge_service_invoices WHERE invoice_id = ?" (Only invId) >>= \case
|
|
[Only status] -> pure status
|
|
rows -> fail $ "invoice " <> show invId <> ": " <> show (length rows) <> " rows"
|
|
|
|
reveal :: NameReveal
|
|
reveal =
|
|
NameReveal
|
|
{ rvLabel = "alicebakery",
|
|
rvSecret = digestFixture 7,
|
|
rvContactLink = Just "https://smp8.simplex.im/a#abc",
|
|
rvChannelLink = Nothing,
|
|
rvNonce = 3,
|
|
rvDeadline = 1792915140,
|
|
rvSignature = signature
|
|
}
|
|
|
|
testVersionBumps :: IO ()
|
|
testVersionBumps = withServiceStore $ \st -> do
|
|
code <- issue st 1 usable
|
|
placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right ()
|
|
r1 <- reg st "r1"
|
|
write st r1 {nrPhase = PCommitting, nrCommitTx = Just (TxHash $ digestFixture 8)} `shouldReturn` Written 2
|
|
r2 <- reg st "r1"
|
|
write st r2 {nrPhase = PCommitted, nrRevealAfter = Just 1792828800, nrRevealBefore = Just 1792915140} `shouldReturn` Written 3
|
|
r3 <- reg st "r1"
|
|
write st r3 {nrReveal = Just reveal} `shouldReturn` Written 4
|
|
write st r3 {nrLinksRefusedAt = Just (at 5)} `shouldReturn` VersionChanged
|
|
write st r1 {nrPhase = PFailed} `shouldReturn` VersionChanged
|
|
r4 <- reg st "r1"
|
|
(nrVersion r4, nrPhase r4, nrReveal r4, nrLinksRefusedAt r4) `shouldBe` (4, PCommitted, Just reveal, Nothing)
|
|
|
|
testRoundTrip :: IO ()
|
|
testRoundTrip = withServiceStore $ \st -> do
|
|
code <- issue st 1 usable
|
|
placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right ()
|
|
r <- reg st "r1"
|
|
let changed =
|
|
r
|
|
{ nrPhase = PRegistered,
|
|
nrReveal = Just reveal {rvChannelLink = Just "simplex:/c#xyz"},
|
|
nrCommitTx = Just (TxHash $ digestFixture 8),
|
|
nrCommitBlock = Just 100,
|
|
nrRegisterTx = Just (TxHash $ digestFixture 9),
|
|
nrRegisterBlock = Just 120,
|
|
nrRevealAfter = Just 1792828800,
|
|
nrRevealBefore = Just 1792915140,
|
|
nrExpiresAt = Just 1861833600,
|
|
nrRegisteredAt = Just (at 3),
|
|
nrFailedAt = Just (at 2),
|
|
nrError = Just "The name was not signed in time",
|
|
nrLinksRefusedAt = Just (at 4),
|
|
nrMoves = 2
|
|
}
|
|
write st changed `shouldReturn` Written 2
|
|
reg st "r1" `shouldReturn` changed {nrVersion = 2, nrLive = False, nrUpdatedAt = at 1}
|
|
write st changed {nrVersion = 2, nrReveal = Nothing} `shouldReturn` Written 3
|
|
nrReveal <$> reg st "r1" `shouldReturn` Nothing
|
|
|
|
testLiveFollowsPhase :: IO ()
|
|
testLiveFollowsPhase = withServiceStore $ \st -> do
|
|
placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right ()
|
|
code <- issue st 1 usable
|
|
placeCode st "r2" ownerB (order "bobsbakery" 2) code `shouldReturn` Right ()
|
|
let step rid phase expected = do
|
|
r <- reg st rid
|
|
write st r {nrPhase = phase} `shouldReturn` Written (nrVersion r + 1)
|
|
(nrPhase &&& nrLive) <$> reg st rid `shouldReturn` (phase, expected)
|
|
step "r1" PQueued True
|
|
step "r1" PFailed True
|
|
step "r1" PTaken False
|
|
r1 <- reg st "r1"
|
|
write st r1 {nrPhase = PFailed} `shouldReturn` NotLive
|
|
step "r2" PRegistering True
|
|
step "r2" PTaken False
|
|
placePaid st "r3" ownerA (order "carolsbakery" 3) (at 60) `shouldReturn` Right ()
|
|
r3 <- reg st "r3"
|
|
write st r3 {nrLive = False} `shouldReturn` Written 2
|
|
nrLive <$> reg st "r3" `shouldReturn` False
|
|
|
|
testOrderLive :: IO ()
|
|
testOrderLive = withServiceStore $ \st -> do
|
|
placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right ()
|
|
paid <- reg st "r1"
|
|
let code = paid {nrPayment = RPCode 1}
|
|
liveIn r phase = (phase, orderLive r {nrPhase = phase})
|
|
map (liveIn paid) [minBound .. maxBound]
|
|
`shouldBe` [(PUnpaid, True), (PQueued, True), (PCommitting, True), (PCommitted, True), (PRegistering, True), (PRegistered, False), (PTaken, False), (PFailed, True)]
|
|
map (liveIn code) [PTaken, PFailed, PRegistered] `shouldBe` [(PTaken, False), (PFailed, True), (PRegistered, False)]
|
|
orderLive paid {nrLive = False} `shouldBe` False
|
|
orderLive paid {nrLive = False, nrPhase = PQueued} `shouldBe` True
|
|
|
|
testReorderAfterRegistered :: IO ()
|
|
testReorderAfterRegistered = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
code <- issue st 1 usable
|
|
placeCode st "r1" ownerA o code `shouldReturn` Right ()
|
|
placePaid st "r2" ownerA o {ofCommitment = digestFixture 2} (at 60) `shouldReturn` Left RRLiveOrder
|
|
r <- reg st "r1"
|
|
write st r {nrPhase = PRegistered, nrRegisteredAt = Just (at 1)} `shouldReturn` Written 2
|
|
nrLive <$> reg st "r1" `shouldReturn` False
|
|
placePaid st "r3" ownerA o {ofCommitment = digestFixture 2} (at 60) `shouldReturn` Right ()
|
|
|
|
testLatePayment :: IO ()
|
|
testLatePayment = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
o' = o {ofCommitment = digestFixture 2, ofSealed = "new"}
|
|
placePaid st "r1" ownerA o (at 60) `shouldReturn` Right ()
|
|
closed <- reg st "r1"
|
|
withTransaction st (\db -> closeOverdue db closed (at 61)) `shouldReturn` Just 2
|
|
r1 <- reg st "r1"
|
|
write st r1 {nrPhase = PQueued} `shouldReturn` NotLive
|
|
reg st "r1" `shouldReturn` r1
|
|
placePaid st "r2" ownerA o' (at 120) `shouldReturn` Right ()
|
|
write st r1 {nrPhase = PQueued} `shouldReturn` NotLive
|
|
reg st "r1" `shouldReturn` r1
|
|
(nrPhase &&& nrLive) <$> reg st "r2" `shouldReturn` (PUnpaid, True)
|
|
write st r1 {nrVersion = 1, nrPhase = PQueued} `shouldReturn` VersionChanged
|
|
|
|
testWriteMeetsLiveOrder :: IO ()
|
|
testWriteMeetsLiveOrder = withServiceStore $ \st -> do
|
|
let held = order "bobsbakery" 2
|
|
placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right ()
|
|
placePaid st "r2" ownerA held (at 60) `shouldReturn` Right ()
|
|
placePaid st "r3" ownerB (order "carolsbakery" 3) (at 60) `shouldReturn` Right ()
|
|
r1 <- reg st "r1"
|
|
write st r1 {nrPhase = PFailed} `shouldReturn` Written 2
|
|
failed <- reg st "r1"
|
|
let retarget o = withTransaction st $ \db -> retargetRegistration db failed o 10000 (at 6)
|
|
retarget held {ofCommitment = digestFixture 4} `shouldReturn` LiveOrderExists
|
|
r3 <- reg st "r3"
|
|
retarget (order "carolsbakery" 5) {ofCommitment = nrCommitment r3} `shouldReturn` LiveOrderExists
|
|
reg st "r1" `shouldReturn` failed
|
|
retarget (order "davesbakery" 6) `shouldReturn` Written 3
|
|
|
|
testRetarget :: IO ()
|
|
testRetarget = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
o' = order "alicebakers" 2
|
|
placePaid st "r1" ownerA o (at 60) `shouldReturn` Right ()
|
|
r <- reg st "r1"
|
|
let failed =
|
|
r
|
|
{ nrPhase = PFailed,
|
|
nrReveal = Just reveal,
|
|
nrCommitTx = Just (TxHash $ digestFixture 8),
|
|
nrCommitBlock = Just 100,
|
|
nrRegisterTx = Just (TxHash $ digestFixture 9),
|
|
nrRegisterBlock = Just 120,
|
|
nrRevealAfter = Just 1,
|
|
nrRevealBefore = Just 2,
|
|
nrLinksRefusedAt = Just (at 4),
|
|
nrFailedAt = Just (at 5),
|
|
nrError = Just "failed"
|
|
}
|
|
write st failed `shouldReturn` Written 2
|
|
withTransaction st (\db -> retargetRegistration db failed {nrVersion = 2} o' 10000 (at 6)) `shouldReturn` Written 3
|
|
r' <- reg st "r1"
|
|
r'
|
|
`shouldBe` r
|
|
{ nrVersion = 3,
|
|
nrLabelHash = ofLabelHash o',
|
|
nrCommitment = ofCommitment o',
|
|
nrSealed = ofSealed o',
|
|
nrPrice = 10000,
|
|
nrPhase = PQueued,
|
|
nrMoves = 1,
|
|
nrUpdatedAt = at 6
|
|
}
|
|
withTransaction st (\db -> retargetRegistration db failed {nrVersion = 2} o 10000 (at 7)) `shouldReturn` VersionChanged
|
|
|
|
testFind :: IO ()
|
|
testFind = withServiceStore $ \st -> do
|
|
forM_ [1 .. 52 :: Int] $ \i -> withTransaction st $ \db -> do
|
|
let rid = "r" <> T.pack (show i)
|
|
o = order ("alicebakery" <> T.pack (show i)) (fromIntegral i)
|
|
insertInvoiceRow db ("inv-" <> rid) (at 60) "open"
|
|
createPaidRegistration db (newReg rid ownerA o) (OTInvoice btc price) (paidOrder $ "inv-" <> rid) (at i) `shouldReturn` Right ()
|
|
placePaid st "other" ownerB (order "bobsbakery" 99) (at 60) `shouldReturn` Right ()
|
|
found <- withTransaction st $ \db -> findRegistrations db ownerA t0
|
|
map (nrId . fst) found `shouldBe` [RegistrationId $ "r" <> T.pack (show i) | i <- [52, 51 .. 3 :: Int]]
|
|
withTransaction st (\db -> findRegistrations db (address "0x0000000000000000000000000000000000000001") t0) `shouldReturn` []
|
|
|
|
testSameSecondOrder :: IO ()
|
|
testSameSecondOrder = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
ids = ["r2", "r3", "r1"]
|
|
forM_ (zip ids [10 ..]) $ \(rid, n) -> do
|
|
placePaid st rid ownerA (order ("alicebakery" <> rid) n) (at 60) `shouldReturn` Right ()
|
|
r <- reg st rid
|
|
write st r {nrPhase = PQueued} `shouldReturn` Written 2
|
|
map (nrId . fst) <$> withTransaction st (\db -> findRegistrations db ownerA t0) `shouldReturn` map RegistrationId ["r3", "r2", "r1"]
|
|
map nrId <$> withTransaction st activeRegistrations `shouldReturn` map RegistrationId ["r1", "r2", "r3"]
|
|
forM_ ["r5", "r4"] $ \rid -> do
|
|
placePaid st rid ownerB o (at 60) `shouldReturn` Right ()
|
|
r <- reg st rid
|
|
withTransaction st (\db -> closeOverdue db r (at 61)) `shouldReturn` Just 2
|
|
found <- withTransaction st $ \db -> getRegistrationByOrderHash db (orderHash ownerB o (OTInvoice btc price))
|
|
nrId <$> found `shouldBe` Just (RegistrationId "r5")
|
|
|
|
testLabelRegistering :: IO ()
|
|
testLabelRegistering = withServiceStore $ \st -> do
|
|
let o = order "alicebakery" 1
|
|
registering = withTransaction st $ \db -> isLabelRegistering db (ofLabelHash o)
|
|
code <- issue st 1 usable
|
|
placeCode st "r1" ownerA o code `shouldReturn` Right ()
|
|
registering `shouldReturn` False
|
|
r <- reg st "r1"
|
|
void $ write st r {nrPhase = PRegistering}
|
|
registering `shouldReturn` True
|
|
r' <- reg st "r1"
|
|
void $ write st r' {nrPhase = PRegistered}
|
|
registering `shouldReturn` False
|
|
|
|
testActive :: IO ()
|
|
testActive = withServiceStore $ \st -> do
|
|
let phases = [PUnpaid ..]
|
|
forM_ (zip [1 ..] phases) $ \(i, phase) -> do
|
|
let rid = "r" <> T.pack (show i)
|
|
placePaid st rid ownerA (order ("alicebakery" <> T.pack (show i)) i) (at 60) `shouldReturn` Right ()
|
|
r <- reg st rid
|
|
void $ write st r {nrPhase = phase}
|
|
active <- withTransaction st activeRegistrations
|
|
sort (map (fromEnum . nrPhase) active) `shouldBe` map fromEnum [PQueued, PCommitting, PCommitted, PRegistering]
|
|
|
|
testUnpaidCleanup :: IO ()
|
|
testUnpaidCleanup = withServiceStore $ \st -> do
|
|
placePaid st "old" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right ()
|
|
placePaid st "funded" ownerA (order "bobsbakery" 2) (at 60) `shouldReturn` Right ()
|
|
placePaid st "queued" ownerA (order "carolsbakery" 3) (at 60) `shouldReturn` Right ()
|
|
placePaid st "open" ownerA (order "davesbakery" 4) (at 120) `shouldReturn` Right ()
|
|
withTransaction st $ \db -> insertPayment db "inv-funded"
|
|
r <- reg st "queued"
|
|
void $ write st r {nrPhase = PQueued}
|
|
candidates <- withTransaction st $ \db -> unpaidCleanupCandidates db (at 61)
|
|
map (\(RegistrationId rid, InvoiceId invId) -> (rid, invId)) candidates `shouldBe` [("old", "inv-old")]
|
|
withTransaction st (\db -> mapM (deleteUnpaidRegistration db . RegistrationId) ["old", "funded", "queued"]) `shouldReturn` [Right (), Left CKPaid, Left CKPaid]
|
|
withTransaction st (\db -> getRegistration db (RegistrationId "old")) `shouldReturn` Nothing
|
|
invoiceCount st "inv-old" `shouldReturn` 0
|
|
invoiceCount st "inv-funded" `shouldReturn` 1
|
|
nrPhase <$> reg st "funded" `shouldReturn` PUnpaid
|
|
|
|
insertPayment :: DB.Connection -> Text -> IO ()
|
|
insertPayment db invId =
|
|
DB.execute
|
|
db
|
|
"INSERT INTO sx_badge_service_payments (payment_id, invoice_id, provider, status, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
|
(invId, invId, "crypto" :: Text, "pending" :: Text, t0, t0)
|
|
|
|
foreignKeyViolation :: Selector DB.SQLError
|
|
#if defined(dbPostgres)
|
|
foreignKeyViolation e = case constraintViolation e of
|
|
Just ForeignKeyViolation {} -> True
|
|
_ -> False
|
|
#else
|
|
foreignKeyViolation e = sqlError e == ErrorConstraint && "FOREIGN KEY" `T.isInfixOf` sqlErrorDetails e
|
|
#endif
|
|
|
|
indexExists :: DBStore -> Text -> IO Bool
|
|
indexExists st index = withTransaction st $ \db -> do
|
|
rows <- DB.query db indexQuery (Only index)
|
|
pure $ not $ null (rows :: [Only Text])
|
|
where
|
|
#if defined(dbPostgres)
|
|
indexQuery = "SELECT indexname FROM pg_indexes WHERE schemaname = current_schema() AND indexname = ?"
|
|
#else
|
|
indexQuery = "SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?"
|
|
#endif
|
|
|
|
invoiceCount :: DBStore -> Text -> IO Int
|
|
invoiceCount st invId = withTransaction st $ \db ->
|
|
DB.query db "SELECT COUNT(*) FROM sx_badge_service_invoices WHERE invoice_id = ?" (Only invId) >>= \case
|
|
[Only n] -> pure n
|
|
rows -> fail $ "count: " <> show (length rows) <> " rows"
|
|
|
|
testFailedCodeOrders :: IO ()
|
|
testFailedCodeOrders = withServiceStore $ \st -> do
|
|
codes@[oldCode, _, releasedCode, _] <- mapM (\n -> issue st n usable) [1, 2, 3, 4]
|
|
forM_ (zip3 ["old", "recent", "released", "queued"] [1 ..] codes) $ \(rid, n, code) ->
|
|
placeCode st rid ownerA (order ("alicebakery" <> rid) n) code `shouldReturn` Right ()
|
|
let failAt rid t = reg st rid >>= \r -> void (write st r {nrPhase = PFailed, nrFailedAt = Just t})
|
|
failAt "old" (at 1)
|
|
failAt "recent" (at 30)
|
|
failAt "released" (at 1)
|
|
withTransaction st (\db -> releasePrepaidCode db (pcId releasedCode) (RegistrationId "released")) `shouldReturn` True
|
|
withTransaction st (\db -> failedCodeOrdersBefore db (at 10)) `shouldReturn` [(RegistrationId "old", pcId oldCode)]
|
|
|
|
testIssueCode :: IO ()
|
|
testIssueCode = withServiceStore $ \st -> do
|
|
code <- issue st 1 usable {npcMinLength = 6, npcYears = 5, npcPaymentStatus = CPSFree, npcExpiresAt = Just (at 100)}
|
|
(pcMinLength code, pcYears code, pcPaymentStatus code, pcExpiresAt code, pcCreatedAt code) `shouldBe` (6, 5, CPSFree, Just (at 100), t0)
|
|
withTransaction st (\db -> insertPrepaidCode db usable {npcCodeHash = digestFixture 1} t0) `shouldReturn` Left CodeHashTaken
|
|
withTransaction st (\db -> getPrepaidCode db (digestFixture 1)) `shouldReturn` Just code
|
|
|
|
testReserveReleaseSpend :: IO ()
|
|
testReserveReleaseSpend = withServiceStore $ \st -> do
|
|
PrepaidCode {pcId} <- issue st 1 usable
|
|
placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right ()
|
|
placePaid st "r2" ownerB (order "bobsbakery" 2) (at 60) `shouldReturn` Right ()
|
|
let r1 = RegistrationId "r1"
|
|
r2 = RegistrationId "r2"
|
|
run :: (DB.Connection -> IO a) -> IO a
|
|
run = withTransaction st
|
|
run (\db -> releasePrepaidCode db pcId r1) `shouldReturn` False
|
|
run (\db -> spendPrepaidCode db pcId r1 t0) `shouldReturn` False
|
|
run (\db -> reservePrepaidCode db pcId r1 (at 1)) `shouldReturn` True
|
|
run (\db -> reservePrepaidCode db pcId r2 (at 1)) `shouldReturn` False
|
|
run (\db -> releasePrepaidCode db pcId r2) `shouldReturn` False
|
|
run (\db -> spendPrepaidCode db pcId r2 (at 2)) `shouldReturn` False
|
|
run (\db -> releasePrepaidCode db pcId r1) `shouldReturn` True
|
|
run (\db -> reservePrepaidCode db pcId r2 (at 2)) `shouldReturn` True
|
|
run (\db -> spendPrepaidCode db pcId r2 (at 3)) `shouldReturn` True
|
|
run (\db -> releasePrepaidCode db pcId r2) `shouldReturn` False
|
|
run (\db -> reservePrepaidCode db pcId r1 (at 4)) `shouldReturn` False
|
|
run (\db -> spendPrepaidCode db pcId r2 (at 4)) `shouldReturn` False
|
|
spent <- run $ \db -> getPrepaidCodeById db pcId
|
|
(pcReservedBy =<< spent, pcSpentAt =<< spent) `shouldBe` (Just r2, Just (at 3))
|
|
|
|
testReserveRefused :: IO ()
|
|
testReserveRefused = withServiceStore $ \st -> do
|
|
placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right ()
|
|
unpaid <- issue st 1 usable {npcPaymentStatus = CPSUnpaid}
|
|
expired <- issue st 2 usable {npcExpiresAt = Just (at 10)}
|
|
revoked <- issue st 3 usable
|
|
free <- issue st 4 usable {npcPaymentStatus = CPSFree, npcExpiresAt = Just (at 30)}
|
|
withTransaction st (\db -> revokePrepaidCode db (pcCodeHash revoked) t0) `shouldReturn` Revoked (pcId revoked)
|
|
let reserve code = withTransaction st $ \db -> reservePrepaidCode db (pcId code) (RegistrationId "r1") (at 20)
|
|
mapM reserve [unpaid, expired, revoked] `shouldReturn` [False, False, False]
|
|
reserve free `shouldReturn` True
|
|
|
|
testOneCodePerRegistration :: IO ()
|
|
testOneCodePerRegistration = withServiceStore $ \st -> do
|
|
first <- issue st 1 usable
|
|
second <- issue st 2 usable
|
|
placeCode st "r1" ownerA (order "alicebakery" 1) first `shouldReturn` Right ()
|
|
withTransaction st (\db -> reservePrepaidCode db (pcId second) (RegistrationId "r1") t0) `shouldThrow` anyException
|
|
free <- withTransaction st $ \db -> getPrepaidCodeById db (pcId second)
|
|
(pcReservedBy =<< free) `shouldBe` Nothing
|
|
|
|
testRevoke :: IO ()
|
|
testRevoke = withServiceStore $ \st -> do
|
|
reserved <- issue st 1 usable
|
|
spent <- issue st 2 usable
|
|
unused <- issue st 3 usable
|
|
placeCode st "r1" ownerA (order "alicebakery" 1) reserved `shouldReturn` Right ()
|
|
placeCode st "r2" ownerA (order "bobsbakery" 2) spent `shouldReturn` Right ()
|
|
withTransaction st (\db -> spendPrepaidCode db (pcId spent) (RegistrationId "r2") t0) `shouldReturn` True
|
|
let revoke code = withTransaction st $ \db -> revokePrepaidCode db (pcCodeHash code) (at 1)
|
|
revoke reserved `shouldReturn` Revoked (pcId reserved)
|
|
revoke unused `shouldReturn` Revoked (pcId unused)
|
|
revoke unused `shouldReturn` AlreadyRevoked (pcId unused)
|
|
revoke spent `shouldReturn` AlreadyRedeemed
|
|
withTransaction st (\db -> revokePrepaidCode db (digestFixture 9) t0) `shouldReturn` NoSuchCode
|
|
held <- withTransaction st $ \db -> getPrepaidCodeById db (pcId reserved)
|
|
(pcReservedBy =<< held, pcRevokedAt =<< held) `shouldBe` (Just (RegistrationId "r1"), Just (at 1))
|
|
|
|
newEdit :: Address -> Word8 -> RecordChange -> NewRecordEdit
|
|
newEdit owner n change =
|
|
NewRecordEdit
|
|
{ nreLabelHash = labelHash "alicebakery",
|
|
nreOwner = owner,
|
|
nreNonce = 4,
|
|
nreChange = change,
|
|
nreDeadline = 1792832400,
|
|
nreSignature = signature,
|
|
nreBodyHash = digestFixture n
|
|
}
|
|
|
|
setChannel :: RecordChange
|
|
setChannel = RCSetText "simplex.channel" "https://smp9.simplex.im/c#abc"
|
|
|
|
testTransactions :: IO ()
|
|
testTransactions = withServiceStore $ \st -> do
|
|
code <- issue st 1 usable
|
|
placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right ()
|
|
Just editId <- withTransaction st $ \db -> insertRecordEdit db (newEdit ownerA 20 setChannel) t0
|
|
let r1 = RegistrationId "r1"
|
|
tx :: Word8 -> NewTransaction
|
|
tx n = NewTransaction {ntxRaw = digestFixture (n + 100), ntxHash = TxHash $ digestFixture n, ntxNonce = fromIntegral n}
|
|
[commitId, _, registerId] <- withTransaction st $ \db ->
|
|
sequence [insertTransaction db (TxCommit r1) (tx 2) (at 1), insertTransaction db (TxEdit editId) (tx 1) (at 2), insertTransaction db (TxRegister r1) (tx 3) (at 3)]
|
|
unsettled <- withTransaction st pendingTransactions
|
|
map (\NameTransaction {ntFor, ntNonce, ntStatus} -> (ntFor, ntNonce, ntStatus)) unsettled
|
|
`shouldBe` [(TxEdit editId, 1, TSPending), (TxCommit r1, 2, TSPending), (TxRegister r1, 3, TSPending)]
|
|
map ntRaw unsettled `shouldBe` map (\n -> digestFixture (n + 100)) [1, 2, 3]
|
|
let outcome n from status block = withTransaction st $ \db -> recordTransactionOutcome db (TxHash $ digestFixture n) from status block
|
|
outcome 2 TSPending TSMined (Just 77) `shouldReturn` True
|
|
outcome 1 TSPending TSReverted (Just 78) `shouldReturn` True
|
|
outcome 50 TSPending TSMined (Just 79) `shouldReturn` False
|
|
outcome 3 TSMined TSPending Nothing `shouldReturn` False
|
|
map ntFor <$> withTransaction st pendingTransactions `shouldReturn` [TxRegister r1]
|
|
ofRegistration <- withTransaction st $ \db -> registrationTransactions db r1
|
|
map (\NameTransaction {ntId, ntFor, ntMinedBlock, ntStatus, ntSentAt} -> (ntId, ntFor, ntMinedBlock, ntStatus, ntSentAt)) ofRegistration
|
|
`shouldBe` [(commitId, TxCommit r1, Just 77, TSMined, at 1), (registerId, TxRegister r1, Nothing, TSPending, at 3)]
|
|
map ntStatus <$> withTransaction st (\db -> editTransactions db editId) `shouldReturn` [TSReverted]
|
|
|
|
testTransactionFromStatus :: IO ()
|
|
testTransactionFromStatus = withServiceStore $ \st -> do
|
|
code <- issue st 1 usable
|
|
placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right ()
|
|
let outcome from status block = withTransaction st $ \db -> recordTransactionOutcome db (TxHash $ digestFixture 2) from status block
|
|
current = withTransaction st $ \db -> map (\NameTransaction {ntStatus, ntMinedBlock} -> (ntStatus, ntMinedBlock)) <$> registrationTransactions db (RegistrationId "r1")
|
|
void $ withTransaction st $ \db -> insertTransaction db (TxCommit $ RegistrationId "r1") NewTransaction {ntxRaw = "raw", ntxHash = TxHash $ digestFixture 2, ntxNonce = 0} t0
|
|
outcome TSPending TSMined (Just 77) `shouldReturn` True
|
|
outcome TSPending TSDropped Nothing `shouldReturn` False
|
|
current `shouldReturn` [(TSMined, Just 77)]
|
|
outcome TSMined TSPending Nothing `shouldReturn` True
|
|
current `shouldReturn` [(TSPending, Nothing)]
|
|
|
|
testEditNonceUnique :: IO ()
|
|
testEditNonceUnique = withServiceStore $ \st -> do
|
|
let run action = withTransaction st action
|
|
Just first <- run $ \db -> insertRecordEdit db (newEdit ownerA 1 setChannel) t0
|
|
run (\db -> insertRecordEdit db (newEdit ownerA 2 RCClear) t0) `shouldReturn` Nothing
|
|
Just other <- run $ \db -> insertRecordEdit db (newEdit ownerB 3 setChannel) t0
|
|
other `shouldNotBe` first
|
|
run (\db -> settleRecordEdit db first (ESReverted (Just "SignatureExpired"))) `shouldReturn` True
|
|
Just second <- run $ \db -> insertRecordEdit db (newEdit ownerA 2 RCClear) (at 1)
|
|
fmap reId <$> run (\db -> getRecordEditByNonce db ownerA 4) `shouldReturn` Just second
|
|
fmap reState <$> run (\db -> getRecordEditByBody db (digestFixture 1)) `shouldReturn` Just (ESReverted (Just "SignatureExpired"))
|
|
run (\db -> settleRecordEdit db second (ESMined Nothing)) `shouldReturn` True
|
|
run (\db -> insertRecordEdit db (newEdit ownerA 5 setChannel) (at 2)) `shouldReturn` Nothing
|
|
|
|
testSettleEdit :: IO ()
|
|
testSettleEdit = withServiceStore $ \st -> do
|
|
Just editId <- withTransaction st $ \db -> insertRecordEdit db (newEdit ownerA 1 setChannel) t0
|
|
withTransaction st (\db -> settleRecordEdit db editId (ESMined (Just $ TxHash $ digestFixture 9))) `shouldReturn` True
|
|
withTransaction st (\db -> settleRecordEdit db editId (ESReverted Nothing)) `shouldReturn` False
|
|
edit <- withTransaction st $ \db -> getRecordEdit db editId
|
|
edit
|
|
`shouldBe` Just
|
|
RecordEdit
|
|
{ reId = editId,
|
|
reLabelHash = labelHash "alicebakery",
|
|
reOwner = ownerA,
|
|
reNonce = 4,
|
|
reChange = setChannel,
|
|
reDeadline = 1792832400,
|
|
reSignature = signature,
|
|
reBodyHash = digestFixture 1,
|
|
reState = ESMined (Just $ TxHash $ digestFixture 9),
|
|
reCreatedAt = t0
|
|
}
|
|
|
|
testClearEdit :: IO ()
|
|
testClearEdit = withServiceStore $ \st -> do
|
|
Just editId <- withTransaction st $ \db -> insertRecordEdit db (newEdit ownerA 1 RCClear) t0
|
|
fmap (\RecordEdit {reChange, reState} -> (reChange, reState)) <$> withTransaction st (\db -> getRecordEdit db editId)
|
|
`shouldReturn` Just (RCClear, ESPending)
|
|
|
|
testRelayedEditTimes :: IO ()
|
|
testRelayedEditTimes = withServiceStore $ \st -> do
|
|
forM_ (zip [1 ..] [0, 10, 20]) $ \(n, m) ->
|
|
withTransaction st $ \db -> void $ insertRecordEdit db (newEdit ownerA n setChannel) {nreNonce = fromIntegral n} (at m)
|
|
withTransaction st (\db -> void $ insertRecordEdit db (newEdit ownerA 9 setChannel) {nreLabelHash = labelHash "bobsbakery", nreNonce = 9} (at 15))
|
|
withTransaction st (\db -> relayedEditTimes db (labelHash "alicebakery") (at 5)) `shouldReturn` [at 10, at 20]
|