{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE TypeFamilies #-} module Utils where import Control.Exception.Safe import Control.Monad.IO.Class import Control.Monad.Reader import Data.ByteString (ByteString) import Data.Kind import Data.Pool (Pool) import qualified Data.Pool as Pool import Data.Text (Text) import Data.Time import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.Vector as V import Data.Word import Database.PostgreSQL.Entity.DBT (withPool) import Database.PostgreSQL.Simple (Connection, SqlError (..)) import Database.PostgreSQL.Transact import Env import GHC.Generics import Hedgehog (MonadGen (..)) import qualified Hedgehog.Gen as H import qualified Hedgehog.Range as Range import Optics.Core import Test.Tasty (TestTree) import qualified Test.Tasty as Test import qualified Test.Tasty.HUnit as Test import BlogPost import qualified Database.PostgreSQL.Simple as PG import Database.PostgreSQL.Simple.Migration newtype TestM (a :: Type) = TestM {getTestM :: ReaderT TestEnv IO a} deriving newtype (Applicative, Functor, Monad, MonadIO, MonadThrow) data TestEnv = TestEnv { pool :: Pool Connection , connectionInfo :: ByteString } deriving stock (Generic) data TestConfig = TestConfig { connectionInfo :: ByteString } deriving stock (Generic, Show, Eq) liftDB :: DBT IO a -> TestM a liftDB comp = do env <- getTestEnv let pool = env ^. #pool liftIO $ catch (withPool pool comp) ( \(e :: SqlError) -> if sqlErrorMsg e == "connection disconnected" then withPool pool comp else throw e ) migrate :: Connection -> IO () migrate conn = void $ runMigrations conn defaultOptions [MigrationInitialization, MigrationDirectory "./test/migrations"] runTestM :: TestM a -> TestEnv -> IO a runTestM comp env = runReaderT (getTestM comp) env testThis :: String -> TestM () -> TestM TestTree testThis name assertion = do env <- getTestEnv let test = runTestM assertion env pure $ Test.testCase name test testThese :: String -> [TestM TestTree] -> TestM TestTree testThese groupName tests = fmap (Test.testGroup groupName) newTests where newTests :: TestM [TestTree] newTests = sequenceA tests getTestEnv :: TestM TestEnv getTestEnv = TestM ask assertEqual :: (Eq a, Show a) => a -> a -> TestM () assertEqual expected actual = liftIO $ Test.assertEqual "" expected actual -- genAuthorId :: MonadGen m => m AuthorId genAuthorId = AuthorId <$> genUUID genUUID :: MonadGen m => m UUID genUUID = UUID.fromWords <$> genWord32 <*> genWord32 <*> genWord32 <*> genWord32 where genWord32 :: MonadGen m => m Word32 genWord32 = H.word32 (Range.constant minBound maxBound) genUUIDList :: MonadGen m => m UUIDList genUUIDList = UUIDList . V.fromList <$> H.list (Range.linear 1 10) genUUID genUTCTime :: MonadGen m => m UTCTime genUTCTime = do year <- toInteger <$> H.int (Range.constant 2000 2022) month <- H.int (Range.constant 1 12) day <- H.int (Range.constant 1 28) let date = fromGregorian year month day secs <- toInteger <$> H.int (Range.constant 0 86401) pure $ UTCTime date (secondsToDiffTime secs) genName :: MonadGen m => m Text genName = H.text (Range.constant 3 25) H.unicode genAuthor :: MonadGen m => m Author genAuthor = do authorId <- genAuthorId name <- genName createdAt <- genUTCTime pure Author{..} data RandomAuthorTemplate m = RandomAuthorTemplate { generateAuthorId :: m AuthorId , generateName :: m Text , generateCreatedAt :: m UTCTime } deriving stock (Generic) randomAuthorTemplate :: MonadIO m => RandomAuthorTemplate m randomAuthorTemplate = RandomAuthorTemplate { generateAuthorId = H.sample genAuthorId , generateName = H.sample genName , generateCreatedAt = H.sample genUTCTime } randomAuthor :: MonadIO m => RandomAuthorTemplate m -> m Author randomAuthor RandomAuthorTemplate{..} = do authorId <- generateAuthorId name <- generateName createdAt <- generateCreatedAt pure Author{..} instantiateRandomAuthor :: (MonadIO m, m ~ IO) => RandomAuthorTemplate m -> DBT m Author instantiateRandomAuthor RandomAuthorTemplate{..} = do authorId <- liftIO generateAuthorId name <- liftIO generateName createdAt <- liftIO generateCreatedAt let author = Author{..} insertAuthor author pure author -- genBlogPost :: MonadGen m => m BlogPost genBlogPost = do blogPostId <- genBlogPostId authorId <- genAuthorId uuidList <- genUUIDList title <- genName content <- genName createdAt <- genUTCTime pure BlogPost{..} genBlogPostId :: MonadGen m => m BlogPostId genBlogPostId = BlogPostId <$> genUUID data RandomBlogPostTemplate m = RandomBlogPostTemplate { generateBlogPostId :: m BlogPostId , generateAuthorId :: m AuthorId , generateUUIDList :: m UUIDList , generateTitle :: m Text , generateContent :: m Text , generateCreatedAt :: m UTCTime } deriving stock (Generic) randomBlogPostTemplate :: MonadIO m => RandomBlogPostTemplate m randomBlogPostTemplate = RandomBlogPostTemplate { generateBlogPostId = H.sample genBlogPostId , generateAuthorId = H.sample genAuthorId , generateUUIDList = H.sample genUUIDList , generateTitle = H.sample genName , generateContent = H.sample genName , generateCreatedAt = H.sample genUTCTime } randomBlogPost :: MonadIO m => RandomBlogPostTemplate m -> m BlogPost randomBlogPost RandomBlogPostTemplate{..} = do blogPostId <- generateBlogPostId authorId <- generateAuthorId uuidList <- generateUUIDList title <- generateTitle content <- generateContent createdAt <- generateCreatedAt pure BlogPost{..} instantiateRandomBlogPost :: (MonadIO m, m ~ IO) => RandomBlogPostTemplate m -> DBT m BlogPost instantiateRandomBlogPost RandomBlogPostTemplate{..} = do blogPostId <- liftIO generateBlogPostId authorId <- liftIO generateAuthorId uuidList <- liftIO generateUUIDList title <- liftIO generateTitle content <- liftIO generateContent createdAt <- liftIO generateCreatedAt let blogPost = BlogPost{..} insertBlogPost blogPost pure blogPost retrieveTestEnv :: IO TestConfig retrieveTestEnv = Env.parse id parseTestConfig parseConnectionInfo :: Parser Error ByteString parseConnectionInfo = var str "DB_CONNSTRING" (help "libpq-compatible connection string") parseTestConfig :: Parser Error TestConfig parseTestConfig = TestConfig <$> parseConnectionInfo testConfigToTestEnv :: TestConfig -> IO TestEnv testConfigToTestEnv TestConfig{..} = do let connectionTimeout = 100 let maxResources = 10 pool <- mkPool connectionInfo connectionTimeout maxResources pure TestEnv{..} mkPool :: ByteString -- Database access information -> NominalDiffTime -- Allowed timeout -> Int -- Number of connections -> IO (Pool PG.Connection) mkPool connectionInfo timeout' maxResources = Pool.newPool $ Pool.setNumStripes (Just 10) $ Pool.defaultPoolConfig (PG.connectPostgreSQL connectionInfo) PG.close (realToFrac timeout') maxResources