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"