{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} module Main where import Control.Exception (bracket, throw, throwIO, try) import Control.Monad.Logger.CallStack (logWarn, runStderrLoggingT) import Data.Function ((&)) import Data.Functor ((<&>), void) import Data.Text (Text) import Database.PostgreSQL.Simple ( Only(..), SqlError(sqlErrorMsg), Connection, close, connectPostgreSQL, execute_, query ) import Database.PostgreSQL.Simple.LTree (mkLabel) import System.Environment (lookupEnv) import Test.Hspec (describe, expectationFailure, hspec, it) import Test.QuickCheck (Gen, arbitrary, property, suchThat) import Test.QuickCheck.Monadic (forAllM, monadicIO, run) import qualified Data.Bifunctor as Bifunctor import qualified Data.ByteString.Char8 as C8 import qualified Data.Text as Text import qualified Database.Postgres.Temp as TmpPostgres main :: IO () main = withTmpPostgres $ \conn -> hspec $ do describe "mkLabel" $ do it "should only produce valid labels" $ property $ monadicIO $ forAllM genPGText $ \t -> run $ mkLabel t & either (const $ verifyTextCannotBeALabel conn t) (const $ verifyTextCanBeALabel conn t) where withTmpPostgres f = do let go conn = do _ <- execute_ conn "create extension if not exists ltree" f conn -- Checks for the PGDATABASE env var. If it's not set, use -- tmp-postgres. Otherwise, connect to the database -- specified via env vars. lookupEnv "PGDATABASE" >>= \case Nothing -> either throwIO pure =<< TmpPostgres.with (\db -> bracket (connectPostgreSQL $ TmpPostgres.toConnectionString db) close go ) Just _ -> -- Uses the env vars as specified here - -- https://www.postgresql.org/docs/13/libpq-envars.html bracket (connectPostgreSQL "") close go verifyTextCanBeALabel :: Connection -> Text -> IO () verifyTextCanBeALabel conn t = tryConvertTextToLabelInPG conn t >>= \case Left msg -> expectationFailure msg Right () -> pure () -- Making this throw an expectationFailure is a bit too strong of a test. -- It's probably ok if 'mkLabel' is too strict; it's mostly important that -- it's not too lenient. We'll just log a warning for cases found in which -- 'mkLabel' is too strict. verifyTextCannotBeALabel :: Connection -> Text -> IO () verifyTextCannotBeALabel conn t = tryConvertTextToLabelInPG conn t >>= \case Left _ -> pure () Right () -> runStderrLoggingT $ logWarn $ "PostgreSQL unexpectedly parsed label: " <> Text.pack (show t) tryConvertTextToLabelInPG :: Connection -> Text -> IO (Either String ()) tryConvertTextToLabelInPG conn t = try (void $ query @_ @[Int] conn "select ?::ltree where false" $ Only ltree) <&> Bifunctor.first (\(e :: SqlError) -> if "syntax error at position " `C8.isPrefixOf` sqlErrorMsg e then "Failed to parse label via PostgreSQL: " <> show t else throw e) where -- Produce an ltree by joining a label with itself ltree = t <> "." <> t genAnyText :: Gen Text genAnyText = Text.pack <$> arbitrary genPGText :: Gen Text genPGText = genAnyText `suchThat` (not . Text.isInfixOf "\0")