diff --git a/PaymentServer.cabal b/PaymentServer.cabal index 6aa2bf4a74aaceef8f0dae146de76c7991c51adb..8ef0a3a13bb8f7e8d0c8eb2325fc4b08c45fb6d9 100644 --- a/PaymentServer.cabal +++ b/PaymentServer.cabal @@ -41,6 +41,7 @@ test-suite PaymentServer-test hs-source-dirs: test main-is: Driver.hs other-modules: SpecStripe + , SpecPersistence , Util.WAI , Util.Gen , Util.JSON diff --git a/src/PaymentServer/Persistence.hs b/src/PaymentServer/Persistence.hs index 23b95dce86da68508118ae4829714ec371e29316..4fa499a860b7179a682a2efab78168cac2a40e5a 100644 --- a/src/PaymentServer/Persistence.hs +++ b/src/PaymentServer/Persistence.hs @@ -1,9 +1,14 @@ module PaymentServer.Persistence ( Voucher + , Fingerprint + , RedeemError(NotPaid, AlreadyRedeemed) , VoucherDatabase(payForVoucher, redeemVoucher) , memory ) where +import Control.Monad + ( liftM + ) import Data.Text ( Text ) @@ -13,6 +18,7 @@ import Data.IORef ( IORef , newIORef , modifyIORef + , readIORef ) -- | A voucher is a unique identifier which can be associated with a payment. @@ -27,6 +33,7 @@ data RedeemError = NotPaid -- | The voucher has already been redeemed. | AlreadyRedeemed + deriving (Show, Eq) -- | A fingerprint cryptographically identifies a redemption of a voucher. -- When a voucher is redeemed, a number of random tokens are received @@ -70,6 +77,21 @@ instance VoucherDatabase MemoryVoucherDatabase where modifyIORef paid (Set.insert voucher) return () + redeemVoucher Memory{ paid = paid, redeemed = redeemed } voucher fingerprint = do + unpaid <- (liftM $ Set.notMember voucher) . readIORef $ paid + existingFingerprint <- (liftM $ Map.lookup voucher) . readIORef $ redeemed + case (unpaid, existingFingerprint) of + (True, _) -> + return $ Left NotPaid + (False, Nothing) -> do + modifyIORef redeemed (Map.insert voucher fingerprint) + return $ Right () + (False, Just fingerprint') -> + if fingerprint == fingerprint' then + return $ Right () + else + return $ Left AlreadyRedeemed + -- | Create a new, empty MemoryVoucherDatabase. memory :: IO MemoryVoucherDatabase memory = do diff --git a/test/SpecPersistence.hs b/test/SpecPersistence.hs new file mode 100644 index 0000000000000000000000000000000000000000..8d1c21c3865ce382b25a6cf33c3fb96424767d7d --- /dev/null +++ b/test/SpecPersistence.hs @@ -0,0 +1,79 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +-- +-- Test suite related to the persistence system. +-- + +module SpecPersistence where + +import Test.QuickCheck + ( Property + , (===) + ) +import Control.Monad.IO.Class + ( liftIO + ) +import Test.QuickCheck.Monadic + ( monadicIO + , run + , assert + , pre + ) +import PaymentServer.Persistence + ( RedeemError(NotPaid, AlreadyRedeemed) + , Voucher + , Fingerprint + , VoucherDatabase(payForVoucher, redeemVoucher) + , memory + ) + +-- | A voucher which has not been paid for cannot be redeemed. +unpaidVoucherNotRedeemable :: VoucherDatabase d => IO d -> Voucher -> Fingerprint -> Property +unpaidVoucherNotRedeemable getDB voucher fingerprint = monadicIO $ do + db <- liftIO getDB + result <- run $ redeemVoucher db voucher fingerprint + assert (result == Left NotPaid) + +-- | The in-memory implementation for unpaidVoucherNotRedeemable. +prop_memory_unpaidVoucherNotRedeemable = unpaidVoucherNotRedeemable memory + +-- | A voucher which is paid for can be redeemed with any fingerprint. +paidVoucherRedeemable :: VoucherDatabase d => IO d -> Voucher -> Fingerprint -> Property +paidVoucherRedeemable getDB voucher fingerprint = monadicIO $ do + db <- liftIO getDB + () <- run $ payForVoucher db voucher + result <- run $ redeemVoucher db voucher fingerprint + assert (result == Right ()) + +-- | The in-memory implementation for paidVoucherRedeemable. +prop_memory_paidVoucherRedeemable = paidVoucherRedeemable memory + +-- | A voucher which is paid for can be redeemed more than once as long as the +-- same fingerprint is used each time. +paidVoucherMultiRedeemable :: VoucherDatabase d => IO d -> Voucher -> Fingerprint -> Property +paidVoucherMultiRedeemable getDB voucher fingerprint = monadicIO $ do + db <- liftIO getDB + () <- run $ payForVoucher db voucher + let redeem = redeemVoucher db voucher fingerprint + run redeem + result <- run redeem + assert (result == Right ()) + +-- | The in-memory implementation for paidVoucherMultiRedeemable. +prop_memory_paidVoucherMultiRedeemable = paidVoucherMultiRedeemable memory + +-- | A voucher which is paid for can not be redeemed a second time with a +-- different fingerprint than was used on the first attempt. +paidVoucherMismatchFingerprint :: VoucherDatabase d => IO d -> Voucher -> Fingerprint -> Fingerprint -> Property +paidVoucherMismatchFingerprint getDB voucher fingerprint fingerprint' = monadicIO $ do + pre (fingerprint /= fingerprint') + db <- liftIO getDB + () <- run $ payForVoucher db voucher + let redeem = redeemVoucher db voucher + run $ redeem fingerprint + result <- run $ redeem fingerprint' + assert (result == Left AlreadyRedeemed) + +-- | The in-memory implementation for paidVoucherMismatchFingerprint. +prop_memory_paidVoucherMismatchFingerprint = paidVoucherMismatchFingerprint memory