packages feed

pqi-conformance-0.0.1.0: src/library/Pqi/Conformance/Operation/Connectdb.hs

-- | Coverage for 'Pqi.connectdb': a blocking connection from a conninfo
-- string, including the rejected-conninfo error paths and SCRAM-SHA-256
-- authentication.
module Pqi.Conformance.Operation.Connectdb
  ( spec,
  )
where

import Control.Exception (bracket)
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Char8 as ByteString.Char8
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import Pqi (IsConnection (..))
import Pqi.Conformance.Harness
import Pqi.Conformance.Observation
import Pqi.Conformance.Prelude
import Pqi.Conformance.Reference (Reference)
import Test.Hspec
import qualified TestcontainersPostgresql as TcPg

spec :: forall c. (IsConnection c) => Proxy c -> SpecWith ByteString
spec proxy = do
  describe "connectdb" do
    it "opens a usable connection" \conninfo ->
      differential proxy conninfo observeConnection

    it "accepts a URI-format conninfo" \conninfo ->
      differentialConnect proxy conninfo \(_ :: Proxy d) conninfo' -> do
        connection <- connectdb (kvToUri conninfo') :: IO d
        s <- status connection
        finish connection
        pure s

    it "rejects an unknown database" \conninfo ->
      differentialConnect proxy conninfo \(_ :: Proxy d) conninfo' -> do
        connection <- connectdb (conninfo' <> " dbname=pqi_no_such_db") :: IO d
        observation <- status connection
        nullness <- pure (isNullConnection connection)
        finish connection
        pure (observation, nullness)

    it "rejects an unknown user" \conninfo ->
      differentialConnect proxy conninfo \(_ :: Proxy d) conninfo' -> do
        connection <- connectdb (conninfo' <> " user=pqi_no_such_user") :: IO d
        observation <- status connection
        finish connection
        pure observation

  describe "SCRAM-SHA-256 authentication" do
    it "the candidate authenticates and queries like the FFI reference" \_ ->
      let scramConfig =
            TcPg.Config
              { TcPg.tagName = "postgres:17",
                TcPg.forwardLogs = False,
                TcPg.auth = TcPg.CredentialsAuth "scram" "secret"
              }

          scramScenario :: forall c. (IsConnection c) => c -> IO (Maybe ResultObservation)
          scramScenario connection = exec connection "select 1 as scram_works" >>= traverse observeResult
       in TcPg.run scramConfig \(host, port) -> do
            let conninfo =
                  ByteString.Char8.pack
                    ( "host="
                        <> Text.unpack host
                        <> " port="
                        <> show port
                        <> " user=scram password=secret dbname=scram"
                    )
            native <- bracket (connectdb conninfo) finish (scramScenario @c)
            reference <- bracket (connectdb conninfo) finish (scramScenario @Reference)
            native `shouldBe` reference

-- | Convert a @key=value@ conninfo to a @postgresql://@ URI, for testing
-- that adapters accept URI-format connection strings.
kvToUri :: ByteString -> ByteString
kvToUri raw =
  "postgresql://"
    <> user
    <> (if ByteString.null password then "" else ":" <> password)
    <> (if ByteString.null user && ByteString.null password then "" else "@")
    <> host
    <> (if ByteString.null port then "" else ":" <> port)
    <> (if ByteString.null dbname then "" else "/" <> dbname)
  where
    pairs = Map.fromList $ mapMaybe toPair (ByteString.Char8.words raw)
    get k = Map.findWithDefault "" k pairs
    toPair token = case ByteString.Char8.break (== '=') token of
      (k, v) | not (ByteString.null v) -> Just (k, ByteString.drop 1 v)
      _ -> Nothing
    host = get "host"
    port = get "port"
    user = get "user"
    password = get "password"
    dbname = get "dbname"