hedgehog-gen-json-0.4.0: test/Hedgehog/Gen/JSON/JSONSpec.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Hedgehog.Gen.JSON.JSONSpec
( tests
) where
import Control.Lens (over, set)
import qualified Data.Aeson as Aeson
import Data.Fixed (mod')
import qualified Data.HashMap.Strict as H
import qualified Data.HashSet as HS
import qualified Data.Scientific as Scientific
import qualified Data.Text as Text
import Hedgehog
import qualified Hedgehog.Gen as Gen
import Hedgehog.Gen.JSON
import Hedgehog.Gen.JSON.JSONSchema
import qualified Hedgehog.Range as Range
import qualified Prelude as P
import Protolude
import Test.Tasty
import Test.Tasty.Hedgehog
import Text.Regex.PCRE
prop_generatedUnconstrainedJSON :: Property
prop_generatedUnconstrainedJSON =
property $ do
v <- forAll $ genJSON sensibleRanges
assert $ isJust (Aeson.decodeStrict v :: Maybe Aeson.Value)
prop_constrainedValueFromEnum :: Property
prop_constrainedValueFromEnum =
property $ do
values <- forAll $ Gen.nonEmpty (Range.linear 1 10) (Gen.text (Range.linear 1 100) Gen.unicode)
schema <- forAll $ over schemaEnum (const $ Just $ AnyConstraintEnum (Aeson.String <$> values)) <$> genSchema
v <- forAll $ genConstrainedJSON sensibleRanges schema
assert $ isJust $ find (== Aeson.decodeStrict v) (Just <$> values)
prop_constrainedValueFromConst :: Property
prop_constrainedValueFromConst =
property $ do
c <- forAll $ genJSONValue sensibleRanges
schema <- forAll $ over schemaConst (const $ Just $ AnyConstraintConst c) <$> genSchema
v <- forAll $ genConstrainedJSON sensibleRanges schema
Aeson.decodeStrict v === Just c
prop_constrainedNumber :: Property
prop_constrainedNumber =
property $ do
vmin <- forAll $ Gen.maybe $ Scientific.fromFloatDigits <$> Gen.double (Range.linearFrac 0 5000)
vminEx <- forAll $ Gen.maybe $ Scientific.fromFloatDigits <$> Gen.double (Range.linearFrac 0 5000)
vmax <- forAll $ Gen.maybe $ Scientific.fromFloatDigits <$> Gen.double (Range.linearFrac 6000 10000)
vmaxEx <- forAll $ Gen.maybe $ Scientific.fromFloatDigits <$> Gen.double (Range.linearFrac 6000 10000)
let schema =
(set schemaMinimum (NumberConstraintMinimum <$> vmin) .
set schemaMaximum (NumberConstraintMaximum <$> vmax) .
set schemaExclusiveMinimum (NumberConstraintExclusiveMinimum <$> vminEx) . set schemaExclusiveMaximum (NumberConstraintExclusiveMaximum <$> vmaxEx))
(singleTypeSchema NumberType)
(Aeson.Number v) <- forAll $ genConstrainedJSONValue sensibleRanges schema
assert $ maybe True (v >=) vmin
assert $ maybe True (v <=) vmax
assert $ maybe True (v <) vmaxEx
assert $ maybe True (v >) vminEx
prop_constrainedInteger :: Property
prop_constrainedInteger =
property $ do
vmin <- forAll $ Gen.maybe $ fromInteger <$> Gen.integral (Range.linear (-50) 50)
vminEx <- forAll $ Gen.maybe $ fromInteger <$> Gen.integral (Range.linear (-50) 50)
vmax <- forAll $ Gen.maybe $ fromInteger <$> Gen.integral (Range.linear 50 100)
vmaxEx <- forAll $ Gen.maybe $ fromInteger <$> Gen.integral (Range.linear 50 100)
multipleOf <- forAll $ Gen.maybe $ fromInteger <$> Gen.integral (Range.linear 1 10)
let schema =
(set schemaMinimum (NumberConstraintMinimum <$> vmin) .
set schemaMaximum (NumberConstraintMaximum <$> vmax) .
set schemaExclusiveMinimum (NumberConstraintExclusiveMinimum <$> vminEx) .
set schemaExclusiveMaximum (NumberConstraintExclusiveMaximum <$> vmaxEx) . set schemaMultipleOf (NumberConstraintMultipleOf <$> multipleOf))
(singleTypeSchema IntegerType)
(Aeson.Number v) <- forAll $ genConstrainedJSONValue sensibleRanges schema
assert $ maybe True (v >=) vmin
assert $ maybe True (v <=) vmax
assert $ maybe True (v <) vmaxEx
assert $ maybe True (v >) vminEx
assert $ maybe True (\x -> v `mod'` x == 0) multipleOf
assert $ Scientific.isInteger v
prop_constrainedString :: Property
prop_constrainedString =
property $ do
minLength <- forAll $ Gen.maybe $ Gen.integral (Range.linear 0 50)
maxLength <- forAll $ Gen.maybe $ Gen.integral (Range.linear 50 100)
regexp <- forAll $ Gen.maybe $ Gen.constant "[a-zA-Z0-9]{3,9}" -- Not very arbitrary, I know.
format <- forAll $ Gen.maybe $ Gen.element ["uuid", "date-time", "RFC 3339 date-time"]
let schema =
(set schemaPattern (StringConstraintPattern <$> regexp) .
set schemaMinLength (StringConstraintMinLength <$> minLength) .
set schemaMaxLength (StringConstraintMaxLength <$> maxLength) . set schemaFormat (StringConstraintFormat <$> format))
(singleTypeSchema StringType)
(Aeson.String v) <- forAll $ genConstrainedJSONValue sensibleRanges schema
case regexp of
Just p -> regexMatches p v
Nothing ->
case format of
(Just f) -> hasFormat (StringConstraintFormat f) v
Nothing -> assert $ Text.length v >= fromMaybe 0 minLength && Text.length v <= fromMaybe 1000 maxLength
prop_constrainedObject :: Property
prop_constrainedObject =
property $ do
nonRequiredFields <- forAll $ H.fromList <$> (Gen.list (Range.linear 1 10) $ ((,) <$> (Gen.text (Range.linear 1 10) Gen.unicode) <*> genSchema))
requiredFields <- forAll $ H.fromList <$> (Gen.list (Range.linear 1 10) $ ((,) <$> (Gen.text (Range.linear 1 10) Gen.unicode) <*> genSchema))
let schema =
(set schemaProperties (Just $ ObjectConstraintProperties (nonRequiredFields `H.union` requiredFields)) .
set schemaRequired (Just $ ObjectConstraintRequired $ H.keys requiredFields))
(singleTypeSchema ObjectType)
(Aeson.Object v) <- forAll $ genConstrainedJSONValue sensibleRanges schema
assert $ all (`elem` (H.keys v)) (H.keys requiredFields)
prop_constrainedArray :: Property
prop_constrainedArray =
property $ do
uniqueItems <- forAll $ Gen.maybe Gen.bool
minItems <- forAll $ Gen.maybe $ Gen.integral (Range.linear 0 2)
maxItems <- forAll $ Gen.maybe $ Gen.integral (Range.linear 3 5)
itemSchema <- forAll genSchema
let schema =
(set schemaMinItems (ArrayConstraintMinItems <$> minItems) .
set schemaMaxItems (ArrayConstraintMaxItems <$> maxItems) .
set schemaUniqueItems (ArrayConstraintUniqueItems <$> uniqueItems) . set schemaItems (Just $ ArrayConstraintItems itemSchema))
(singleTypeSchema ArrayType)
(Aeson.Array v) <- forAll $ genConstrainedJSONValue sensibleRanges schema
assert $ maybe True (length v >=) minItems
assert $ maybe True (length v <=) maxItems
assert $
maybe
True
(\b ->
if b
then isUnique v
else True)
uniqueItems
prop_constrainedValueFromReferencedSchema :: Property
prop_constrainedValueFromReferencedSchema =
property $ do
subSchema <- forAll genSchema
let schema = emptySchema {
_schemaType = Just $ SingleType ArrayType,
_schemaItems = Just $ ArrayConstraintItems $ emptySchema { _schemaRef = Just $ AnyConstraintRef "#/definitions/x" },
_schemaDefinitions = Just $ AnyConstraintDefinitions $ H.fromList [("x", subSchema)]
}
_ <- forAll $ genConstrainedJSONValue sensibleRanges schema
success
prop_decodesSchema :: Property
prop_decodesSchema = property $ decoded === Right expected
where
schemaJson = "{\"type\":\"object\",\"properties\":{\"user_id\":{\"type\":\"integer\"},\"user_domain\":{\"type\":\"string\"}},\"required\":[\"user_id\"], \"definitions\": {\"positiveInteger\": { \"type\": \"integer\", \"exclusiveMinimum\": 0 }}}"
decoded :: Either P.String Schema = Aeson.eitherDecode schemaJson
expected =
(set schemaRequired (Just (ObjectConstraintRequired ["user_id"])) .
set schemaProperties ((Just . ObjectConstraintProperties) (H.fromList [("user_id", singleTypeSchema IntegerType), ("user_domain", singleTypeSchema StringType)])) .
set schemaDefinitions ((Just . AnyConstraintDefinitions) (H.fromList [("positiveInteger", (singleTypeSchema IntegerType) { _schemaExclusiveMinimum = Just (NumberConstraintExclusiveMinimum 0)})])))
(singleTypeSchema ObjectType)
genSchema :: Gen Schema
genSchema = do
t <- Gen.enumBounded
pure $ singleTypeSchema t
singleTypeSchema :: PrimitiveType -> Schema
singleTypeSchema t = set schemaType (Just $ SingleType t) emptySchema
isUnique :: (Hashable a, Eq a, Foldable t) => t a -> Bool
isUnique xs = (toList . HS.fromList . toList) xs == toList xs
regexMatches :: (MonadTest m, HasCallStack) => Text -> Text -> m ()
regexMatches regex t = do
footnoteShow (t <> " does not match the regexp " <> regex)
assert $ (toS t :: P.String) =~ (toS regex :: P.String)
hasFormat :: (MonadTest m) => StringConstraintFormat -> Text -> m ()
hasFormat (StringConstraintFormat "uuid") t =
footnoteShow (t <> " does hot have the format of uuid") >> regexMatches "[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}" t
hasFormat (StringConstraintFormat "date-time") t =
footnoteShow (t <> " does hot have the format of date-time") >>
regexMatches
"([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\\.[0-9]+)?(([Zz])|([\\+|\\-]([01][0-9]|2[0-3]):[0-5][0-9]))"
t
hasFormat (StringConstraintFormat "RFC 3339 date-time") t = hasFormat (StringConstraintFormat "date-time") t
hasFormat _ _ = footnote "Format unsupported" >> failure
tests :: TestTree
tests =
testGroup
"Generated JSON"
[ testProperty "Generated unconstrained values are valid JSON" prop_generatedUnconstrainedJSON
, testProperty "Decodes JSON Schema" prop_decodesSchema
, testProperty "Generates constrained values from JSON Schema enum when present" prop_constrainedValueFromEnum
, testProperty "Generates constrained values from JSON Schema const when present" prop_constrainedValueFromConst
, testProperty "Generates a constrained number" prop_constrainedNumber
, testProperty "Generates a constrained integer" prop_constrainedInteger
, testProperty "Generates a constrained string" prop_constrainedString
, testProperty "Generates a constrained object" prop_constrainedObject
, testProperty "Generates a constrained array" prop_constrainedArray
, testProperty "Generates a value from a referenced schema" prop_constrainedValueFromReferencedSchema
]