packages feed

ihp-ide-1.4.0: IHP/Test/Database.hs

module IHP.Test.Database (withIHPApp) where

import IHP.Prelude
import qualified Database.PostgreSQL.Simple as PG
import qualified Database.PostgreSQL.Simple.Types as PG
import qualified Data.UUID.V4 as UUID
import qualified Data.UUID as UUID
import qualified Data.Text as Text
import qualified Control.Exception as Exception
import IHP.IDE.CodeGen.MigrationGenerator (findIHPSchemaSql)


import qualified Data.Vault.Lazy                           as Vault
import           Network.Wai
import           Network.Wai.Internal                      (ResponseReceived (..))


import           IHP.ApplicationContext                    (ApplicationContext (..))
import qualified IHP.AutoRefresh.Types                     as AutoRefresh
import           IHP.Controller.RequestContext             (RequestBody (..), RequestContext (..))
import           IHP.ControllerSupport                     (InitControllerContext, Controller, runActionWithNewContext)
import           IHP.FrameworkConfig                       (ConfigBuilder (..), FrameworkConfig (..))
import qualified IHP.FrameworkConfig                       as FrameworkConfig
import           IHP.ModelSupport                          (createModelContext, Id')
import           IHP.Prelude
import           IHP.Log.Types
import qualified IHP.PGListener as PGListener
import IHP.Controller.Session (sessionVaultKey)

import qualified System.Process as Process
import IHP.Test.Mocking

withConnection databaseUrl = Exception.bracket (PG.connectPostgreSQL databaseUrl) PG.close

-- | Create contexts that can be used for mocking
withIHPApp :: (InitControllerContext application) => application -> ConfigBuilder -> (MockContext application -> IO ()) -> IO ()
withIHPApp application configBuilder hspecAction = do
    FrameworkConfig.withFrameworkConfig configBuilder \frameworkConfig -> do
        let FrameworkConfig { dbPoolMaxConnections, dbPoolIdleTime } = frameworkConfig

        logger <- newLogger def { level = Warn } -- don't log queries


        withTestDatabase frameworkConfig.databaseUrl \testDatabaseUrl -> do
            modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections testDatabaseUrl logger

            PGListener.withPGListener modelContext \pgListener -> do
                autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener)
                let sessionVault = Vault.insert sessionVaultKey mempty Vault.empty
                let applicationContext = ApplicationContext { modelContext = modelContext, autoRefreshServer, frameworkConfig, pgListener }

                let requestContext = RequestContext
                     { request = defaultRequest {vault = sessionVault}
                     , requestBody = FormBody [] []
                     , respond = const (pure ResponseReceived)
                     , frameworkConfig = frameworkConfig }

                (hspecAction MockContext { .. })

withTestDatabase masterDatabaseUrl callback = do
    testDatabaseName <- randomDatabaseName

    withConnection masterDatabaseUrl \masterConnection ->
        Exception.bracket_
            (PG.execute masterConnection "CREATE DATABASE ?" [PG.Identifier testDatabaseName])
            (
                -- The WITH FORCE is required to force close open connections
                -- Otherwise the DROP DATABASE takes a few seconds to execute
                PG.execute masterConnection "DROP DATABASE ? WITH (FORCE)" [PG.Identifier testDatabaseName]
            )
            (do
                importSchema (testDatabaseUrl masterDatabaseUrl testDatabaseName)
                callback (testDatabaseUrl masterDatabaseUrl testDatabaseName)
            )

testDatabaseUrl :: ByteString -> Text -> ByteString
testDatabaseUrl masterDatabaseUrl testDatabaseName =
    masterDatabaseUrl
        |> cs
        |> Text.replace "postgresql:///app" ("postgresql:///" <> testDatabaseName)
        |> cs

randomDatabaseName :: IO Text
randomDatabaseName = do
    databaseId <- UUID.nextRandom
    pure ("test-" <> UUID.toText databaseId)

importSchema :: ByteString -> IO ()
importSchema databaseUrl = do
    -- We use the system psql to handle the initial Schema Import as it can handle
    -- complex Schema including variations in formatting, custom types, functions, and table definitions.
    let importSql file = Process.callCommand ("psql " <> (cs databaseUrl) <> " < " <> file)

    ihpSchemaSql <- findIHPSchemaSql
    importSql ihpSchemaSql
    importSql "Application/Schema.sql"