packages feed

pg-migrate-embed-1.0.0.0: test/unit/Test/Component.hs

module Test.Component (tests) where

import Data.ByteString (ByteString)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Set qualified as Set
import Data.Text (Text)
import Database.PostgreSQL.Migrate qualified as Migrate
import Database.PostgreSQL.Migrate.Embed (checkMigrationManifest)
import Database.PostgreSQL.Migrate.Internal qualified as Internal
import Paths_pg_migrate_embed qualified as Paths
import System.FilePath ((</>))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "embedded component"
    [ testCase "manifest order and suffix-derived names are authoritative" $ do
        entries <- validEntries
        component <-
          assertRight
            ( Migrate.migrationComponentFromEmbeddedSql
                "example-component"
                Set.empty
                entries
            )
        plan <- assertRight (Migrate.migrationPlan (component :| []))
        let descriptions = migrationDescriptions (Internal.planDescription plan)
        fmap Internal.migrationId descriptions
          @?= ( assertMigrationId "example-component" "0002-second"
                  :| [assertMigrationId "example-component" "0001-first"]
              ),
      testCase "each embedded file retains its own derived transaction mode" $ do
        component <-
          assertRight
            ( Migrate.migrationComponentFromEmbeddedSql
                "example-component"
                Set.empty
                ( ("0001-transactional.sql", "SELECT 1")
                    :| [ ( "0002-nontransactional.sql",
                           "-- pg-migrate: no-transaction\nCREATE INDEX CONCURRENTLY example_idx ON example (id)"
                         )
                       ]
                )
            )
        plan <- assertRight (Migrate.migrationPlan (component :| []))
        fmap Internal.transactionMode (migrationDescriptions (Internal.planDescription plan))
          @?= (Internal.Transactional :| [Internal.NonTransactional]),
      testCase "invalid SQL keeps its structured definition error" $
        assertDefinitionLeft
          (Migrate.InvalidSql (Migrate.ProhibitedTransactionCommand "BEGIN"))
          ( Migrate.migrationComponentFromEmbeddedSql
              "example-component"
              Set.empty
              (("0001-invalid.sql", "BEGIN") :| [])
          ),
      testCase "manual entries must have the SQL suffix" $
        assertDefinitionLeft
          (Migrate.InvalidEmbeddedMigrationFile "0001-invalid.txt")
          ( Migrate.migrationComponentFromEmbeddedSql
              "example-component"
              Set.empty
              (("0001-invalid.txt", "SELECT 1") :| [])
          )
    ]

validEntries :: IO (NonEmpty (FilePath, ByteString))
validEntries = do
  manifestPath <- Paths.getDataFileName ("test/fixtures" </> "valid/migrations/manifest")
  result <- checkMigrationManifest manifestPath
  case result of
    Left err -> assertFailure ("expected valid fixture, received Left " <> show err)
    Right entries -> pure entries

migrationDescriptions :: Internal.PlanDescription -> NonEmpty Internal.MigrationDescription
migrationDescriptions (Internal.PlanDescription (component :| [])) = Internal.migrations component
migrationDescriptions _ = error "expected one component"

assertMigrationId :: Text -> Text -> Migrate.MigrationId
assertMigrationId component name =
  case Migrate.migrationId component name of
    Left err -> error (show err)
    Right value -> value

assertRight :: (Show error) => Either error value -> IO value
assertRight = \case
  Left err -> assertFailure ("expected Right, received Left " <> show err)
  Right value -> pure value

assertDefinitionLeft ::
  Migrate.DefinitionError ->
  Either Migrate.DefinitionError value ->
  IO ()
assertDefinitionLeft expected = \case
  Left actual -> actual @?= expected
  Right _ -> assertFailure ("expected Left " <> show expected <> ", received Right")