pg-migrate-cli-1.0.0.0: test/integration/Main.hs
module Main (main) where
import Control.Exception qualified as Exception
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Set qualified as Set
import Data.Text qualified as Text
import Database.PostgreSQL.Migrate
import Database.PostgreSQL.Migrate.CLI
import Hasql.Connection qualified as Connection
import Hasql.Connection.Settings qualified as Settings
import Hasql.Session qualified as Session
import System.Environment (lookupEnv)
import Test.Tasty
import Test.Tasty.HUnit
main :: IO ()
main = do
maybeConnectionString <- lookupEnv "PG_CONNECTION_STRING"
case maybeConnectionString of
Nothing ->
putStrLn
"pg-migrate-cli integration tests skipped: PG_CONNECTION_STRING is not set"
Just connectionString ->
defaultMain (tests (Settings.connectionString (Text.pack connectionString)))
tests :: Settings.Settings -> TestTree
tests settings =
testGroup
"pg-migrate CLI PostgreSQL"
[ testCase "status verify and full up preserve their contracts" (testCommandLifecycle settings),
testCase "confirmed repair dispatches through the shared lifecycle" (testRepairLifecycle settings)
]
testCommandLifecycle :: Settings.Settings -> Assertion
testCommandLifecycle settings =
withCleanLedger settings "pgmigrate_cli_commands" 0x70676D636C690001 $ \options -> do
let environment = cliEnvironment settings commandPlan options
statusBefore <- runMigrationCommand environment statusCommand
exitClass statusBefore @?= ExitSuccess
case payload statusBefore of
Right (StatusPayload (StatusReport issues applied pending unknown)) -> do
issues @?= []
applied @?= []
length pending @?= 1
unknown @?= []
other -> assertFailure ("unexpected status outcome: " <> show other)
verifyBefore <- runMigrationCommand environment verifyCommand
exitClass verifyBefore @?= ExitVerificationFailed
case payload verifyBefore of
Right (VerifyPayload (VerificationReport issues _ pending _)) -> do
length issues @?= 1
length pending @?= 1
other -> assertFailure ("unexpected pre-run verify outcome: " <> show other)
firstUp <- runMigrationCommand environment upCommand
exitClass firstUp @?= ExitSuccess
assertSingleOutcome AppliedNow firstUp
verifyAfter <- runMigrationCommand environment verifyCommand
exitClass verifyAfter @?= ExitSuccess
case payload verifyAfter of
Right (VerifyPayload (VerificationReport issues applied pending unknown)) -> do
issues @?= []
length applied @?= 1
pending @?= []
unknown @?= []
other -> assertFailure ("unexpected post-run verify outcome: " <> show other)
secondUp <- runMigrationCommand environment upCommand
exitClass secondUp @?= ExitSuccess
assertSingleOutcome AlreadyApplied secondUp
testRepairLifecycle :: Settings.Settings -> Assertion
testRepairLifecycle settings =
withCleanLedger settings "pgmigrate_cli_repair" 0x70676D636C690002 $ \options -> do
let environment = cliEnvironment settings failingPlan options
failedUp <- runMigrationCommand environment upCommand
exitClass failedUp @?= ExitExecutionFailed
repaired <-
runMigrationCommand
environment
( Repair
( RepairOptions
failingMigrationId
MarkApplied
"operator inspected the database result"
Confirmed
noConnectionOverride
defaultExecution
jsonOutput
)
)
exitClass repaired @?= ExitSuccess
case payload repaired of
Right (RepairPayload (RepairReport identifier MarkApplied Failed Applied)) ->
identifier @?= failingMigrationId
other -> assertFailure ("unexpected repair outcome: " <> show other)
verified <- runMigrationCommand environment verifyCommand
exitClass verified @?= ExitSuccess
assertSingleOutcome :: MigrationOutcome -> CliOutcome -> Assertion
assertSingleOutcome expected outcome =
case payload outcome of
Right (UpPayload (MigrationReport _ _ (MigrationResult _ actual _ :| []))) ->
actual @?= expected
other -> assertFailure ("unexpected up outcome: " <> show other)
statusCommand :: MigrationCommand
statusCommand = Status (StatusOptions noInspection noConnectionOverride jsonOutput)
verifyCommand :: MigrationCommand
verifyCommand = Verify (VerifyOptions noInspection noConnectionOverride jsonOutput)
upCommand :: MigrationCommand
upCommand = Up (UpOptions noConnectionOverride defaultExecution jsonOutput)
noInspection :: InspectionOptions
noInspection = InspectionOptions Nothing Nothing
noConnectionOverride :: ConnectionOptions
noConnectionOverride = ConnectionOptions Nothing
defaultExecution :: ExecutionOptions
defaultExecution = ExecutionOptions WaitIndefinitely Nothing
jsonOutput :: OutputOptions
jsonOutput = OutputOptions JsonOutput
commandPlan :: MigrationPlan
commandPlan =
expectRight
( migrationPlan
( expectRight
( migrationComponent
"cli-commands"
Set.empty
( expectRight
( sqlMigration
"0001"
"CREATE TABLE pgmigrate_cli_commands.cli_command_probe (id bigint PRIMARY KEY)"
)
:| []
)
)
:| []
)
)
failingPlan :: MigrationPlan
failingPlan =
expectRight
( migrationPlan
( expectRight
( migrationComponent
"cli-repair"
Set.empty
(failingMigration :| [])
)
:| []
)
)
failingMigration :: Migration
failingMigration =
expectRight
( sessionMigration
"0001"
(migrationFingerprint "cli-repair-v1")
(Session.script "SELECT * FROM pgmigrate_cli_deliberately_missing_table")
)
failingMigrationId :: MigrationId
failingMigrationId = expectRight (migrationId "cli-repair" "0001")
withCleanLedger ::
Settings.Settings ->
Text.Text ->
Int64 ->
(RunOptions -> IO value) ->
IO value
withCleanLedger settings schemaName lockKey action = do
let config = expectRight (ledgerConfig schemaName lockKey)
Exception.bracket_
(dropSchema settings schemaName)
(dropSchema settings schemaName)
(action (withLedger config defaultRunOptions))
dropSchema :: Settings.Settings -> Text.Text -> IO ()
dropSchema settings schemaName = do
acquired <- Connection.acquire settings
case acquired of
Left connectionError -> assertFailure ("could not acquire integration connection: " <> show connectionError)
Right connection ->
Exception.finally
( do
result <- Connection.use connection (Session.script ("DROP SCHEMA IF EXISTS " <> schemaName <> " CASCADE"))
case result of
Left sessionError -> assertFailure ("could not clean integration schema: " <> show sessionError)
Right () -> pure ()
)
(Connection.release connection)
expectRight :: (Show error) => Either error value -> value
expectRight = either (error . show) id