packages feed

pgmq-hasql-0.1.0.0: test/MessageSpec.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Tests for message operations
module MessageSpec (tests) where

import Data.Aeson (object, (.=))
import Data.Vector qualified as V
import EphemeralDb (TestFixture (..), withTestFixture)
import Hasql.Pool qualified as Pool
import Pgmq.Hasql.Sessions qualified as Sessions
import Pgmq.Hasql.Statements.Types
  ( BatchSendMessage (..),
    BatchSendMessageWithHeaders (..),
    MessageQuery (..),
    ReadMessage (..),
    SendMessage (..),
    SendMessageWithHeaders (..),
    VisibilityTimeoutQuery (..),
  )
import Pgmq.Types (MessageBody (..), MessageHeaders (..), MessageId (..))
import Pgmq.Types qualified as PgmqTypes
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, testCase)
import TestUtils (assertSession, cleanupQueue)

-- | All message operation tests
tests :: Pool.Pool -> TestTree
tests p =
  testGroup
    "Message Operations"
    [ testSendMessageNullDelay p,
      testSendMessageZeroDelay p,
      testBatchSendMessageNullDelay p,
      testReadMessage p,
      testDeleteMessage p,
      testArchiveMessage p,
      testChangeVisibilityTimeout p,
      testSendMessageWithHeaders p,
      testBatchSendMessageWithHeaders p
    ]

-- | Test sendMessage with delay=Nothing (validates COALESCE fix)
-- This is a critical test - it validates that our fix for NULL delay
-- correctly uses the pgmq function overload resolution
testSendMessageNullDelay :: Pool.Pool -> TestTree
testSendMessageNullDelay p = testCase "sendMessage with delay=Nothing works" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    let msg =
          SendMessage
            { queueName = queueName,
              messageBody = MessageBody (object ["test" .= ("hello" :: String)]),
              delay = Nothing
            }
    msgId <- assertSession pool (Sessions.sendMessage msg)
    assertBool "Message ID should be positive" (unMessageId msgId > 0)
    -- Verify message can be read immediately (no delay)
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Should read 1 message" 1 (V.length messages)
    cleanupQueue pool queueName

-- | Test sendMessage with delay=Just 0
testSendMessageZeroDelay :: Pool.Pool -> TestTree
testSendMessageZeroDelay p = testCase "sendMessage with delay=Just 0 works" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    let msg =
          SendMessage
            { queueName = queueName,
              messageBody = MessageBody (object ["test" .= ("hello" :: String)]),
              delay = Just 0
            }
    msgId <- assertSession pool (Sessions.sendMessage msg)
    assertBool "Message ID should be positive" (unMessageId msgId > 0)
    -- Verify message can be read immediately
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Should read 1 message" 1 (V.length messages)
    cleanupQueue pool queueName

-- | Test batchSendMessage with delay=Nothing (validates COALESCE fix)
testBatchSendMessageNullDelay :: Pool.Pool -> TestTree
testBatchSendMessageNullDelay p = testCase "batchSendMessage with delay=Nothing works" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    let msgs =
          BatchSendMessage
            { queueName = queueName,
              messageBodies =
                [ MessageBody (object ["batch" .= (1 :: Int)]),
                  MessageBody (object ["batch" .= (2 :: Int)]),
                  MessageBody (object ["batch" .= (3 :: Int)])
                ],
              delay = Nothing
            }
    msgIds <- assertSession pool (Sessions.batchSendMessage msgs)
    assertEqual "Should return 3 message IDs" 3 (length msgIds)
    -- Verify messages can be read
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 10,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Should read 3 messages" 3 (V.length messages)
    cleanupQueue pool queueName

-- | Test readMessage
testReadMessage :: Pool.Pool -> TestTree
testReadMessage p = testCase "readMessage retrieves and hides message" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send a message
    let msg =
          SendMessage
            { queueName = queueName,
              messageBody = MessageBody (object ["key" .= ("value" :: String)]),
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.sendMessage msg)
    -- Read the message
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30, -- 30 second visibility timeout
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Should read 1 message" 1 (V.length messages)
    -- Message should not be readable again (still hidden)
    messages2 <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Should not read hidden message" 0 (V.length messages2)
    cleanupQueue pool queueName

-- | Test deleteMessage
testDeleteMessage :: Pool.Pool -> TestTree
testDeleteMessage p = testCase "deleteMessage removes message from queue" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send and read a message
    let sendMsg =
          SendMessage
            { queueName = queueName,
              messageBody = MessageBody (object ["delete" .= ("me" :: String)]),
              delay = Nothing
            }
    msgId <- assertSession pool (Sessions.sendMessage sendMsg)
    -- Delete the message
    let deleteQuery =
          MessageQuery
            { queueName = queueName,
              messageId = msgId
            }
    deleted <- assertSession pool (Sessions.deleteMessage deleteQuery)
    assertBool "Delete should succeed" deleted
    -- Verify message can't be read anymore
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Queue should be empty after delete" 0 (V.length messages)
    cleanupQueue pool queueName

-- | Test archiveMessage
testArchiveMessage :: Pool.Pool -> TestTree
testArchiveMessage p = testCase "archiveMessage moves message to archive" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send a message
    let sendMsg =
          SendMessage
            { queueName = queueName,
              messageBody = MessageBody (object ["archive" .= ("me" :: String)]),
              delay = Nothing
            }
    msgId <- assertSession pool (Sessions.sendMessage sendMsg)
    -- Archive the message
    let archiveQuery =
          MessageQuery
            { queueName = queueName,
              messageId = msgId
            }
    archived <- assertSession pool (Sessions.archiveMessage archiveQuery)
    assertBool "Archive should succeed" archived
    -- Verify message can't be read from queue anymore
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Queue should be empty after archive" 0 (V.length messages)
    cleanupQueue pool queueName

-- | Test changeVisibilityTimeout
testChangeVisibilityTimeout :: Pool.Pool -> TestTree
testChangeVisibilityTimeout p = testCase "changeVisibilityTimeout extends/resets VT" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send a message
    let sendMsg =
          SendMessage
            { queueName = queueName,
              messageBody = MessageBody (object ["vt" .= ("test" :: String)]),
              delay = Nothing
            }
    msgId <- assertSession pool (Sessions.sendMessage sendMsg)
    -- Read the message to set visibility timeout
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              conditional = Nothing
            }
    _ <- assertSession pool (Sessions.readMessage readQuery)
    -- Change visibility timeout
    let vtQuery =
          VisibilityTimeoutQuery
            { queueName = queueName,
              messageId = msgId,
              visibilityTimeoutOffset = 60
            }
    msg <- assertSession pool (Sessions.changeVisibilityTimeout vtQuery)
    assertEqual "Should return the message" msgId (PgmqTypes.messageId msg)
    cleanupQueue pool queueName

-- | Test sendMessageWithHeaders
testSendMessageWithHeaders :: Pool.Pool -> TestTree
testSendMessageWithHeaders p = testCase "sendMessageWithHeaders includes headers" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    let msg =
          SendMessageWithHeaders
            { queueName = queueName,
              messageBody = MessageBody (object ["data" .= ("test" :: String)]),
              messageHeaders = MessageHeaders (object ["trace_id" .= ("abc123" :: String)]),
              delay = Nothing
            }
    msgId <- assertSession pool (Sessions.sendMessageWithHeaders msg)
    assertBool "Message ID should be positive" (unMessageId msgId > 0)
    -- Read and verify headers
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Should read 1 message" 1 (V.length messages)
    let readMsg = V.head messages
    case PgmqTypes.headers readMsg of
      Just _ -> assertBool "Headers should contain trace_id" True
      Nothing -> assertBool "Headers should be present" False
    cleanupQueue pool queueName

-- | Test batchSendMessageWithHeaders
testBatchSendMessageWithHeaders :: Pool.Pool -> TestTree
testBatchSendMessageWithHeaders p = testCase "batchSendMessageWithHeaders sends batch with headers" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    let msgs =
          BatchSendMessageWithHeaders
            { queueName = queueName,
              messageBodies =
                [ MessageBody (object ["batch" .= (1 :: Int)]),
                  MessageBody (object ["batch" .= (2 :: Int)])
                ],
              messageHeaders =
                [ MessageHeaders (object ["idx" .= (1 :: Int)]),
                  MessageHeaders (object ["idx" .= (2 :: Int)])
                ],
              delay = Nothing
            }
    msgIds <- assertSession pool (Sessions.batchSendMessageWithHeaders msgs)
    assertEqual "Should return 2 message IDs" 2 (length msgIds)
    cleanupQueue pool queueName