packages feed

poppy-1.0.0: test/Poppy/MigrateSpec.hs

module Poppy.MigrateSpec
  ( migrateSpec,
  )
where

import Control.Exception (bracket)
import Data.Int (Int64)
import Data.Text (Text)
import qualified Data.Text.IO as TIO
import Data.UUID.V4 (nextRandom)
import Database.PostgreSQL.Simple (Only (..))
import qualified Database.PostgreSQL.Simple as PG
import Poppy (MigrateError (..), applyMigrations)
import Poppy.Internal.Db (DbPool, withConn)
import Support.TestDb (TestEnv (..))
import System.Directory (createDirectoryIfMissing, getTemporaryDirectory, removeDirectoryRecursive)
import System.FilePath ((</>))
import Test.Hspec (SpecWith, describe, it, shouldBe, shouldSatisfy)

migrateSpec :: SpecWith TestEnv
migrateSpec =
  describe "Poppy.Internal.Migrate" $ do
    it "applies SQL files in name order and is a no-op on replay" $ \TestEnv {envPool = pool} ->
      withCleanProbe pool
        $ withMigrationDir
          [ ("notes.txt", "not sql"),
            ("002-add-label.sql", "ALTER TABLE _poppy_phase10_probe ADD COLUMN label TEXT;"),
            ("001-create.sql", "CREATE TABLE _poppy_phase10_probe (id INT PRIMARY KEY);")
          ]
        $ \dir -> do
          first <- applyMigrations pool dir
          first `shouldBe` Right ["001-create.sql", "002-add-label.sql"]
          columns <- probeColumns pool
          columns `shouldBe` 2
          second <- applyMigrations pool dir
          second `shouldBe` Right []
          probeColumns pool >>= (`shouldBe` 2)

    it "does not record a failed file" $ \TestEnv {envPool = pool} ->
      withCleanProbe pool
        $ withMigrationDir
          [ ("001-ok.sql", "CREATE TABLE _poppy_phase10_probe (id INT PRIMARY KEY);"),
            ("002-bad.sql", "SELECT * FROM definitely_not_a_poppy_migrate_table;")
          ]
        $ \dir -> do
          result <- applyMigrations pool dir
          result
            `shouldSatisfy` ( \case
                                Left (MigrateFailed "002-bad.sql" _) -> True
                                _ -> False
                            )
          recorded <- recordedNames pool
          recorded `shouldBe` ["001-ok.sql"]
          exists <- probeExists pool
          exists `shouldBe` True

    it "returns DirectoryError when the path is not a directory" $ \TestEnv {envPool = pool} -> do
      result <- applyMigrations pool "/definitely-not-a-poppy-migrate-dir"
      result
        `shouldSatisfy` ( \case
                            Left (MigrateDirectoryError _) -> True
                            _ -> False
                        )

withCleanProbe :: DbPool -> IO a -> IO a
withCleanProbe pool action = dropProbeState pool >> action <* dropProbeState pool

dropProbeState :: DbPool -> IO ()
dropProbeState pool =
  withConn pool $ \conn -> do
    _ <- PG.execute_ conn "DROP TABLE IF EXISTS _poppy_phase10_probe CASCADE"
    _ <- PG.execute_ conn "DROP TABLE IF EXISTS _poppy_migrations CASCADE"
    pure ()

withMigrationDir :: [(FilePath, Text)] -> (FilePath -> IO a) -> IO a
withMigrationDir files action = do
  tmp <- getTemporaryDirectory
  token <- nextRandom
  let dir = tmp </> ("poppy-migrate-" <> show token)
  bracket
    (createDirectoryIfMissing True dir >> pure dir)
    removeDirectoryRecursive
    ( \dir -> do
        mapM_ (\(name, body) -> TIO.writeFile (dir </> name) body) files
        action dir
    )

probeExists :: DbPool -> IO Bool
probeExists pool =
  withConn pool $ \conn -> do
    [Only exists] <-
      PG.query_
        conn
        "SELECT to_regclass('public._poppy_phase10_probe') IS NOT NULL"
    pure exists

probeColumns :: DbPool -> IO Int64
probeColumns pool =
  withConn pool $ \conn -> do
    [Only n] <-
      PG.query_
        conn
        "SELECT COUNT(*) FROM information_schema.columns \
        \WHERE table_schema = 'public' AND table_name = '_poppy_phase10_probe'"
    pure n

recordedNames :: DbPool -> IO [Text]
recordedNames pool =
  withConn pool $ \conn -> do
    rows <- PG.query_ conn "SELECT name FROM _poppy_migrations ORDER BY name"
    pure [name | Only name <- rows]