packages feed

pg-transact-0.1.0.0: test/Database/PostgreSQL/TransactSpec.hs

{-# LANGUAGE QuasiQuotes          #-}
{-# LANGUAGE DeriveDataTypeable   #-}
{-# LANGUAGE ScopedTypeVariables  #-}
module Database.PostgreSQL.TransactSpec where

import           Control.Monad              (void)
import           Control.Monad.Catch
import qualified Data.ByteString.Char8      as BSC
import           Data.String
import           Data.Typeable
import qualified Database.PostgreSQL.Simple as PS
import           Database.PostgreSQL.Simple ( Connection
                                            , Only (..)
                                            , SqlError (..)
                                            )
import           Database.PostgreSQL.Simple.SqlQQ
import           Database.PostgreSQL.Transact
import qualified Database.Postgres.Temp as Temp
import           Test.Hspec

-------------------------       Test DB Creation       -------------------------
createDB :: IO (Connection, Temp.DB)
createDB = do
    Right tempDB <- Temp.startAndLogToTmp []
    let connectionString = Temp.connectionString tempDB
    connection <- PS.connectPostgreSQL $ BSC.pack connectionString
    void $ PS.execute_ connection $
        [sql| CREATE TABLE fruit (name VARCHAR(100) PRIMARY KEY ) |]
    return (connection, tempDB)

shutdown :: (Connection, Temp.DB) -> IO ()
shutdown (conn, db) = do
  PS.close conn
  void $ Temp.stop db

-------------------------        Test Utilities        -------------------------
insertFruit :: String -> DB ()
insertFruit fruit
  = void $ execute [sql| INSERT INTO fruit (name) VALUES (?) |] (Only fruit)

fruits :: Connection -> IO [String]
fruits conn
  = fmap (map fromOnly)
  $ PS.query_ conn [sql|SELECT name FROM fruit ORDER BY name|]

runDB :: Connection -> DB a -> IO a
runDB = flip runDBTSerializable

shouldBeM :: (Eq a, Show a) => IO a -> a -> IO ()
shouldBeM action expected = do
    actual <- action
    actual `shouldBe` expected

-- Simple exception type for testing
data Forbidden = Forbidden
    deriving (Show, Eq, Typeable)

instance Exception Forbidden

-------------------------         Tests Start          -------------------------
spec :: Spec
spec = describe "TransactionSpec" $ do
    -- Notice the 'beforeAll'. The second test uses the same db as the first
    beforeAll createDB $ afterAll shutdown $ do
        it "execute_ happen path succeeds" $ \(conn, _) -> do
            let apple = "apple"
            runDB conn $ insertFruit apple

            fruits conn `shouldBeM` ["apple"]

        it "execute_ rollbacks on exception" $ \(conn, _) -> do
            flip shouldThrow (\(SqlError {}) -> True) $
                runDB conn $ do
                    insertFruit "orange"
                    -- This should cause an exception because of the UNIQUE
                    -- constraint on 'name'
                    insertFruit "apple"

            fruits conn `shouldBeM` ["apple"]

    before createDB $ do
        it "multiple execute_'s succeed" $ \(conn, _) -> do
            runDB conn $ do
                insertFruit "grapes"
                insertFruit "orange"

            fruits conn `shouldBeM` ["grapes", "orange"]

        it "throwM causes a rollback" $ \(conn, _) -> do
            flip shouldThrow (\Forbidden -> True) $
                runDB conn $ do
                    insertFruit "salak"
                    () <- throwM Forbidden
                    insertFruit "banana"

            fruits conn `shouldBeM` []

        it "query recovers when exception is caught" $ \(conn, _) -> do
            runDB conn $ do
                -- This should always happen because of the handle below
                insertFruit "banana"
                handle (\Forbidden -> insertFruit "tomato") $ do
                    insertFruit "salak"
                    throwM Forbidden

            fruits conn `shouldBeM` ["banana", "tomato"]

        it "multiple catch statements work correctly" $ \(conn, _) -> do
            runDB conn $ do
                insertFruit "banana"
                handle (\Forbidden -> insertFruit "tomato") $ do
                    -- This will happen ... even if there is an exception below
                    -- if we catch it
                    insertFruit "blueberry"
                    handle (\Forbidden -> insertFruit "frankenberry") $ do
                        insertFruit "salak"
                        throwM Forbidden

            fruits conn `shouldBeM` ["banana", "blueberry", "frankenberry"]

        it "alternate branches can also have savepoints" $ \(conn, _) -> do
            runDB conn $ do
                insertFruit "banana"
                catch (insertFruit "tomato" >> throwM Forbidden) $
                    \Forbidden -> do
                        insertFruit "blueberry"
                        handle (\Forbidden -> insertFruit "frankenberry") $ do
                            insertFruit "salak"
                            throwM Forbidden

            fruits conn `shouldBeM` ["banana", "blueberry", "frankenberry"]

        it "releasing silently fails if the transaction errors" $ \(conn, _) -> do
            runDB conn $ do
                insertFruit "banana"
                catchAll (void $ execute_ [sql| ABORT |]) $
                    \_ -> insertFruit "tomato"

            fruits conn `shouldBeM` []