packages feed

pg-migrate-cli-1.1.0.0: test/unit/Test/Json.hs

module Test.Json (tests) where

import Data.Aeson qualified as Aeson
import Data.ByteString qualified as ByteString
import Data.Foldable (traverse_)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Time (UTCTime)
import Database.PostgreSQL.Migrate
import Database.PostgreSQL.Migrate.CLI
import Database.PostgreSQL.Migrate.Embed (ManifestError (EmptyManifest))
import Database.PostgreSQL.Migrate.Internal
  ( ComponentDescription (ComponentDescription),
    MigrationDescription (MigrationDescription),
    MigrationKind (..),
    TransactionMode (..),
  )
import Paths_pg_migrate_cli qualified as Paths
import Test.Tasty
import Test.Tasty.HUnit

tests :: TestTree
tests =
  testGroup
    "JSON schema v1"
    [ goldenCase "plan" planOutcome,
      goldenCase "status" statusOutcome,
      goldenCase "verify" verifyOutcome,
      goldenCase "up" upOutcome,
      goldenCase "up-cleanup" upCleanupOutcome,
      goldenCase "repair" repairOutcome,
      goldenCase "error" errorOutcome,
      testCase "import matches its golden contract" testImportGolden,
      testCase "rendering is byte-for-byte stable" testStableRendering
    ]

goldenCase :: FilePath -> CliOutcome -> TestTree
goldenCase name outcome =
  testCase (name <> " matches its golden contract") $ do
    goldenPath <- Paths.getDataFileName ("test/golden/json/" <> name <> ".json")
    goldenBytes <- ByteString.readFile goldenPath
    expected <-
      case Aeson.eitherDecodeStrict' goldenBytes of
        Left decodeError -> assertFailure ("invalid golden JSON: " <> decodeError)
        Right value -> pure value
    renderMigrationCommandJson outcome @?= expected

testImportGolden :: Assertion
testImportGolden = do
  goldenPath <- Paths.getDataFileName "test/golden/json/import.json"
  goldenBytes <- ByteString.readFile goldenPath
  expected <-
    case Aeson.eitherDecodeStrict' goldenBytes of
      Left decodeError -> assertFailure ("invalid golden JSON: " <> decodeError)
      Right value -> pure value
  renderHistoryImportJson "codd" importReport @?= expected

testStableRendering :: Assertion
testStableRendering =
  traverse_
    ( \outcome ->
        Aeson.encode (renderMigrationCommandJson outcome)
          @?= Aeson.encode (renderMigrationCommandJson outcome)
    )
    fixtureOutcomes

fixtureOutcomes :: [CliOutcome]
fixtureOutcomes =
  [ planOutcome,
    statusOutcome,
    verifyOutcome,
    upOutcome,
    upCleanupOutcome,
    repairOutcome,
    errorOutcome
  ]

planOutcome :: CliOutcome
planOutcome =
  successful
    "plan"
    ( PlanPayload
        [ ComponentDescription
            accounts
            1
            Set.empty
            (MigrationDescription migrationIdentifier 1 emptyChecksum SqlKind Transactional :| [])
        ]
    )

statusOutcome :: CliOutcome
statusOutcome =
  successful "status" (StatusPayload (StatusReport [] [migrationIdentifier] [] []))

verifyOutcome :: CliOutcome
verifyOutcome =
  CliOutcome
    { command = "verify",
      exitClass = ExitVerificationFailed,
      payload =
        Right
          ( VerifyPayload
              (VerificationReport [PendingMigration migrationIdentifier] [] [migrationIdentifier] [])
          )
    }

upOutcome :: CliOutcome
upOutcome =
  successful
    "up"
    ( UpPayload
        ( MigrationReport
            fixtureStartedAt
            fixtureFinishedAt
            (MigrationResult migrationIdentifier AppliedNow (Just 0.125) :| [])
            []
        )
    )

upCleanupOutcome :: CliOutcome
upCleanupOutcome =
  successful
    "up"
    ( UpPayload
        ( MigrationReport
            fixtureStartedAt
            fixtureFinishedAt
            (MigrationResult migrationIdentifier AppliedNow (Just 0.125) :| [])
            [AdvisoryUnlockReturnedFalse]
        )
    )

repairOutcome :: CliOutcome
repairOutcome =
  successful
    "repair"
    (RepairPayload (RepairReport migrationIdentifier MarkApplied Failed Applied []))

errorOutcome :: CliOutcome
errorOutcome =
  CliOutcome
    { command = "check",
      exitClass = ExitUsageFailed,
      payload = Left (CliManifestError EmptyManifest)
    }

successful :: Text -> CliPayload -> CliOutcome
successful command payload = CliOutcome {command, exitClass = ExitSucceeded, payload = Right payload}

accounts :: ComponentName
accounts = expectRight (componentName "accounts")

migrationIdentifier :: MigrationId
migrationIdentifier = expectRight (migrationId "accounts" "0001")

emptyChecksum :: MigrationChecksum
emptyChecksum = migrationFingerprint ByteString.empty

fixtureStartedAt :: UTCTime
fixtureStartedAt = read "2026-07-10 12:00:00 UTC"

fixtureFinishedAt :: UTCTime
fixtureFinishedAt = read "2026-07-10 12:00:01 UTC"

importReport :: HistoryImportReport
importReport =
  HistoryImportReport
    { importResults =
        HistoryImportResult migrationIdentifier Imported
          :| [HistoryImportResult (expectRight (migrationId "accounts" "0002")) AlreadyImported],
      cleanupIssues = []
    }

expectRight :: (Show error) => Either error value -> value
expectRight = either (error . show) id