pgmq-hasql-0.7.0.0: test/InspectionForeignNameSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | IR-1 acceptance 5: every inspection read works for queues whose names
-- 'parseQueueName' rejects, because an inspection surface sees whatever exists
-- in the database, not only what this library created.
--
-- The names are created and fed through raw SQL (the Haskell API rightly
-- refuses them) and read back through the new sessions. Runs on a dedicated
-- PostgreSQL instance: a mixed-case row in pgmq.meta poisons the typed
-- listQueues decoding for every test sharing a database.
module InspectionForeignNameSpec (tests) where
import Control.Monad (void)
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V
import Data.Word (Word32)
import Database.PostgreSQL.Migrate (defaultRunOptions, migrationPlan, runMigrationPlan)
import EphemeralDb (ephemeralConfig)
import EphemeralPg qualified as Pg
import Hasql.Decoders qualified as D
import Hasql.Pool qualified as Pool
import Hasql.Pool.Config qualified as PoolConfig
import Hasql.Session (Session, statement)
import Hasql.Statement (unpreparable)
import Pgmq.Hasql.Sessions qualified as Sessions
import Pgmq.Hasql.Statements.Types (LookupMessage (LookupMessage), PeekMessages (PeekMessages), QueueMetrics (..))
import Pgmq.Migration qualified as Migration
import Pgmq.Types (ArchivedMessage (..), Message (..), MessageId (..), parseQueueName)
import System.Random (randomRIO)
import Test.Tasty (TestTree, testGroup, withResource)
import Test.Tasty.HUnit (assertEqual, assertFailure, testCase, (@?=))
tests :: TestTree
tests =
withResource acquireDb releaseDb $ \getDb ->
testGroup
"Inspection of foreign queue names"
[ testForeignName getDb "mixed-case" ("MyQueue_" <>),
testForeignName getDb "hyphenated" ("odd-name_" <>)
]
testForeignName :: IO (Pg.Database, Pool.Pool) -> String -> (Text -> Text) -> TestTree
testForeignName getDb label mkName = testCase (label <> " name: peek, lookup, archive, and metrics work") $ do
(_, pool) <- getDb
suffix <- T.pack . show <$> randomRIO (10000 :: Word32, 99999)
let name = mkName suffix
-- The whole point: this library's validator refuses the name.
case parseQueueName name of
Left _ -> pure ()
Right _ -> assertFailure ("expected parseQueueName to reject " <> T.unpack name)
assertSession pool (rawUnit ("select pgmq.create('" <> name <> "')"))
ids <- mapM (\i -> MessageId <$> assertSession pool (rawId ("select pgmq.send('" <> name <> "', '{\"i\":" <> T.pack (show i) <> "}'::jsonb)"))) [1 .. 3 :: Int]
firstId <- case ids of
x : _ -> pure x
[] -> assertFailure "expected three ids"
page <- assertSession pool (Sessions.peekMessages (PeekMessages name Nothing 10))
map messageId (V.toList page) @?= ids
one <- assertSession pool (Sessions.lookupMessage (LookupMessage name firstId))
fmap messageId one @?= Just firstId
metrics <- assertSession pool (Sessions.queueMetricsUnvalidated name)
queueLength metrics @?= 3
assertEqual "metrics report the name as given" name (queueName metrics)
void $ assertSession pool (rawBool ("select pgmq.archive('" <> name <> "', " <> T.pack (show (unMessageId firstId)) <> "::bigint)"))
archived <- assertSession pool (Sessions.peekArchivedMessages (PeekMessages name Nothing 10))
map (messageId . archivedMessage) (V.toList archived) @?= [firstId]
found <- assertSession pool (Sessions.lookupArchivedMessage (LookupMessage name firstId))
fmap (messageId . archivedMessage) found @?= Just firstId
remaining <- assertSession pool (Sessions.peekMessages (PeekMessages name Nothing 10))
map messageId (V.toList remaining) @?= drop 1 ids
void $ assertSession pool (rawBool ("select pgmq.drop_queue('" <> name <> "')"))
-- Dedicated database plumbing (as in MixedCaseRemediationSpec) -----------------
acquireDb :: IO (Pg.Database, Pool.Pool)
acquireDb = do
config <- ephemeralConfig
started <- Pg.startCached config Pg.defaultCacheConfig
db <- either (\err -> error ("could not start a dedicated PostgreSQL: " <> show err)) pure started
component <- either (error . ("Invalid PGMQ migration component: " <>) . show) pure Migration.pgmqMigrations
plan <- either (error . ("Invalid PGMQ migration plan: " <>) . show) pure (migrationPlan (component :| []))
installResult <- runMigrationPlan defaultRunOptions (Pg.connectionSettings db) plan
case installResult of
Left migrationErr -> error $ "Migration failed: " <> show migrationErr
Right _ -> pure ()
pool <-
Pool.acquire $
PoolConfig.settings
[ PoolConfig.size 2,
PoolConfig.staticConnectionSettings (Pg.connectionSettings db)
]
pure (db, pool)
releaseDb :: (Pg.Database, Pool.Pool) -> IO ()
releaseDb (db, pool) = do
Pool.release pool
Pg.stop db
-- Raw statement helpers. Names are spliced into SQL text because these tests
-- must construct names the Haskell API refuses; every spliced value is built
-- above from [A-Za-z0-9_-], so splicing is safe here.
rawUnit :: Text -> Session ()
rawUnit sqlText = statement () (unpreparable sqlText mempty D.noResult)
rawBool :: Text -> Session Bool
rawBool sqlText = statement () (unpreparable sqlText mempty (D.singleRow (D.column (D.nonNullable D.bool))))
rawId :: Text -> Session Int64
rawId sqlText = statement () (unpreparable sqlText mempty (D.singleRow (D.column (D.nonNullable D.int8))))
assertSession :: Pool.Pool -> Session a -> IO a
assertSession pool session = do
result <- Pool.use pool session
case result of
Left err -> assertFailure $ "Session failed: " <> show err
Right a -> pure a