module Test.SqlMarshaller
( sqlMarshallerTests
)
where
import qualified Control.Monad.IO.Class as MIO
import qualified Data.Bifunctor as Bifunctor
import qualified Data.ByteString.Char8 as B8
import qualified Data.Either as Either
import qualified Data.Int as Int
import qualified Data.Set as Set
import qualified Data.String as String
import qualified Data.Text as T
import Hedgehog ((===))
import qualified Hedgehog as HH
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import qualified Orville.PostgreSQL.ErrorDetailLevel as ErrorDetailLevel
import qualified Orville.PostgreSQL.Execution.ExecutionResult as Result
import qualified Orville.PostgreSQL.Marshall as Marshall
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue
import Test.Expr.TestSchema (assertEqualSqlRows)
import qualified Test.PgGen as PgGen
import qualified Test.Property as Property
sqlMarshallerTests :: Property.Group
sqlMarshallerTests =
Property.group
"SqlMarshaller"
[ property_returnPureValue
, property_combineWithApplicative
, property_marshellField_readSingleField
, prop_marshallField_missingColumn
, prop_marshallField_decodeValueFailure
, prop_marshallResultFromSql_Foo
, prop_marshallResultFromSql_Bar
, prop_foldMarshallerFields
, prop_passMaybeThrough
, prop_partialMap
]
property_returnPureValue :: Property.NamedProperty
property_returnPureValue =
Property.namedProperty "Can read a pure Int via SqlMarshaller" $ do
someInt <- HH.forAll generateInt
result <- marshallTestRowFromSql (pure someInt) (Result.mkFakeLibPQResult [] [[]])
Bifunctor.first show result === Right [someInt]
property_combineWithApplicative :: Property.NamedProperty
property_combineWithApplicative =
Property.namedProperty "Can combine SqlMarshallers with <*>" $ do
firstInt <- HH.forAll generateInt
secondInt <- HH.forAll generateInt
result <- marshallTestRowFromSql ((pure (+ firstInt)) <*> (pure secondInt)) (Result.mkFakeLibPQResult [] [[]])
Bifunctor.first show result === Right [firstInt + secondInt]
property_marshellField_readSingleField :: Property.NamedProperty
property_marshellField_readSingleField =
Property.namedProperty "Read a single field from a result row using marshallField" $ do
targetName <- HH.forAll generateName
targetValue <- HH.forAll generateInt32
namesBefore <- HH.forAll (generateNamesOtherThan targetName)
namesAfter <- HH.forAll (generateNamesOtherThan targetName)
valuesBefore <- HH.forAll (generateAssociatedValues namesBefore generateInt32)
valuesAfter <- HH.forAll (generateAssociatedValues namesAfter generateInt32)
let
fieldDef = Marshall.integerField targetName
marshaller = Marshall.marshallField id fieldDef
input =
Result.mkFakeLibPQResult
(map B8.pack (namesBefore ++ (targetName : namesAfter)))
[map SqlValue.fromInt32 (valuesBefore ++ (targetValue : valuesAfter))]
result <- marshallTestRowFromSql marshaller input
Bifunctor.first show result === Right [targetValue]
prop_marshallField_missingColumn :: Property.NamedProperty
prop_marshallField_missingColumn =
Property.namedProperty "marshallField fails gracefully when decoding a non-existent column" $ do
targetName <- HH.forAll generateName
otherNames <- HH.forAll (generateNamesOtherThan targetName)
otherValues <- HH.forAll (generateAssociatedValues otherNames generateInt32)
let
fieldDef = Marshall.integerField targetName
marshaller = Marshall.marshallField id fieldDef
input =
Result.mkFakeLibPQResult
(map B8.pack otherNames)
[map SqlValue.fromInt32 otherValues]
expectedError =
Marshall.MarshallError
{ Marshall.marshallErrorDetailLevel = ErrorDetailLevel.maximalErrorDetailLevel
, Marshall.marshallErrorRowIdentifier = mempty
, Marshall.marshallErrorDetails =
Marshall.MissingColumnError $
Marshall.MissingColumnErrorDetails
{ Marshall.missingColumnName = B8.pack targetName
, Marshall.actualColumnNames = Set.fromList $ fmap B8.pack otherNames
}
}
result <- marshallTestRowFromSql marshaller input
-- Use show on the error here so MarshallError and friends don't
-- need an Eq instance
Bifunctor.first show result === Left (show expectedError)
prop_marshallField_decodeValueFailure :: Property.NamedProperty
prop_marshallField_decodeValueFailure =
Property.namedProperty "marshallField fails gracefully when failing to decode a value" $ do
targetName <- HH.forAll generateName
nonIntegerText <- HH.forAll (Gen.text (Range.linear 0 10) Gen.alpha)
let
fieldDef = Marshall.integerField targetName
marshaller = Marshall.marshallField id fieldDef
input =
Result.mkFakeLibPQResult
[B8.pack targetName]
[[SqlValue.fromText nonIntegerText]]
result <- marshallTestRowFromSql marshaller input
case result of
Right n -> do
HH.annotateShow n
HH.footnote "Expected decoding failure, but got success"
HH.failure
Left rowDecodeErr ->
case Marshall.marshallErrorDetails rowDecodeErr of
Marshall.DecodingError details ->
map fst (Marshall.decodingErrorValues details) === [B8.pack targetName]
err -> do
HH.annotate $ Marshall.renderMarshallErrorDetails ErrorDetailLevel.maximalErrorDetailLevel err
HH.footnote "Expected DecodingError error, but got another error instead."
HH.failure
prop_marshallResultFromSql_Foo :: Property.NamedProperty
prop_marshallResultFromSql_Foo =
Property.namedProperty "marshallResultFromSql decodes all rows in Foo result set" $ do
foos <- HH.forAll $ Gen.list (Range.linear 0 10) generateFoo
let
mkRowValues foo =
[ SqlValue.fromText (fooName foo)
, SqlValue.fromInt32 (fooSize foo)
, maybe SqlValue.sqlNull SqlValue.fromBool (fooOption foo)
]
input =
Result.mkFakeLibPQResult
[B8.pack "name", B8.pack "size", B8.pack "option"]
(map mkRowValues foos)
result <- marshallTestRowFromSql fooMarshaller input
Bifunctor.first show result === Right foos
prop_marshallResultFromSql_Bar :: Property.NamedProperty
prop_marshallResultFromSql_Bar =
Property.namedProperty "marshallResultFromSql decodes all rows in Bar result set" $ do
bars <- HH.forAll $ Gen.list (Range.linear 0 10) generateBar
let
mkRowValues bar =
[ SqlValue.fromDouble (barNumber bar)
, maybe SqlValue.sqlNull SqlValue.fromText (barComment bar)
, maybe SqlValue.sqlNull SqlValue.fromText (barLabel bar)
]
input =
Result.mkFakeLibPQResult
[B8.pack "number", B8.pack "comment", B8.pack "label"]
(map mkRowValues bars)
result <- marshallTestRowFromSql barMarshaller input
Bifunctor.first show result === Right bars
prop_foldMarshallerFields :: Property.NamedProperty
prop_foldMarshallerFields =
Property.namedProperty "foldMarshallerFields collects all fields as their sql values" $ do
foo <- HH.forAll generateFoo
let
addField entry fields =
case entry of
Marshall.Natural fieldDef (Just getValue) ->
(Marshall.fieldName fieldDef, Marshall.fieldValueToSqlValue fieldDef (getValue foo)) : fields
Marshall.Natural _ Nothing ->
fields
Marshall.Synthetic _ ->
fields
actualFooRow =
Marshall.foldMarshallerFields
fooMarshaller
[]
addField
expectedFooRow =
[ (Marshall.stringToFieldName "name", SqlValue.fromText $ fooName foo)
, (Marshall.stringToFieldName "size", SqlValue.fromInt32 $ fooSize foo)
, (Marshall.stringToFieldName "option", maybe SqlValue.sqlNull SqlValue.fromBool $ fooOption foo)
]
[actualFooRow] `assertEqualSqlRows` [expectedFooRow]
prop_passMaybeThrough :: Property.NamedProperty
prop_passMaybeThrough =
Property.namedProperty "can pass a Maybe through SqlMarshaller" $ do
someMaybeBool <- HH.forAll $ Gen.maybe Gen.bool
result <- marshallTestRowFromSql (pure someMaybeBool) (Result.mkFakeLibPQResult [] [[]])
Bifunctor.first show result === Right [someMaybeBool]
prop_partialMap :: Property.NamedProperty
prop_partialMap =
Property.namedProperty "can use marshallPartial to fail decoding with in a custom way" $ do
texts <- HH.forAll $ Gen.list (Range.linear 0 10) (PgGen.pgText (Range.linear 0 10))
let
validateText text =
if T.length text > 8
then Left "Text too long"
else Right text
mkRowValues text =
[ SqlValue.fromText text
]
input =
Result.mkFakeLibPQResult
[B8.pack "text"]
(map mkRowValues texts)
marshaller =
Marshall.marshallPartial $
validateText
<$> Marshall.marshallField id (Marshall.unboundedTextField "text")
mkExpected text =
case validateText text of
Right validText ->
Right validText
Left message ->
Left $
-- Use show here to render the error so that MarshallError
-- and friends don't need to have an Eq instance
show $
Marshall.MarshallError
{ Marshall.marshallErrorDetailLevel = ErrorDetailLevel.maximalErrorDetailLevel
, Marshall.marshallErrorRowIdentifier = mempty
, Marshall.marshallErrorDetails =
Marshall.DecodingError $
Marshall.DecodingErrorDetails
{ Marshall.decodingErrorValues = [(B8.pack "text", SqlValue.fromText text)]
, Marshall.decodingErrorMessage = message
}
}
expected =
traverse mkExpected texts
HH.cover 1 (String.fromString "With no errors") (Either.isRight expected)
HH.cover 1 (String.fromString "With at least one error") (Either.isLeft expected)
result <- marshallTestRowFromSql marshaller input
Bifunctor.first show result === expected
data Foo = Foo
{ fooName :: T.Text
, fooSize :: Int.Int32
, fooOption :: Maybe Bool
}
deriving (Eq, Show)
data Bar = Bar
{ barNumber :: Double
, barComment :: Maybe T.Text
, barLabel :: Maybe T.Text
}
deriving (Eq, Show)
fooMarshaller :: Marshall.SqlMarshaller Foo Foo
fooMarshaller =
Foo
<$> Marshall.marshallField fooName (Marshall.unboundedTextField "name")
<*> Marshall.marshallField fooSize (Marshall.integerField "size")
<*> Marshall.marshallField fooOption (Marshall.nullableField $ Marshall.booleanField "option")
generateFoo :: HH.Gen Foo
generateFoo =
Foo
<$> PgGen.pgText (Range.linear 0 16)
<*> generateInt32
<*> Gen.maybe Gen.bool
barMarshaller :: Marshall.SqlMarshaller Bar Bar
barMarshaller =
Bar
<$> Marshall.marshallField barNumber (Marshall.doubleField "number")
<*> Marshall.marshallField barComment (Marshall.nullableField $ Marshall.unboundedTextField "comment")
<*> Marshall.marshallField barLabel (Marshall.nullableField $ Marshall.boundedTextField "label" 16)
generateBar :: HH.Gen Bar
generateBar =
Bar
<$> PgGen.pgDouble
<*> Gen.maybe (PgGen.pgText $ Range.linear 0 32)
<*> Gen.maybe (PgGen.pgText $ Range.linear 1 16)
generateNamesOtherThan :: String -> HH.Gen [String]
generateNamesOtherThan specialName =
Gen.list
(Range.linear 0 10)
(generateNameOtherThan specialName)
generateAssociatedValues :: [key] -> HH.Gen value -> HH.Gen [value]
generateAssociatedValues keys genValue =
traverse (const genValue) keys
generateInt32 :: HH.Gen Int.Int32
generateInt32 =
Gen.int32 (Range.exponentialFrom 0 minBound maxBound)
generateNameOtherThan :: String -> HH.Gen String
generateNameOtherThan specialName =
Gen.filter (/= specialName) generateName
generateName :: HH.Gen String
generateName =
Gen.string (Range.linear 1 256) Gen.alphaNum
generateInt :: HH.MonadGen m => m Int
generateInt = Gen.int $ Range.exponential 1 1024
marshallTestRowFromSql ::
( HH.MonadTest m
, MIO.MonadIO m
, Result.ExecutionResult result
) =>
Marshall.SqlMarshaller writeEntity readEntity ->
result ->
m (Either Marshall.MarshallError [readEntity])
marshallTestRowFromSql marshaller input =
HH.evalIO $
Marshall.marshallResultFromSqlUsingRowIdExtractor
ErrorDetailLevel.maximalErrorDetailLevel
(Marshall.mkRowIdentityExtractor [] input)
marshaller
input