packages feed

trek-app-0.1.0.0: test/Tests/Database/Trek/RunSpec.hs

module Tests.Database.Trek.RunSpec where
import Database.Trek.Run
import Test.Hspec
import Data.Maybe
import Data.List.Split
import Data.Time.Format
import System.IO.Temp
import System.Directory
import Control.Exception
import Data.Time
import System.FilePath.Posix
import Data.List.NonEmpty (NonEmpty(..))
import qualified Database.Postgres.Temp as Temp
import Control.Concurrent
import Control.Concurrent.Async
import Data.IORef
import Data.Foldable
import qualified Database.PostgreSQL.Simple.Options as P
import qualified Database.Trek.Db as Db
import Database.PostgreSQL.Simple.Types
import Data.Time.QQ
import qualified Database.PostgreSQL.Simple as Psql
import Paths_trek_app (getDataDir)

aroundAll :: forall a. ((a -> IO ()) -> IO ()) -> SpecWith a -> Spec
aroundAll withFunc specWith = do
  (var, stopper, asyncer) <- runIO $
    (,,) <$> newEmptyMVar <*> newEmptyMVar <*> newIORef Nothing
  let theStart :: IO a
      theStart = do

        thread <- async $ do
          withFunc $ \x -> do
            putMVar var x
            takeMVar stopper
          pure $ error "Don't evaluate this"

        writeIORef asyncer $ Just thread

        either pure pure =<< (wait thread `race` takeMVar var)

      theStop :: a -> IO ()
      theStop _ = do
        putMVar stopper ()
        traverse_ cancel =<< readIORef asyncer

  beforeAll theStart $ afterAll theStop $ specWith

foo :: String
foo = "CREATE SCHEMA IF NOT EXISTS test; CREATE TABLE test.foo (id SERIAL PRIMARY KEY)"

bar :: String
bar = "CREATE SCHEMA IF NOT EXISTS test; CREATE TABLE test.bar (id SERIAL PRIMARY KEY)"

quux :: String
quux = "CREATE SCHEMA IF NOT EXISTS test; CREATE TABLE test.quux (id SERIAL PRIMARY KEY)"

inputGroup :: NonEmpty FilePath -> IO FilePath
inputGroup = error "inputGroup"

withSetup :: (P.Options -> IO a) -> IO a
withSetup f = do
  -- Helper to throw exceptions
  let throwE x = either throwIO pure =<< x

  throwE $ Temp.withDbCache $ \dbCache -> do
    let combinedConfig = Temp.defaultConfig <> Temp.cacheConfig dbCache
    Temp.withConfig combinedConfig $ \db -> f $ Temp.toConnectionOptions db


spec :: Spec
spec = do
  describe "Database.Trek.Run" $ do
    it "creates a file" $ do
      withSystemTempDirectory "trek-test" $ \tmp -> do
        old <- getCurrentDirectory
        bracket_ (setCurrentDirectory tmp) (setCurrentDirectory old) $ do
          createDirectory "path"
          let name = "path/migration.sql"
          output <- create name
          let (dir, theFileName) = splitFileName output
              [date, actualName] = splitOn "_" theFileName
          dir `shouldBe` "path/"
          actualName `shouldBe` "migration.sql"
          isJust (parseTimeM True defaultTimeLocale "%Y-%m-%dT%H-%M-%S" date :: Maybe UTCTime) `shouldBe` True
          doesFileExist output `shouldReturn` True

  aroundAll withSetup $ describe "Database.Trek.Run.apply" $ do
    it "empty directory does nothing" $ \options -> withSystemTempDirectory "trek-test" $ \tmp -> do
      apply options tmp `shouldReturn` Nothing
      -- Doing it twice should be the same
      apply options tmp `shouldReturn` Nothing

    it "standard migrations succeed" $ \options -> do
      dataDir <- fmap (</> "data") getDataDir

      Just (OutputGroup (Db.OutputGroup {ogMigrations})) <- apply options dataDir
      let fooM :| [barM, quuxM] = ogMigrations
      fooM  `shouldBe` Db.OutputMigration
        { Db.omVersion = [utcIso8601ms|2020-07-12T06:21:21.00000|]
        , Db.omHash = Binary
          { fromBinary = "L\DLE\137\195\169\&0\163o!I\189\253`\250\203\147\215\200\224\137S\160m{\179\227\240\ESC\194P-I" }
          }
      barM  `shouldBe` Db.OutputMigration
        { Db.omVersion = [utcIso8601ms| 2020-07-12T06:21:27.00000 |]
        , Db.omHash = Binary
          { fromBinary = "\ETX\225\155\215\184\144\147\DLEn\SO\195\175\&4\167\208~-\244S\146\&9\215K\223i\173\EOT\209A'Z7" }
          }
      quuxM `shouldBe` Db.OutputMigration
        { Db.omVersion = [utcIso8601ms| 2020-07-12T06:21:32.00000 |]
        , Db.omHash = Binary
          { fromBinary = "\DLE*\221\")\SO\204\207\EMdmn\b\197\233a\212-NA\133;\255\167/\t\133\139\163\222Tz" }
          }

      let action :: Psql.Connection -> IO [String]
          action conn = fmap Psql.fromOnly <$> Psql.query_ conn
            "SELECT CAST(table_name AS varchar) FROM information_schema.tables where table_schema = 'test' ORDER BY table_name"

      withOptions options action `shouldReturn` ["bar", "foo", "quux"]

    it "reapplying does nothing" $ \options -> do
      (apply options . (</> "data") =<< getDataDir) `shouldReturn` Nothing