mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 17:58:47 +00:00
* badges: webapp (#7433) * badges: service migrations, store and catalog * badges: BTCPay provider and settlement poller * badges: web listener and /api endpoints * web: checkout single-page app * badges: tests and BTCPay fixtures * badges: README and ini reference * badges: fix hex16 build on GHC 8.10.7 * badges: Stripe card lane * badges: fix Stripe card checkout, add theming * badges: add a discount row to the order summary * badges: site navbar, embedding, theme, Forget move * badges: use SB code prefix in web checkout * badges: rename sxb app namespace to sb * badges: embed checkout nav via site; keep original app navbar * badges: post iframe height, apply site background when embedded * badges: embed dark surfaces, steadier iframe height * badges: hide app footer when embedded * badges: size embedded body to content, not viewport * badges: declare color-scheme to stop reload flash * badges: fade shell in on load, no reload blank * badges: prerender app shell into index.html * badges: pre-paint theme, hide shell on deep reload * badges: logo returns to landing client-side * badges: embedded wizard back, buy-a-code, resume * badges: signal app-managed screens, resume across reload * badges: rebuild wizard history on deep load so Back walks it * badges: carry welcome-page height as the iframe floor * badges: keep selection on Buy a code; rename to Your codes * badges: read web shell as UTF-8, not locale * badges: resume the exact paid order after Stripe card redirect * badges: move docker deploy under scripts * badges: add serve_webapp toggle and webapp export * badges: wire split webapp deploy in docker config * badges: quiet agent logs by default * badges: resume card redirect in the embedded frame * badges: migrate Stripe adapter to PaymentIntents * badges: correct Stripe restricted key scopes in ini example * badges: card via Payment Element and PaymentIntents * badges: fix stale Checkout Session wording in Stripe adapter * badges: fix stale CheckoutActions reference in card comment * badges: order shell stylesheet before bootstrap script * badges: remove development card stand-in * badges: theme the Stripe card form with the site palette * badges: exclude web from the Haskell build stage * badges: unify invoice cancel and mark canceled * badges: default log level to info * badges: unify closed-invoice buy-again button * badges: mute agent connection logs at info level * badges: show purchase time in local timezone in Your codes * badges: log service events on own channel, quiet agent * badges: fold service migrations into one baseline * badges: run compose on postgres over host network * badges: use high-res hero art * badges: add web CI to catch stale builds * badges: rebuild web shell from committed source * badges: normalize invoice-code link and columns * badges: drop unused columns, rename index * badges: note deferred receipt_hash in migrations * badges: apply code-review fixes * badges: reduce comments across service and web --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> * badges: improve web page (#7546) * badges: improve web page * improve layout * improve layout * fix * small changes --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> * badges: read one issuer key from the ini * badges: move and group the service tests * badges: service fixes (#7567) * badges: match the redeem error wording in tests * badges: drop unused imports in the bot tests * badges: cancel Stripe orders when they expire * badges: correct the Stripe config and docs * badges: refuse to revoke a redeemed code * badges: make the fake Stripe cancel like Stripe * badges: limit replayed webhook deliveries --------- Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com> Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> Co-authored-by: shum <github.shum@liber.li> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
147 lines
5.8 KiB
Haskell
147 lines
5.8 KiB
Haskell
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NumericUnderscores #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
|
|
module Bots.BadgeService.WaitersTests (badgeWaitersTests) where
|
|
|
|
import BadgeService.Waiters
|
|
import Control.Concurrent.Async (wait, withAsync)
|
|
import Control.Concurrent.STM
|
|
import Control.Exception (Exception, SomeException, throwIO, try)
|
|
import Data.IORef (newIORef, readIORef)
|
|
import Data.Text (Text)
|
|
import Simplex.Chat.PaymentService.Types (InvoiceId (..), InvoiceStatus (..))
|
|
import System.Timeout (timeout)
|
|
import Test.Hspec
|
|
|
|
badgeWaitersTests :: Spec
|
|
badgeWaitersTests = describe "badge service waiters" $ do
|
|
it "wakes on a status published between subscribe and the database read" testSubscribeBeforeRead
|
|
it "answers a payment whose publish landed before this request subscribed" testPaymentPublishedBeforeSubscribe
|
|
it "is not woken by a publish for a different invoice" testDifferentInvoiceNoWake
|
|
it "returns the current status, not an error, when the wait times out" testTimeoutReturnsCurrentStatus
|
|
it "empties the map when the waiting action throws" testMapEmptiesOnException
|
|
it "empties the map after a normal return" testMapEmptiesOnNormalReturn
|
|
it "keeps one map entry for two waiters on the same invoice" testRefcountSharedInvoice
|
|
it "counts one entry per invoice, not per waiter" testWaitingCountPerInvoice
|
|
|
|
iid, iid2 :: InvoiceId
|
|
iid = InvoiceId "inv-1"
|
|
iid2 = InvoiceId "inv-2"
|
|
|
|
-- | An invoice nobody has paid into carries no figure and no verdict from the provider.
|
|
unpaid :: (Text, Bool)
|
|
unpaid = ("", False)
|
|
|
|
openUnpaid :: Seen
|
|
openUnpaid = (ISOpen, unpaid)
|
|
|
|
data Boom = Boom deriving (Show)
|
|
|
|
instance Exception Boom
|
|
|
|
-- | This suite has hung rather than failed before. `us` must exceed every awaitStatus
|
|
-- timeout the test uses.
|
|
bounded :: String -> Int -> IO a -> IO a
|
|
bounded what us act =
|
|
timeout us act >>= \case
|
|
Just a -> pure a
|
|
Nothing -> do
|
|
expectationFailure (what <> " did not finish within " <> show us <> "us")
|
|
error "unreachable: expectationFailure always throws"
|
|
|
|
testSubscribeBeforeRead :: IO ()
|
|
testSubscribeBeforeRead = bounded "the ordering wait" 2_000_000 $ do
|
|
w <- newWaiters
|
|
ref <- newIORef ISOpen
|
|
let readStatus = do
|
|
atomically $ publish w iid ISPaid
|
|
(\s -> (s, unpaid)) <$> readIORef ref
|
|
s <- awaitStatus w iid readStatus openUnpaid 200_000
|
|
s `shouldBe` ISPaid
|
|
|
|
testDifferentInvoiceNoWake :: IO ()
|
|
testDifferentInvoiceNoWake = bounded "the isolation wait" 2_000_000 $ do
|
|
w <- newWaiters
|
|
otherSubscribed <- newEmptyTMVarIO
|
|
let otherRead = atomically (putTMVar otherSubscribed ()) >> pure openUnpaid
|
|
withAsync (awaitStatus w iid2 otherRead openUnpaid 200_000) $ \other ->
|
|
withAsync (awaitStatus w iid (pure openUnpaid) openUnpaid 200_000) $ \watched -> do
|
|
atomically $ takeTMVar otherSubscribed
|
|
atomically $ publish w iid2 ISPaid
|
|
otherResult <- wait other
|
|
otherResult `shouldBe` ISPaid
|
|
watchedResult <- wait watched
|
|
watchedResult `shouldBe` ISOpen
|
|
|
|
testTimeoutReturnsCurrentStatus :: IO ()
|
|
testTimeoutReturnsCurrentStatus = bounded "the timeout wait" 1_000_000 $ do
|
|
w <- newWaiters
|
|
s <- awaitStatus w iid (pure openUnpaid) openUnpaid 50_000
|
|
s `shouldBe` ISOpen
|
|
|
|
testMapEmptiesOnException :: IO ()
|
|
testMapEmptiesOnException = bounded "the exception-cleanup wait" 1_000_000 $ do
|
|
w <- newWaiters
|
|
_ <- try @SomeException $ awaitStatus w iid (throwIO Boom) openUnpaid 1_000_000
|
|
waitingCount w >>= (`shouldBe` 0)
|
|
|
|
testMapEmptiesOnNormalReturn :: IO ()
|
|
testMapEmptiesOnNormalReturn = bounded "the normal-return cleanup wait" 1_000_000 $ do
|
|
w <- newWaiters
|
|
_ <- awaitStatus w iid (pure openUnpaid) openUnpaid 50_000
|
|
waitingCount w >>= (`shouldBe` 0)
|
|
|
|
testRefcountSharedInvoice :: IO ()
|
|
testRefcountSharedInvoice = bounded "the shared-invoice refcount wait" 2_000_000 $ do
|
|
w <- newWaiters
|
|
entered <- newTVarIO (0 :: Int)
|
|
go <- newTVarIO False
|
|
let readStatus = do
|
|
atomically $ modifyTVar' entered (+ 1)
|
|
atomically $ readTVar go >>= check
|
|
pure openUnpaid
|
|
withAsync (awaitStatus w iid readStatus openUnpaid 500_000) $ \a1 ->
|
|
withAsync (awaitStatus w iid readStatus openUnpaid 500_000) $ \a2 -> do
|
|
atomically $ readTVar entered >>= check . (== 2)
|
|
waitingCount w >>= (`shouldBe` 1)
|
|
atomically $ writeTVar go True
|
|
atomically $ publish w iid ISPaid
|
|
r1 <- wait a1
|
|
r2 <- wait a2
|
|
r1 `shouldBe` ISPaid
|
|
r2 `shouldBe` ISPaid
|
|
waitingCount w >>= (`shouldBe` 0)
|
|
|
|
testWaitingCountPerInvoice :: IO ()
|
|
testWaitingCountPerInvoice = bounded "the per-invoice waiting-count wait" 2_000_000 $ do
|
|
w <- newWaiters
|
|
entered <- newTVarIO (0 :: Int)
|
|
go <- newTVarIO False
|
|
let readStatus = do
|
|
atomically $ modifyTVar' entered (+ 1)
|
|
atomically $ readTVar go >>= check
|
|
pure openUnpaid
|
|
withAsync (awaitStatus w iid readStatus openUnpaid 500_000) $ \a1 ->
|
|
withAsync (awaitStatus w iid2 readStatus openUnpaid 500_000) $ \a2 -> do
|
|
atomically $ readTVar entered >>= check . (== 2)
|
|
waitingCount w >>= (`shouldBe` 2)
|
|
atomically $ writeTVar go True
|
|
atomically $ publish w iid ISPaid
|
|
atomically $ publish w iid2 ISPaid
|
|
r1 <- wait a1
|
|
r2 <- wait a2
|
|
r1 `shouldBe` ISPaid
|
|
r2 `shouldBe` ISPaid
|
|
waitingCount w >>= (`shouldBe` 0)
|
|
|
|
-- | The payment committed before this request subscribed, so its publish found no watch and
|
|
-- the counter cannot report it. The read after subscribing is the only thing that can, and a
|
|
-- request that waits for the counter alone parks for its whole timeout with the money already in.
|
|
testPaymentPublishedBeforeSubscribe :: IO ()
|
|
testPaymentPublishedBeforeSubscribe = do
|
|
w <- newWaiters
|
|
s <- bounded "the lost-publish wait" 1_000_000 $ awaitStatus w iid (pure (ISOpen, ("0.005", True))) openUnpaid 30_000_000
|
|
s `shouldBe` ISOpen
|