medea-1.0.0: test/validator-quickcheck/Main.hs
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad.Except (runExcept)
import Data.Aeson (Array, Object, ToJSON, Value, Value (..), encode)
import Data.Aeson.Arbitrary
( ObjGenOpts (..),
arbitraryArray,
arbitraryObj,
arbitraryValue,
isArray,
isBool,
isNull,
isNumber,
isObject,
isString,
)
import Data.Either (isLeft, isRight)
import Data.HashMap.Strict (filterWithKey, lookup)
import Data.Medea (Schema, loadSchemaFromFile, validate)
import Data.Text (Text)
import qualified Data.Vector as V
import Test.Hspec (Spec, describe, hspec, it, parallel, runIO, shouldNotSatisfy)
import Test.Hspec.Core.Spec (SpecM)
import Test.QuickCheck ((==>), Gen, Property, arbitrary, forAll, property)
import qualified Test.QuickCheck.Gen as Gen
import TestM (isParseError, isSchemaError, runTestM)
import Prelude hiding (lookup)
main :: IO ()
main = hspec . parallel $ do
describe "Any schema" . testAny $ "any.medea"
describe "Null schema" . testSingular "null.medea" "null" $ isNull
describe "Boolean schema" . testSingular "boolean.medea" "boolean" $ isBool
describe "Number schema" . testSingular "number.medea" "number" $ isNumber
describe "String schema" . testSingular "string.medea" "string" $ isString
describe "Array schema" . testSingular "array.medea" "array" $ isArray
describe "Object schema" . testSingular "object.medea" "object" $ isObject
describe "Boolean/null schema" . testSingular "nullable-boolean.medea" "boolean/null" $ isNull .|| isBool
describe "Number/null schema" . testSingular "nullable-number.medea" "number/null" $ isNull .|| isNumber
describe "String/null schema" . testSingular "nullable-string.medea" "string/null" $ isNull .|| isString
describe "Array/null schema" . testSingular "nullable-array.medea" "array/null" $ isNull .|| isArray
describe "Object/null schema" . testSingular "nullable-object.medea" "object/null" $ isNull .|| isObject
describe "String with Values Schema" $ do
testStringVals "stringVals.medea" ["bar", "baz"]
testStringVals "stringVals2.medea" ["accountant", "barber", "bishop", "baker"]
describe "Object schema with 1 property and no additional allowed" $ do
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo"] [] 0 2,
objTestPath = "1-property-no-additional-1.medea",
objTestPred = hasProperty "foo" isBool,
objAdditionalPred = const False
}
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo"] [] 0 2,
objTestPath = "1-property-no-additional-2.medea",
objTestPred = hasProperty "foo" isNull,
objAdditionalPred = const False
}
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo"] [] 0 2,
objTestPath = "1-property-no-additional-3.medea",
objTestPred = hasProperty "foo" isArray,
objAdditionalPred = const False
}
describe "Object schema with 1 property and additional allowed" $ do
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo"] [] 0 3,
objTestPath = "1-property-additional-1.medea",
objTestPred = hasProperty "foo" isString,
objAdditionalPred = const True
}
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo"] [] 0 3,
objTestPath = "1-property-additional-2.medea",
objTestPred = hasProperty "foo" isNumber,
objAdditionalPred = const True
}
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo"] [] 0 3,
objTestPath = "1-property-additional-3.medea",
objTestPred = hasProperty "foo" isObject,
objAdditionalPred = const True
}
describe "Object schema with 3 properties and no additional allowed" $ do
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo", "bar", "bazz"] [] 0 1,
objTestPath = "3-property-no-additional-1.medea",
objTestPred =
hasProperty "foo" (isNumber .|| isArray)
.&& hasProperty "bazz" (isNull .|| isBool),
objAdditionalPred = const False
}
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["bar", "bazz"] ["foo"] 0 1,
objTestPath = "3-property-no-additional-2.medea",
objTestPred =
hasOptionalProperty "foo" (isNumber .|| isArray)
.&& hasProperty "bazz" (isNull .|| isBool),
objAdditionalPred = const False
}
describe "Object schema with 3 properties and additional allowed" $ do
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo", "bar", "bazz"] [] 0 3,
objTestPath = "3-property-additional-allowed-1.medea",
objTestPred = hasProperty "foo" isBool .&& hasProperty "bazz" isString,
objAdditionalPred = const True
}
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["bar", "bazz"] ["foo"] 0 3,
objTestPath = "3-property-additional-allowed-2.medea",
objTestPred = hasOptionalProperty "foo" isNumber .&& hasProperty "bazz" isNull,
objAdditionalPred = const True
}
describe "Object schema with additional property schema" $ do
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts [] [] 0 3,
objTestPath = "map-number-bool.medea",
objTestPred = const True,
objAdditionalPred = isNumber .|| isBool
}
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo"] [] 0 3,
objTestPath = "map-with-1-specified.medea",
objTestPred = hasProperty "foo" (isArray .|| isObject),
objAdditionalPred = isNumber .|| isBool
}
testObject
ObjTestParams
{ objTestOpts = ObjGenOpts ["foo"] ["bazz"] 0 3,
objTestPath = "map-with-2-specified.medea",
objTestPred = hasProperty "foo" (isArray .|| isObject),
objAdditionalPred = isNumber .|| isBool
}
describe "Array schema with element_type only" $ do
testList
ListTestParams
{ listTestOpts = (0, 3),
listTestPath = "list-1.medea",
elementPred = isNumber .|| isBool .|| isObject,
lenPred = const True
}
testList
ListTestParams
{ listTestOpts = (1, 3),
listTestPath = "list-2.medea",
elementPred = isNumber .|| isBool .|| isObject,
lenPred = const True
}
describe "Array schema with length spec only" $ do
testList
ListTestParams
{ listTestOpts = (1, 6),
listTestPath = "list-3.medea",
elementPred = const True,
lenPred = arrayLenGE 2
}
testList
ListTestParams
{ listTestOpts = (1, 6),
listTestPath = "list-4.medea",
elementPred = const True,
lenPred = arrayLenLE 5
}
testList
ListTestParams
{ listTestOpts = (1, 6),
listTestPath = "list-5.medea",
elementPred = const True,
lenPred = arrayLenLE 5 .&& arrayLenGE 3
}
describe "Array schema with length and element type" $ do
testList
ListTestParams
{ listTestOpts = (1, 4),
listTestPath = "list-6.medea",
elementPred = isNull .|| isBool .|| isNumber,
lenPred = arrayLenGE 2 .&& arrayLenLE 3
}
testList
ListTestParams
{ listTestOpts = (1, 4),
listTestPath = "list-7.medea",
elementPred = isNull .|| isBool .|| isNumber,
lenPred = arrayLenGE 2 .&& arrayLenLE 3
}
describe "Array schema with tuple spec" $ do
testTuple
TupleTestParams
{ tupleTestOpts = (3, 4),
tupleTestPath = "3-tuple.medea",
tuplePreds = [isNumber .|| isArray, isBool, const True]
}
testTuple
TupleTestParams
{ tupleTestOpts = (1, 3),
tupleTestPath = "2-tuple.medea",
tuplePreds = [isObject .|| isNull, isString .|| isNumber]
}
data ObjTestParams
= ObjTestParams
{ objTestOpts :: ObjGenOpts,
objTestPath :: FilePath,
objTestPred :: Object -> Bool,
-- | The predice to be used on additional properties
objAdditionalPred :: Value -> Bool
}
data ListTestParams
= ListTestParams
{ listTestOpts :: (Int, Int),
listTestPath :: FilePath,
elementPred :: Value -> Bool,
lenPred :: Array -> Bool
}
data TupleTestParams
= TupleTestParams
{ tupleTestOpts :: (Int, Int),
tupleTestPath :: FilePath,
tuplePreds :: [Value -> Bool]
}
-- Helpers
(.||) :: (a -> Bool) -> (a -> Bool) -> (a -> Bool)
f .|| g = (||) <$> f <*> g
(.&&) :: (a -> Bool) -> (a -> Bool) -> (a -> Bool)
f .&& g = (&&) <$> f <*> g
testAny :: FilePath -> Spec
testAny fp = do
scm <- loadAndParse $ prependTestDir fp
it ("Should validate anything: " ++ fp) (validationSuccess arbitraryValue (const True) scm)
testSingular :: FilePath -> String -> (Value -> Bool) -> Spec
testSingular fp name p = do
scm <- loadAndParse $ prependTestDir fp
it ("Should validate " ++ name ++ "s: " ++ fp) (validationSuccess arbitraryValue p scm)
it ("Should not validate non-" ++ name ++ "s: " ++ fp) (validationFail arbitraryValue (not . p) scm)
testObject :: ObjTestParams -> Spec
testObject (ObjTestParams opts fp p extraPred) = do
scm <- loadAndParse $ prependTestDir fp
it ("Should validate valid objects" ++ ": " ++ fp) (validationSuccess gen p' scm)
it ("Should not validate invalid objects" ++ ": " ++ fp) (validationFail gen (not . p') scm)
where
gen = arbitraryObj opts
p' = p .&& makeMapPred opts extraPred
testList :: ListTestParams -> Spec
testList (ListTestParams opts fp pTypes pLen) = do
scm <- loadAndParse $ prependTestDir fp
it ("Should validate valid lists" ++ ": " ++ fp) (validationSuccess gen p scm)
it ("Should not validate invalid lists" ++ ": " ++ fp) (validationFail gen (not . p) scm)
where
gen = arbitraryArray opts
p = all pTypes .&& pLen
testTuple :: TupleTestParams -> Spec
testTuple (TupleTestParams opts fp preds) = do
scm <- loadAndParse $ prependTestDir fp
it ("Should validate valid tuples" ++ ": " ++ fp) (validationSuccess gen p scm)
it ("Should not validate invalid tuples" ++ ": " ++ fp) (validationFail gen (not . p) scm)
where
gen = arbitraryArray opts
p arr = (and . zipWith ($) preds . V.toList $ arr) && (V.length arr == length preds)
-- "validation succeeded" property
validationSuccess :: (ToJSON a, Show a) => Gen a -> (a -> Bool) -> Schema -> Property
validationSuccess gen p scm = property $ forAll gen prop
where
prop v = p v ==> isRight . runExcept . validate scm . encode $ v
-- "validation failed" property
validationFail :: (ToJSON a, Show a) => Gen a -> (a -> Bool) -> Schema -> Property
validationFail gen p scm = property $ forAll gen prop
where
prop v = p v ==> isLeft . runExcept . validate scm . encode $ v
-- Returns true iff the value is an object with the given property and the
-- property-value satisfies the predicate.
hasProperty :: Text -> (Value -> Bool) -> Object -> Bool
hasProperty propName p obj = maybe False p $ lookup propName obj
-- Like hasProperty but is also true when the given property is absent.
hasOptionalProperty :: Text -> (Value -> Bool) -> Object -> Bool
hasOptionalProperty propName p obj = maybe True p $ lookup propName obj
makeMapPred :: ObjGenOpts -> (Value -> Bool) -> Object -> Bool
makeMapPred (ObjGenOpts props optProps _ _) p = all p . filterWithKey (\k _ -> k `notElem` specifiedProps)
where
specifiedProps = props ++ optProps
testStringVals :: FilePath -> [String] -> Spec
testStringVals fp validStrings = do
scm <- loadAndParse $ prependTestDir fp
it ("Should validate only strings in " ++ show validStrings ++ ": " ++ fp) (validationSuccess genString p scm)
it ("Shouldn't validate strings not in " ++ show validStrings ++ "s: " ++ fp) (validationFail genString (not . p) scm)
where
genString :: Gen.Gen String
genString = Gen.oneof [Gen.elements validStrings, arbitrary]
p = (`elem` validStrings)
loadAndParse :: FilePath -> SpecM () Schema
loadAndParse fp = do
result <- runIO . runTestM . loadSchemaFromFile $ fp
it ("Should parse: " ++ fp) (result `shouldNotSatisfy` isParseError)
it ("Should build: " ++ fp) (result `shouldNotSatisfy` isSchemaError)
case result of
Left e -> error ("This should never happen: " ++ show e)
Right scm -> pure scm
prependTestDir :: FilePath -> FilePath
prependTestDir = ("./conformance/validation/" ++)
arrayLenGE :: Int -> Array -> Bool
arrayLenGE len arr = V.length arr >= len
arrayLenLE :: Int -> Array -> Bool
arrayLenLE len arr = V.length arr <= len