packages feed

valiant-0.1.0.0: integration/ConnectionFeaturesSpec.hs

module ConnectionFeaturesSpec (spec) where

import Data.ByteString qualified as BS
import Data.Int (Int32)
import Data.IORef
import Data.Text (Text)
import Data.Maybe (isJust)
import Data.Vector qualified as V
import Valiant
import PgWire.Connection
import PgWire.Connection.Config (parseConnString)
import PgWire.Protocol.Backend (TxStatus (..))
import TestSupport
import Test.Hspec

spec :: Spec
spec = do
  describe "connectionStatus" $ do
    it "returns True for a live connection" $ do
      withTestConnection $ \conn -> do
        status <- connectionStatus conn
        status `shouldBe` True

  describe "transactionStatus" $ do
    it "returns TxIdle outside a transaction" $ do
      withTestConnection $ \conn -> do
        status <- transactionStatus conn
        status `shouldBe` TxIdle

  describe "parameterStatus" $ do
    it "returns server_version" $ do
      withTestConnection $ \conn -> do
        version <- parameterStatus conn "server_version"
        version `shouldSatisfy` isJust

    it "returns server_encoding" $ do
      withTestConnection $ \conn -> do
        enc <- parameterStatus conn "server_encoding"
        enc `shouldSatisfy` isJust

    it "returns Nothing for unknown params" $ do
      withTestConnection $ \conn -> do
        val <- parameterStatus conn "nonexistent_param"
        val `shouldBe` Nothing

  describe "serverVersion" $ do
    it "returns a positive integer" $ do
      withTestConnection $ \conn -> do
        version <- serverVersion conn
        case version of
          Just v -> v `shouldSatisfy` (> 100000)  -- at least PG 10
          Nothing -> expectationFailure "Expected version"

  describe "backendPid" $ do
    it "returns a positive PID" $ do
      withTestConnection $ \conn -> do
        let pid = backendPid conn
        pid `shouldSatisfy` (> 0)

  describe "reset" $ do
    it "reconnects and returns a working connection" $ do
      url <- requireDatabaseUrl
      conn <- connectString url
      conn2 <- reset conn
      -- conn is now closed; conn2 is the new connection
      (rows, _) <- simpleQuery conn2 "SELECT 1"
      length rows `shouldBe` 1
      close conn2

  describe "checkNotification" $ do
    it "returns Nothing when no notifications pending" $ do
      withTestConnection $ \conn -> do
        result <- checkNotification conn
        result `shouldBe` Nothing

  describe "describePrepared" $ do
    it "describes a prepared statement" $ do
      withTestConnection $ \conn -> withSchema conn $ do
        -- Prepare a statement manually
        _ <- simpleQuery conn "PREPARE test_stmt (int4) AS SELECT id, name FROM users WHERE id = $1"
        (params, cols) <- describePrepared conn "test_stmt"
        -- Should have 1 param (int4 = OID 23)
        length (pdOids params) `shouldBe` 1
        -- Should have 2 columns
        length (cdFields cols) `shouldBe` 2
        _ <- simpleQuery conn "DEALLOCATE test_stmt"
        pure ()

  describe "escapeLiteral" $ do
    it "produces valid SQL when used in a query" $ do
      withTestConnection $ \conn -> do
        let escaped = escapeLiteral conn "O'Brien"
        (rows, _) <- simpleQuery conn ("SELECT " <> escaped)
        case rows of
          [[Just val]] -> val `shouldBe` "O'Brien"
          _ -> expectationFailure $ "Unexpected: " <> show rows

  describe "escapeIdentifier" $ do
    it "produces valid SQL for identifiers" $ do
      withTestConnection $ \conn -> do
        let escaped = escapeIdentifier conn "my col"
        (rows, _) <- simpleQuery conn ("SELECT 1 AS " <> escaped)
        length rows `shouldBe` 1

  describe "ping" $ do
    it "returns True for a reachable server" $ do
      url <- requireDatabaseUrl
      case parseConnString url of
        Right cfg -> do
          result <- ping cfg
          result `shouldBe` True
        Left _ -> expectationFailure "Failed to parse URL"

  describe "encryptPassword" $ do
    it "produces md5-prefixed hash" $ do
      let hash = encryptPassword "testuser" "testpass"
      BS.isPrefixOf "md5" hash `shouldBe` True
      BS.length hash `shouldBe` 35  -- "md5" + 32 hex chars

    it "produces different hashes for different passwords" $ do
      let h1 = encryptPassword "user" "pass1"
          h2 = encryptPassword "user" "pass2"
      h1 `shouldNotBe` h2

  describe "setTraceHandler" $ do
    it "captures send and recv traffic" $ do
      withTestConnection $ \conn -> do
        sendRef <- newIORef (0 :: Int)
        recvRef <- newIORef (0 :: Int)
        setTraceHandler conn $ \isSend _bytes ->
          if isSend
            then modifyIORef' sendRef (+ 1)
            else modifyIORef' recvRef (+ 1)
        _ <- simpleQuery conn "SELECT 1"
        sends <- readIORef sendRef
        recvs <- readIORef recvRef
        sends `shouldSatisfy` (> 0)
        recvs `shouldSatisfy` (> 0)

  describe "executeWithFold" $ do
    it "folds rows in constant memory" $ do
      withTestConnection $ \conn -> withSchema conn $ do
        insertTestUsers conn
        let countStmt :: Statement () (Int32, Text, Maybe Text)
            countStmt = mkStatement
              "SELECT id, name, email FROM users"
              [] ["id", "name", "email"] "<test>"
        count <- executeWithFold conn countStmt () $
          RowFold (0 :: Int) (\acc _ -> acc + 1)
        count `shouldBe` 5

  describe "copyInBinary" $ do
    it "bulk inserts via binary COPY" $ do
      withTestConnection $ \conn -> withSchema conn $ do
        result <- copyInBinary conn
          "COPY users (name, email) FROM STDIN WITH (FORMAT binary)"
          2
          $ \sendRow -> do
            sendRow (V.fromList [Just (pgEncode ("BinAlice" :: BS.ByteString)), Just (pgEncode ("alice@bin.com" :: BS.ByteString))])
            sendRow (V.fromList [Just (pgEncode ("BinBob" :: BS.ByteString)), Nothing])
        copyRows result `shouldBe` 2
        (rows, _) <- simpleQuery conn "SELECT count(*) FROM users"
        case rows of
          [[Just n]] -> n `shouldBe` "2"
          _ -> expectationFailure $ "Expected 2, got: " <> show rows