packages feed

wai-middleware-validation-0.1.0.0: test/Network/Wai/Middleware/ValidationSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}

module Network.Wai.Middleware.ValidationSpec (spec) where

import           Control.Monad                     (forM_)
import           Control.Monad.IO.Class            (liftIO)
import qualified Data.ByteString.Lazy              as L
import           Data.Maybe                        (fromMaybe, isJust)
import           Data.String.Here                  (here)
import           Network.HTTP.Types                (StdMethod (GET, POST), methodPost, status201,
                                                    status400, status500)
import           Network.Wai                       (requestMethod, responseLBS)
import           Network.Wai.Test
import           Test.Hspec

import           Network.Wai.Middleware.Validation


openApiJson :: L.ByteString
openApiJson = [here|
{
    "openapi": "3.0.0",
    "info": { "title": "validator test", "version": "1.0.0" },
    "paths": {
        "/articles": {
            "get": {
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Article"
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/Article"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Article"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/articles/{articleId}": {
            "parameters": [
                {
                    "name": "articleId",
                    "in": "path",
                    "required": true,
                    "schema": { "type": "integer" }
                }
            ],
            "put": {
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/Article"
                            }
                        }
                    }
                },
                "responses": { "default": { "description": "response example" } }
            }
        }
    },
    "components": {
        "schemas": {
            "Article": {
                "type": "object",
                "required": [ "cint", "ctxt" ],
                "properties": {
                    "cint": { "type": "integer" },
                    "ctxt": { "type": "string" }
                }
            }
        }
    }
}
|]


spec :: Spec
spec = do
    describe "Middleware" $ do
        let
            mkApp responseBody _ respond = respond $ responseLBS status201 responseHeaders responseBody
            validResponseBody = [here| {"cint": 1, "ctxt": "RESPONSE"} |]
            invalidResponseBody = [here| {"cint": 1} |]
            mkSession requestBody = srequest $ SRequest (setPath (defaultRequest {requestMethod = methodPost}) "/articles") requestBody
            validRequestBody = [here| {"cint": 1, "ctxt": "REQUEST"} |]
            invalidRequestBody = [here| {"cint": 1, "ctxt": 0} |]

        context "request and response validation" $ do
            let validator = fromMaybe (error "Invalid OpenAPI document") (mkValidator' openApiJson)

            it "do nothing if the request and response body is valid" $ do
                sResponse <- liftIO $ runSession (mkSession validRequestBody) $ validator (mkApp validResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe` (status201, validResponseBody)

            it "returns 400 if the request body is invalid" $ do
                sResponse <- liftIO $ runSession (mkSession invalidRequestBody) $ validator (mkApp validResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe`
                    (status400, [here| {"title":"Validation failed","detail":"expected JSON value of type string\n"} |])

            it "returns 500 if the response body is invalid" $ do
                sResponse <- liftIO $ runSession (mkSession validRequestBody) $ validator (mkApp invalidResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe`
                    (status500, [here| {"title":"Validation failed","detail":"property \"ctxt\" is required, but not found in \"{\\\"cint\\\":1}\"\n"} |])

        context "request validation" $ do
            let reqValidator = fromMaybe (error "Invalid OpenAPI document") (mkRequestValidator' openApiJson)

            it "do nothing if the request and response body is valid" $ do
                sResponse <- liftIO $ runSession (mkSession validRequestBody) $ reqValidator (mkApp validResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe` (status201, validResponseBody)

            it "returns 400 if the request body is invalid" $ do
                sResponse <- liftIO $ runSession (mkSession invalidRequestBody) $ reqValidator (mkApp validResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe`
                    (status400, [here| {"title":"Validation failed","detail":"expected JSON value of type string\n"} |])

            it "do nothing even if the response body is invalid" $ do
                sResponse <- liftIO $ runSession (mkSession validRequestBody) $ reqValidator (mkApp invalidResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe` (status201, invalidResponseBody)

        context "response validation" $ do
            let resValidator = fromMaybe (error "Invalid OpenAPI document") (mkResponseValidator' openApiJson)

            it "do nothing if the request and response body is valid" $ do
                sResponse <- liftIO $ runSession (mkSession validRequestBody) $ resValidator (mkApp validResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe` (status201, validResponseBody)

            it "do nothing even if the request body is invalid" $ do
                sResponse <- liftIO $ runSession (mkSession invalidRequestBody) $ resValidator (mkApp validResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe` (status201, validResponseBody)

            it "returns 500 if the response body is invalid" $ do
                sResponse <- liftIO $ runSession (mkSession validRequestBody) $ resValidator (mkApp invalidResponseBody)
                (simpleStatus sResponse, simpleBody sResponse) `shouldBe`
                    (status500, [here| {"title":"Validation failed","detail":"property \"ctxt\" is required, but not found in \"{\\\"cint\\\":1}\"\n"} |])

    describe "mkValidator'" $ do
        it "returns Just Middleware if the OpenAPI document is valid" $
            isJust (mkValidator' openApiJson) `shouldBe` True

        it "returns Nothing if the OpenAPI document is invalid" $
            isJust (mkValidator' "") `shouldBe` False

        it "returns Nothing if the OpenAPI document has no paths object" $ do
            let json = [here|
{
    "openapi": "3.0.0",
    "info": { "title": "info example", "version": "1.0.0" },
    "components": {
    }
}
|]
            isJust (mkValidator' json) `shouldBe` False

    describe "mkRequestValidator'" $ do
        it "returns Just Middleware if the OpenAPI document is valid" $
            isJust (mkRequestValidator' openApiJson) `shouldBe` True

        it "returns Nothing if the OpenAPI document is invalid" $
            isJust (mkRequestValidator' "") `shouldBe` False

        it "returns Nothing if the OpenAPI document has no paths object" $ do
            let json = [here|
{
    "openapi": "3.0.0",
    "info": { "title": "info example", "version": "1.0.0" },
    "components": {
    }
}
|]
            isJust (mkRequestValidator' json) `shouldBe` False

    describe "mkResponseValidator'" $ do
        it "returns Just Middleware if the OpenAPI document is valid" $
            isJust (mkResponseValidator' openApiJson) `shouldBe` True

        it "returns Nothing if the OpenAPI document is invalid" $
            isJust (mkResponseValidator' "") `shouldBe` False

        it "returns Nothing if the OpenAPI document has no paths object" $ do
            let json = [here|
{
    "openapi": "3.0.0",
    "info": { "title": "info example", "version": "1.0.0" },
    "components": {
    }
}
|]
            isJust (mkResponseValidator' json) `shouldBe` False

    describe "validateRequestBody" $ do
        let makeOpenApiJson schemaJson = [here|
{
    "openapi": "3.0.0",
    "info": { "title": "info example", "version": "1.0.0" },
    "paths": {
        "/examples": {
            "post": {
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/Example"
                            }
                        }
                    }
                },
                "responses": { "default": { "description": "response example" } }
            }
        }
    },
    "components": {
        "schemas": {
            "Example": {
                "type": "object",
                "properties": |] <> schemaJson <> [here|
            }
        }
    }
}
|]
        context "type: integer" $ do
            let
                tests =
                    [ ( "returns Right [] if the value is integer"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": 1} |]
                      , Right []
                      )
                    , ( "returns Right [] if the value is integer"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": 1} |]
                      , Right []
                      )
                    , ( "returns Right [] if the value is integer"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": 1} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value is a number contains a decimal part"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": 1.1} |]
                      , Right ["not an integer"]
                      )
                    , ( "returns Right [error] if the value is a string type"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": "text"} |]
                      , Right ["expected JSON value of type integer"]
                      )
                    , ( "returns Right [error] if the value is a boolean type"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": true} |]
                      , Right ["expected JSON value of type integer"]
                      )
                    , ( "returns Right [error] if the value is an array type"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": [0, 1, 2]} |]
                      , Right ["expected JSON value of type integer"]
                      )
                    , ( "returns Right [error] if the value is an object type"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": {"k": "v"}} |]
                      , Right ["expected JSON value of type integer"]
                      )
                    , ( "returns Right [] if the value is the same as the minimum"
                      , [here| {"mint": {"type": "integer", "minimum": 10}} |]
                      , [here| {"mint": 10} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value is less than the minimum"
                      , [here| {"mint": {"type": "integer", "minimum": 10}} |]
                      , [here| {"mint": 9} |]
                      , Right ["value 9.0 falls below minimum (should be >=10.0)"]
                      )
                    , ( "returns Right [] if the value is the same as the minimum and exclusiveMinimum is true"
                      , [here| {"mint": {"type": "integer", "minimum": 10, "exclusiveMinimum": true}} |]
                      , [here| {"mint": 10} |]
                      , Right ["value 10.0 falls below minimum (should be >10.0)"]
                      )
                    , ( "returns Right [] if the value is the same as the minimum and exclusiveMinimum is false"
                      , [here| {"mint": {"type": "integer", "minimum": 10, "exclusiveMinimum": false}} |]
                      , [here| {"mint": 10} |]
                      , Right []
                      )
                    , ( "returns Right [] if the value is the same as the maximum"
                      , [here| {"mint": {"type": "integer", "maximum": 10}} |]
                      , [here| {"mint": 10} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value is greater than the maximum"
                      , [here| {"mint": {"type": "integer", "maximum": 10}} |]
                      , [here| {"mint": 11} |]
                      , Right ["value 11.0 exceeds maximum (should be <=10.0)"]
                      )
                    , ( "returns Right [] if the value is the same as the maximum and exclusiveMaximum is true"
                      , [here| {"mint": {"type": "integer", "maximum": 10, "exclusiveMaximum": true}} |]
                      , [here| {"mint": 11} |]
                      , Right ["value 11.0 exceeds maximum (should be <10.0)"]
                      )
                    , ( "returns Right [] if the value is the same as the maximum and exclusiveMaximum is false"
                      , [here| {"mint": {"type": "integer", "maximum": 10, "exclusiveMaximum": false}} |]
                      , [here| {"mint": 10} |]
                      , Right []
                      )
                    , ( "returns Right [] if the value is the multiple of the value of multipleOf"
                      , [here| {"mint": {"type": "integer", "multipleOf": 5}} |]
                      , [here| {"mint": 10} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value is not the multiple of the value of multipleOf"
                      , [here| {"mint": {"type": "integer", "multipleOf": 4}} |]
                      , [here| {"mint": 10} |]
                      , Right ["expected a multiple of 4.0 but got 10.0"]
                      )
                    , ( "returns Right [] if the value is the maximum value of int32"
                      , [here| {"mint": {"type": "integer", "format": "int32"}} |]
                      , [here| {"mint": 2147483647} |]
                      , Right []
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "type: number" $ do
            let
                tests =
                    [ ( "returns Right [] if the value is a number contains a decimal part"
                      , [here| {"mnum": {"type": "number"}} |]
                      , [here| {"mnum": 1.1} |]
                      , Right []
                      )
                    , ( "returns Right [] if the value is an integer"
                      , [here| {"mnum": {"type": "number"}} |]
                      , [here| {"mnum": 1} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value is a string type"
                      , [here| {"mnum": {"type": "number"}} |]
                      , [here| {"mnum": "text"} |]
                      , Right ["expected JSON value of type number"]
                      )
                    , ( "returns Right [error] if the value is a boolean type"
                      , [here| {"mnum": {"type": "number"}} |]
                      , [here| {"mnum": true} |]
                      , Right ["expected JSON value of type number"]
                      )
                    , ( "returns Right [error] if the value is an array type"
                      , [here| {"mnum": {"type": "number"}} |]
                      , [here| {"mnum": [0, 1, 2 ]} |]
                      , Right ["expected JSON value of type number"]
                      )
                    , ( "returns Right [error] if the value is an object type"
                      , [here| {"mnum": {"type": "number"}} |]
                      , [here| {"mnum": {"k": "v"}} |]
                      , Right ["expected JSON value of type number"]
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "type: string" $ do
            let
                tests =
                    [ ( "returns Right [] if the value is a string type"
                      , [here| {"mstr": {"type": "string"}} |]
                      , [here| {"mstr": "text"} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value is an integer type"
                      , [here| {"mstr": {"type": "string"}} |]
                      , [here| {"mstr": 1} |]
                      , Right ["expected JSON value of type string"]
                      )
                    , ( "returns Right [error] if the value is a number type"
                      , [here| {"mstr": {"type": "string"}} |]
                      , [here| {"mstr": 1.1} |]
                      , Right ["expected JSON value of type string"]
                      )
                    , ( "returns Right [error] if the value is a boolean type"
                      , [here| {"mstr": {"type": "string"}} |]
                      , [here| {"mstr": true} |]
                      , Right ["expected JSON value of type string"]
                      )
                    , ( "returns Right [error] if the value is an array type"
                      , [here| {"mstr": {"type": "string"}} |]
                      , [here| {"mstr": [0, 1, 2]} |]
                      , Right ["expected JSON value of type string"]
                      )
                    , ( "returns Right [error] if the value is an object type"
                      , [here| {"mstr": {"type": "string"}} |]
                      , [here| {"mstr": {"k": "v"}} |]
                      , Right ["expected JSON value of type string"]
                      )
                    , ( "returns Right [] if the length of the value is the same as minLength"
                      , [here| {"mstr": {"type": "string", "minLength": 3}} |]
                      , [here| { "mstr": "abc" } |]
                      , Right []
                      )
                    , ( "returns Right [error] if the length of the value is less than minLength"
                      , [here| {"mstr": {"type": "string", "minLength": 4}} |]
                      , [here| { "mstr": "abc" } |]
                      , Right ["string is too short (length should be >=4)"]
                      )
                    , ( "returns Right [] if the length of the value is the same as maxLength"
                      , [here| {"mstr": {"type": "string", "maxLength": 3}} |]
                      , [here| { "mstr": "abc" } |]
                      , Right []
                      )
                    , ( "returns Right [error] if the length of the value is greater than maxLength"
                      , [here| {"mstr": {"type": "string", "maxLength": 2}} |]
                      , [here| { "mstr": "abc" } |]
                      , Right ["string is too long (length should be <=2)"]
                      )
                    , ( "returns Right [] if the value follows format:date"
                      , [here| {"mstr": {"type": "string", "format": "date"}} |]
                      , [here| { "mstr": "2017-07-21" } |]
                      , Right []
                      )
                    , ( "returns Right [] if the value follows format:date-time"
                      , [here| {"mstr": {"type": "string", "format": "date-time"}} |]
                      , [here| { "mstr": "2017-07-21T17:32:28Z" } |]
                      , Right []
                      )
                    , ( "returns Right [] if the value follows format:byte"
                      , [here| {"mstr": {"type": "string", "format": "byte"}} |]
                      , [here| {"mstr": "U3dhZ2dlciByb2Nrcw=="} |]
                      , Right []
                      )
                    , ( "returns Right [] if the value matches the pattern"
                      , [here| {"mstr": {"type": "string", "pattern": "^abc$"}} |]
                      , [here| {"mstr": "abc"} |]
                      , Right []
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "type: string (pending)" $ do
            let
                tests =
                    [ ( "returns Right [] if the value does not follows format:date"
                      , [here| {"mstr": {"type": "string", "format": "date"}} |]
                      , [here| { "mstr": "not date!" } |]
                    --   , Right ["expected JSON value of type string and format of date"]
                      , "Keyword `format` is not supported"
                      )
                    , ( "returns Right [] if the value does not follows format:date-time"
                      ,  [here| {"mstr": {"type": "string", "format": "date-time"}} |]
                      ,  [here| {"mstr": "not datetime!"} |]
                    --   , Right ["expected JSON value of type string and format of date-time"]
                      , "Keyword `format` is not supported"
                      )
                    , ( "returns Right [] if the value does not follows format:byte"
                      , [here| {"mstr": {"type": "string", "format": "byte"}} |]
                      , [here| {"mstr": "not byte!"} |]
                    --   , Right ["expected JSON value of type string and format of byte"]
                      , "Keyword `format` is not supported"
                      )
                    , ( "returns Right [error] if the value does not matches the pattern"
                      , [here| {"mstr": {"type": "string", "pattern": "^abc$"}} |]
                      , [here| {"mstr": "def"} |]
                    --   , Right ["expected JSON value of type string and of pattern"]
                      , "Keyword `pattern` is not supported"
                      )
                    ] :: [(String, L.ByteString, L.ByteString, String)]
            -- forM_ tests $ \(description, schemaJson, bodyJson, result) ->
            --     it description $ do
            --         let apiJson = makeOpenApiJson schemaJson
            --         validateRequestBody POST "/examples" apiJson bodyJson `shouldNotBe` result
            forM_ tests $ \(description, _, _, reason) ->
                it description $
                    pendingWith reason

        context "type: boolean" $ do
            let
                tests =
                    [ ( "returns Right [] if the value is an boolean value"
                      , [here| {"mbol": {"type": "boolean"}} |]
                      , [here| {"mbol": true} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value is an integer type"
                      , [here| {"mbol": {"type": "boolean"}} |]
                      , [here| {"mbol": 1} |]
                      , Right ["expected JSON value of type boolean"]
                      )
                    , ( "returns Right [error] if the value is a number type"
                      , [here| {"mbol": {"type": "boolean"}} |]
                      , [here| {"mbol": 1.1} |]
                      , Right ["expected JSON value of type boolean"]
                      )
                    , ( "returns Right [error] if the value is a string type"
                      , [here| {"mbol": {"type": "boolean"}} |]
                      , [here| {"mbol": "text"} |]
                      , Right ["expected JSON value of type boolean"]
                      )
                    , ( "returns Right [error] if the value is an array type"
                      , [here| {"mbol": {"type": "boolean"}} |]
                      , [here| {"mbol": [0, 1, 2]} |]
                      , Right ["expected JSON value of type boolean"]
                      )
                    , ( "returns Right [error] if the value is an object type"
                      , [here| {"mbol": {"type": "boolean"}} |]
                      , [here| {"mbol": {"k": "v"}} |]
                      , Right ["expected JSON value of type boolean"]
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "type: array" $ do
            let
                tests =
                    [ ( "returns Right [] if all values in the list are the correct type"
                      , [here| {"mlst": {"type": "array", "items": {"type": "integer"}}} |]
                      , [here| {"mlst": [1, 2]} |]
                      , Right []
                      )
                    , ( "returns Right [error] if there are different types of values in the list"
                      , [here| {"mlst": {"type": "array", "items": {"type": "integer"}}} |]
                      , [here| {"mlst": [1, "2"]} |]
                      , Right ["expected JSON value of type integer"]
                      )
                    , ( "returns Right [] if all values in the tuple are the correct type"
                      , [here| {"mtpl": {"type": "array", "items": [{"type": "integer"}, {"type": "string"}]}} |]
                      , [here| {"mtpl": [1, "text"]} |]
                      , Right []
                      )
                    , ( "returns Right [error] if there are different types of values in the tuple"
                      , [here| {"mtpl": {"type": "array", "items": [{"type": "integer"}, {"type": "string"}]}} |]
                      , [here| {"mtpl": [1, 2]} |]
                      , Right ["expected JSON value of type string"]
                      )
                    , ( "returns Right [error] if the number of elements in the array is less than minItems"
                      , [here| {"mlst": {"type": "array", "items": {"type": "integer"}, "minItems": 2}} |]
                      , [here| {"mlst": [1]} |]
                      , Right ["array is too short (size should be >=2)"]
                      )
                    , ( "returns Right [error] if the number of elements in the array is greater than maxItems"
                      , [here| {"mlst": {"type": "array", "items": {"type": "integer"}, "maxItems": 2}} |]
                      , [here| {"mlst": [1, 2, 3]} |]
                      , Right ["array exceeds maximum size (should be <=2)"]
                      )
                    , ( "returns Right [] if uniqueItems is not specified and the array has some duplicated values"
                      , [here| {"mlst": {"type": "array", "items": {"type": "integer"}}} |]
                      , [here| {"mlst": [1, 2, 1]} |]
                      , Right []
                      )
                    , ( "returns Right [error] if uniqueItems is true and the array has some duplicated values"
                      , [here| {"mlst": {"type": "array", "items": {"type": "integer"}, "uniqueItems": true}} |]
                      , [here| {"mlst": [1, 2, 1]} |]
                      , Right ["array is expected to contain unique items, but it does not"]
                      )
                    , ( "returns Right [error] if uniqueItems is false and the array has some duplicated values"
                      , [here| {"mlst": {"type": "array", "items": {"type": "integer"}, "uniqueItems": false}} |]
                      , [here| {"mlst": [1, 2, 1]} |]
                      , Right []
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "type: object" $ do
            let
                tests =
                    [ ( "returns Right [error] if the value contains undefined property"
                      , [here|
{
    "mobj": {
        "type": "object",
        "properties": {
            "mint": {"type": "integer"}
        }
    }
}
|]
                      , [here| {"mobj": {"mstr": "hello"}} |]
                      , Right ["property \"mstr\" is found in JSON value, but it is not mentioned in Swagger schema"]
                      )
                    , ( "returns Right [] if the value contains required property"
                      , [here|
{
    "mobj": {
        "type": "object",
        "required": ["cint", "cstr"],
        "properties": {
            "cint": {"type": "integer"},
            "cstr": {"type": "string"},
            "mbol": {"type": "boolean"}
        }
    }
}
|]
                      , [here| {"mobj": {"cint": 1, "cstr": "hello"}} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value does not contains required property"
                      , [here|
{
    "mobj": {
        "type": "object",
        "required": ["cint", "cstr"],
        "properties": {
            "cint": {"type": "integer"},
            "cstr": {"type": "string"},
            "mbol": {"type": "boolean"}
        }
    }
}
|]
                      , [here| {"mobj": {"cint": 1, "mbol": true}} |]
                      , Right ["property \"cstr\" is required, but not found in \"{\\\"mbol\\\":true,\\\"cint\\\":1}\""]
                      )
                    , ( "returns Right [] if the number of properties of the value is greater than or equal to than minProperties"
                      , [here|
{
    "mobj": {
        "type": "object",
        "minProperties": 2,
        "properties": {
            "mint": {"type": "integer"},
            "mstr": {"type": "string"},
            "mbol": {"type": "boolean"}
        }
    }
}
|]
                      , [here| {"mobj": {"mint": 1, "mstr": "text"}} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the number of properties of the value is less than minProperties"
                      , [here|
{
    "mobj": {
        "type": "object",
        "minProperties": 2,
        "properties": {
            "mint": {"type": "integer"},
            "mstr": {"type": "string"},
            "mbol": {"type": "boolean"}
        }
    }
}
|]
                      , [here| {"mobj": {"mint": 1}} |]
                      , Right ["object size is too small (total number of properties should be >=2)"]
                      )
                    , ( "returns Right [] if the number of properties of the value is less than or equal to maxProperties"
                      , [here|
{
    "mobj": {
        "type": "object",
        "maxProperties": 2,
        "properties": {
            "mint": {"type": "integer"},
            "mstr": {"type": "string"},
            "mbol": {"type": "boolean"}
        }
    }
}
|]
                      , [here| {"mobj": {"mint": 1, "mstr": "text"}} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the number of properties of the value is greater than maxProperties"
                      , [here|
{
    "mobj": {
        "type": "object",
        "maxProperties": 2,
        "properties": {
            "mint": {"type": "integer"},
            "mstr": {"type": "string"},
            "mbol": {"type": "boolean"}
        }
    }
}
|]
                      , [here| {"mobj": {"mint": 1, "mstr": "text", "mbol": true}} |]
                      , Right ["object size exceeds maximum (total number of properties should be <=2)"]
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "enum" $ do
            let
                tests =
                    [ ( "returns Right [] if the value is one of the enumerated values"
                      , [here| {"mint": {"type": "integer", "enum": [10, 20, 30]}} |]
                      , [here| {"mint": 10} |]
                      , Right []
                      )
                    , ( "returns Right [error] if the value is not one of the enumerated values"
                      , [here| {"mint": {"type": "integer", "enum": [20, 30]}} |]
                      , [here| {"mint": 10} |]
                      , Right ["expected one of \"[20,30]\" but got Number 10.0"]
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "description" $ do
            let
                tests =
                    [ ( "returns Right [] if the description is defined"
                      , [here| {"mint": {"type": "integer", "description": "Can be any integer value"}} |]
                      , [here| {"mint": 1} |]
                      , Right []
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "nullable" $ do
            let
                tests =
                    [ ( "returns Right [] if nullable is true and the value is null"
                      , [here| {"mint": {"type": "integer", "nullable": true}} |]
                      , [here| {"mint": null} |]
                      , Right []
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateRequestBody POST "/examples" apiJson bodyJson `shouldBe` result

        context "nullable (pending)" $ do
            let
                tests =
                    [ ( "returns Right [error] if nullable is false and the value is null"
                      , [here| {"mint": {"type": "integer", "nullable": false}} |]
                      , [here| {"mint": null} |]
                    --   , Right ["expected JSON value of type not null"]
                      , "Keyword `nullable` is not supported"
                      )
                    , ( "returns Right [error] if nullable is not specified and the value is null"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| {"mint": null} |]
                    --   , Right ["expected JSON value of type not null"]
                      , "Keyword `nullable` is not supported"
                      )
                    ] :: [(String, L.ByteString, L.ByteString, String)]
            -- forM_ tests $ \(description, schemaJson, bodyJson, result) ->
            --     it description $ do
            --         let apiJson = makeOpenApiJson schemaJson
            --         validateRequestBody POST "/examples" apiJson bodyJson `shouldNotBe` result
            forM_ tests $ \(description, _, _, reason) ->
                it description $
                    pendingWith reason

    describe "validateResponseBody" $ do
        let makeOpenApiJson schemaJson = [here|
{
    "openapi": "3.0.0",
    "info": { "title": "info example", "version": "1.0.0" },
    "paths": {
        "/examples": {
            "get": {
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Example"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "Example": {
                "type": "object",
                "properties": |] <> schemaJson <> [here|
            }
        }
    }
}
|]
        context "type: integer" $ do
            let
                tests =
                    [ ( "returns Right [] if the type of the value is valid"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| [{"mint": 1}, {"mint": 2}] |]
                      , Right []
                      )
                    , ( "returns Right [] if the type of the value is invalid"
                      , [here| {"mint": {"type": "integer"}} |]
                      , [here| [{"mint": 1}, {"mint": "two"}] |]
                      , Right ["expected JSON value of type integer"]
                      )
                    ]
            forM_ tests $ \(description, schemaJson, bodyJson, result) ->
                it description $ do
                    let apiJson = makeOpenApiJson schemaJson
                    validateResponseBody GET "/examples" 200 apiJson bodyJson `shouldBe` result