packages feed

shikumi-0.1.0.0: test/SchemaSpec.hs

{-# LANGUAGE DataKinds #-}

-- | M2 (schema derivation) + M3 (total decode and the failure taxonomy) for
-- "Shikumi.Schema". Schema equality is compared as an aeson 'Value' (key order
-- does not matter); every failing input asserts the exact located 'ShikumiError'.
module SchemaSpec (tests) where

import Data.Aeson (Value, object, (.=))
import Data.Text (Text)
import Fixtures (Author (..), Sentiment (..), Summary (..))
import Shikumi.Error (ShikumiError (..))
import Shikumi.Schema (deriveSchema, parseOutput)
import Shikumi.Schema.Types (field)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))

-- | The schema we expect @deriveSchema \@Summary@ to produce.
expectedSummarySchema :: Value
expectedSummarySchema =
  object
    [ "type" .= s "object",
      "properties"
        .= object
          [ "headline" .= object ["type" .= s "string", "description" .= s "A one-line summary"],
            "bullets"
              .= object
                [ "type" .= s "array",
                  "items" .= object ["type" .= s "string"],
                  "description" .= s "Three to five key points"
                ],
            "author"
              .= object
                [ "type" .= s "object",
                  "properties"
                    .= object
                      ["name" .= object ["type" .= s "string", "description" .= s "Author full name"]],
                  "required" .= ["name" :: Text],
                  "additionalProperties" .= False
                ],
            "sentiment" .= object ["enum" .= (["Positive", "Neutral", "Negative"] :: [Text])],
            "note" .= object ["anyOf" .= [object ["type" .= s "string"], object ["type" .= s "null"]]]
          ],
      "required" .= (["headline", "bullets", "author", "sentiment"] :: [Text]),
      "additionalProperties" .= False
    ]
  where
    s = id :: Text -> Text

-- | A valid provider reply.
goodBody :: Text
goodBody =
  "{\"headline\":\"H\",\"bullets\":[\"a\",\"b\",\"c\"],\
  \\"author\":{\"name\":\"Ada\"},\"sentiment\":\"Positive\",\"note\":null}"

decode :: Text -> Either ShikumiError Summary
decode = parseOutput

tests :: TestTree
tests =
  testGroup
    "SchemaSpec"
    [ testCase "deriveSchema @Summary matches the expected JSON Schema" $
        deriveSchema @Summary @?= expectedSummarySchema,
      testCase "good provider JSON decodes to the expected Summary" $
        decode goodBody
          @?= Right
            Summary
              { headline = field "H",
                bullets = field ["a", "b", "c"],
                author = Author {name = field "Ada"},
                sentiment = Positive,
                note = Nothing
              },
      testCase "missing required field -> MissingField (located)" $
        decode "{\"bullets\":[\"a\",\"b\",\"c\"],\"author\":{\"name\":\"Ada\"},\"sentiment\":\"Positive\"}"
          @?= Left (MissingField "headline"),
      testCase "wrong JSON type -> SchemaMismatch (located)" $
        decode "{\"headline\":\"H\",\"bullets\":\"oops\",\"author\":{\"name\":\"Ada\"},\"sentiment\":\"Positive\"}"
          @?= Left (SchemaMismatch "bullets: expected array, got string"),
      testCase "bad enum value -> SchemaMismatch listing the choices" $
        decode "{\"headline\":\"H\",\"bullets\":[\"a\",\"b\",\"c\"],\"author\":{\"name\":\"Ada\"},\"sentiment\":\"Angry\"}"
          @?= Left (SchemaMismatch "sentiment: expected one of [Positive, Neutral, Negative]"),
      testCase "validation rule violation -> ValidationFailure" $
        decode "{\"headline\":\"H\",\"bullets\":[\"a\"],\"author\":{\"name\":\"Ada\"},\"sentiment\":\"Positive\"}"
          @?= Left (ValidationFailure "bullets: must have 3 to 5 items"),
      testCase "non-JSON body -> InvalidJSON" $
        case decode "not json" of
          Left (InvalidJSON _) -> pure ()
          other -> assertFailure ("expected Left (InvalidJSON ...), got " <> show other)
    ]