packages feed

pgmq-hasql-0.1.0.0: test/RoundTripSpec.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Property-based round-trip tests
-- Verifies that data survives encode -> database -> decode cycles
module RoundTripSpec (tests) where

import Data.Vector qualified as V
import EphemeralDb (TestFixture (..), withTestFixture)
import Generators (genMessageBody, genMessageHeaders)
import Hasql.Pool qualified as Pool
import Hedgehog (annotateShow, forAll, (===))
import Hedgehog qualified as H
import Pgmq.Hasql.Sessions qualified as Sessions
import Pgmq.Hasql.Statements.Types
  ( ReadMessage (..),
    SendMessage (..),
    SendMessageWithHeaders (..),
  )
import Pgmq.Types (Message (..), MessageBody (..), MessageHeaders (..), unMessageId)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Hedgehog (testProperty)
import TestUtils (assertSession, cleanupQueue)

-- | All round-trip property tests
tests :: Pool.Pool -> TestTree
tests p =
  testGroup
    "Round-Trip Properties"
    [ testProperty "message body survives round-trip" (propMessageBodyRoundTrip p),
      testProperty "message with headers survives round-trip" (propMessageWithHeadersRoundTrip p),
      testProperty "timestamp ordering is preserved" (propTimestampOrdering p)
    ]

-- | Property: message body survives send/read round-trip
propMessageBodyRoundTrip :: Pool.Pool -> H.Property
propMessageBodyRoundTrip p = H.property $ do
  body <- forAll genMessageBody
  result <- H.evalIO $ withTestFixture p $ \TestFixture {pool, queueName} -> do
    -- Create queue
    assertSession pool (Sessions.createQueue queueName)

    -- Send message with random body
    let sendMsg =
          SendMessage
            { queueName = queueName,
              messageBody = body,
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.sendMessage sendMsg)

    -- Read message back
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    cleanupQueue pool queueName
    pure messages

  -- Verify body is preserved
  annotateShow result
  H.assert (V.length result == 1)
  let msg = V.head result
  unMessageBody (body :: MessageBody) === unMessageBody (Pgmq.Types.body msg)

-- | Property: message with headers survives round-trip
propMessageWithHeadersRoundTrip :: Pool.Pool -> H.Property
propMessageWithHeadersRoundTrip p = H.property $ do
  body <- forAll genMessageBody
  hdrs <- forAll genMessageHeaders
  result <- H.evalIO $ withTestFixture p $ \TestFixture {pool, queueName} -> do
    -- Create queue
    assertSession pool (Sessions.createQueue queueName)

    -- Send message with headers
    let sendMsg =
          SendMessageWithHeaders
            { queueName = queueName,
              messageBody = body,
              messageHeaders = hdrs,
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.sendMessageWithHeaders sendMsg)

    -- Read message back
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    cleanupQueue pool queueName
    pure messages

  -- Verify body and headers are preserved
  annotateShow result
  H.assert (V.length result == 1)
  let msg = V.head result
  unMessageBody (body :: MessageBody) === unMessageBody (Pgmq.Types.body msg)
  Just (unMessageHeaders hdrs) === Pgmq.Types.headers msg

-- | Property: timestamps maintain ordering (enqueued_at <= vt when read)
propTimestampOrdering :: Pool.Pool -> H.Property
propTimestampOrdering p = H.property $ do
  body <- forAll genMessageBody
  result <- H.evalIO $ withTestFixture p $ \TestFixture {pool, queueName} -> do
    -- Create queue
    assertSession pool (Sessions.createQueue queueName)

    -- Send message
    let sendMsg =
          SendMessage
            { queueName = queueName,
              messageBody = body,
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.sendMessage sendMsg)

    -- Read message with visibility timeout
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30, -- 30 second visibility timeout
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    cleanupQueue pool queueName
    pure messages

  -- Verify timestamp ordering
  H.assert (V.length result == 1)
  let msg = V.head result
  -- enqueued_at should be before visibility_time (vt is set to future after read)
  H.assert (enqueuedAt msg < visibilityTime msg)
  -- Message ID should be positive
  H.assert (unMessageId (messageId msg) > 0)
  -- Read count should be 1 after first read
  readCount msg === 1