packages feed

hyperbole-0.7.0: test/Test/EncodedSpec.hs

{-# LANGUAGE OverloadedLists #-}

module Test.EncodedSpec where

import Data.Aeson (FromJSON (..), ToJSON (..), Value (..))
import Data.Text (Text)
import GHC.Generics (Generic)
import Skeletest
import Web.Hyperbole.Data.Argument
import Web.Hyperbole.Data.Encoded


-- TEST: QueryData underscores vs spaces

data One = One
  -- toJSON automatically delegates to the child's ToJSON instance
  -- when it ought to be enought to delegate to the Generic instance!
  deriving (Generic, Eq, ToEncoded, FromEncoded, ToJSON, FromJSON)


data Tag = A | B | C | D
  deriving (Generic, Eq, ToEncoded, ToJSON, FromJSON)


data Two = Two | Two2 Int
  deriving (Generic, Eq, ToJSON, FromJSON, ToEncoded, FromEncoded)


-- -- Custom Param Encoding
-- instance ToParam Two where
--   toParam Two = "Two"
--   toParam other = genericToParam other
-- instance FromParam Two where
--   parseParam "Two" = pure Two
--   parseParam other = genericParseParam other

data Sum
  = Sumthing
  | Num Int
  | Str Text
  | COne One
  | CTwo Two
  | List [Text]
  deriving (Generic, Eq, ToEncoded, FromEncoded)


data Nested
  = Gogo One
  | RecordN Record
  | RecordM RecordMaybe
  | RecordEx Record Int
  | Tag Tag
  | NProd SubProduct
  deriving (Generic, ToEncoded, FromEncoded, Eq)


data SubProduct
  = MoreThanOne Int
  | AnotherOne Text Int
  deriving (Generic, Eq, ToJSON, FromJSON)


data Product
  = Product Text Int Bool
  deriving (Generic, Eq, ToEncoded, FromEncoded)


data Record = Record
  { one :: Int
  , two :: Text
  }
  deriving (Generic, Show, ToJSON, FromJSON, Eq, ToEncoded, FromEncoded)


data RecordMaybe = RecordMaybe
  { one :: Int
  , two :: Maybe Text
  }
  deriving (Generic, Show, ToJSON, FromJSON, Eq, ToEncoded, FromEncoded)


data Product4 = Product4 Text Text Text Text deriving (Generic, Show, Eq, Read, FromEncoded, ToEncoded)


spec :: Spec
spec = withMarkers ["encoded"] $ do
  describe "genericToEncoded" $ do
    it "should encode single tags" $ do
      genericToEncoded One `shouldBe` Encoded "One" []

    it "should encode multi tags" $ do
      genericToEncoded Two `shouldBe` Encoded "Two" []
      genericToEncoded (Two2 3) `shouldBe` Encoded "Two2" [JSON $ Number 3]
      genericToEncoded (Gogo One) `shouldBe` Encoded "Gogo" [toArgument One]

    it "should encode sum tags" $ do
      genericToEncoded (CTwo Two) `shouldBe` Encoded "CTwo" [toArgument Two]

    it "basic" $ do
      genericToEncoded (Gogo One) `shouldBe` Encoded "Gogo" [toArgument One]

    it "product" $ do
      genericToEncoded (Product "one" 2 True) `shouldBe` Encoded "Product" [toArgument @Text "one", toArgument @Int 2, toArgument True]

    it "product4" $ do
      let prod = Product4 "one" "two" "three" "four"
      genericToEncoded prod `shouldBe` Encoded "Product4" (fmap toArgument ["one" :: Text, "two", "three", "four"])

  describe "genericParseEncoded" $ do
    it "product4" $ do
      genericParseEncoded (Encoded "Product4" (fmap toArgument ["one" :: Text, "two", "three", "four"])) `shouldBe` Right (Product4 "one" "two" "three" "four")

    it "sum" $ do
      genericParseEncoded @Sum (Encoded "Sumthing" []) `shouldBe` Right Sumthing
      genericParseEncoded @Sum (Encoded "Num" [toArgument @Int 2]) `shouldBe` Right (Num 2)
      genericParseEncoded @Sum (Encoded "Str" [toArgument @Text "OK"]) `shouldBe` Right (Str "OK")

      genericParseEncoded @Sum (Encoded "COne" [toArgument One]) `shouldBe` Right (COne One)
      genericParseEncoded @Sum (Encoded "CTwo" [toArgument Two]) `shouldBe` Right (CTwo Two)

  describe "toEncoded" $ do
    it "encodes numbers as text" $ do
      toEncoded (Num 1) `shouldBe` Encoded "Num" [toArgument @Int 1]

  describe "toText" $ do
    it "should encode single tags" $ do
      encodedToText (Encoded "One" []) `shouldBe` "One"

  describe "parseText" $ do
    it "should decode single tags" $ do
      encodedParseText "One" `shouldBe` Right (Encoded "One" [])

    it "parses numbers" $ do
      encodedParseText "Num 1" `shouldBe` Right (Encoded "Num" [JSON $ Number 1])

  describe "encode" $ do
    it "should encode single tags" $ do
      encode One `shouldBe` "One"

    it "encodes strings as JSON" $ do
      encode (Str "hello world") `shouldBe` "Str \"hello world\""
      encode (Str " ") `shouldBe` "Str \" \""
      encode (Str "") `shouldBe` "Str \"\""
      encode (Str "_") `shouldBe` "Str \"_\""
      encode (Str "\n") `shouldBe` "Str \"\\n\""
      encode (Str "hello_world") `shouldBe` "Str \"hello_world\""
      encode (Str "hello+world") `shouldBe` "Str \"hello+world\""
      encode (Str "hello\nworld") `shouldBe` "Str \"hello\\nworld\""

    it "should encode records`" $ do
      -- no field names for ourselves
      encode (Record 1 "two") `shouldBe` "Record 1 \"two\""
      -- but if it is nested it uses the JSON instance, obviously
      let r2 = Record 1 "two"
      encode (RecordN r2) `shouldBe` "RecordN " <> encodeFromArgument (JSON $ toJSON r2)

    it "no special case for nested constructors`" $ do
      encode A `shouldBe` "A"
      encode (Tag A) `shouldBe` "Tag A"

    it "should encode sum" $ do
      encode (Num 1) `shouldBe` "Num 1"
      encode (Str "hello world") `shouldBe` "Str \"hello world\""

    it "should encode products" $ do
      encode (Product "hello world" 2 True) `shouldBe` "Product \"hello world\" 2 true"

    it "encodes more constructors" $ do
      encode (CTwo (Two2 3)) `shouldBe` "CTwo (Two2 3)" -- uses custom tuple constructor
      encode (CTwo Two) `shouldBe` "CTwo (Two)" -- uses the custom simpleTag
      encode (COne One) `shouldBe` "COne []"

    it "encodes nested products" $ do
      encode (NProd (AnotherOne "hi" 3)) `shouldBe` "NProd (AnotherOne \"hi\" 3)"

    it "encodes nested records" $ do
      encode (RecordN (Record 3 "hello")) `shouldBe` "RecordN {\"one\":3,\"two\":\"hello\"}"

    it "encodes nested records with nothing as null" $ do
      encode (RecordM (RecordMaybe 3 Nothing)) `shouldBe` "RecordM {\"one\":3,\"two\":null}"

  describe "decode" $ do
    it "should encode single tags" $ do
      decode "One" `shouldBe` Just One

    it "should decode nested sum" $ do
      decodeEither "Num 1" `shouldBe` Right (Num 1)
      decodeEither "Str \"str\"" `shouldBe` Right (Str "str")
      decodeEither "Str \"hello world\"" `shouldBe` Right (Str "hello world")

    it "no special case for nested constructors`" $ do
      decodeEither "Tag A" `shouldBe` Right (Tag A)

    it "decodes strings" $ do
      decode "Str \"\"" `shouldBe` pure (Str "")

  describe "params" $ do
    -- it "sanitizeText" $ do
    --   encodeParam "hello world" `shouldBe` "hello_world"
    --   encodeParam "hello_world" `shouldBe` "hello\\_world"
    --   encodeParam "hello\nworld" `shouldBe` "hello\\nworld"
    --
    -- it "desanitizeText" $ do
    --   decodeParam "hello_world" `shouldBe` "hello world"
    --   decodeParam "hello\\_world" `shouldBe` "hello_world"
    --   decodeParam "hello\\nworld" `shouldBe` "hello\nworld"

    -- TODO: Add more edge cases to check if "\n" is escaped properly.
    -- it "edge cases" $ do
    --   encodeParam "" `shouldBe` "\"\""
    --   encodeParam " " `shouldBe` "\"_\""
    --   encodeParam "  " `shouldBe` "\"__\""
    --
    --   encodeParam "_" `shouldBe` "\"\\_\""
    --   encodeParam "__" `shouldBe` "\"\\_\\_\""
    --
    --   decodeParam "|" `shouldBe` ParamValue "|"
    --   decodeParam "_" `shouldBe` ParamValue "_"
    --   decodeParam "\\_" `shouldBe` "_"
    --   decodeParam "\\_\\_" `shouldBe` "__"

    describe "round trip" $ do
      it "records" $ do
        let enc = genericToEncoded (Record 1 "two")
        genericParseEncoded enc `shouldBe` Right (Record 1 "two")

      it "product" $ do
        decode (encode (Product "hello world" 2 False)) `shouldBe` Just (Product "hello world" 2 False)
        decode (encode (Product "bob" (-2) True)) `shouldBe` Just (Product "bob" (-2) True)

      it "nested product with records" $ do
        let r = RecordEx (Record 2 "three") 33
        let t = encode r
        decode t `shouldBe` Just r

      it "special case constructors" $ do
        decodeEither (encode (CTwo Two)) `shouldBe` Right (CTwo Two)
        decode (encode (Tag B)) `shouldBe` Just (Tag B)

      it "special case products" $ do
        decodeEither (encode (CTwo (Two2 3))) `shouldBe` Right (CTwo (Two2 3))
        decodeEither (encode (NProd (AnotherOne "HI" 3))) `shouldBe` Right (NProd (AnotherOne "HI" 3))

      it "big product" $ do
        let p = Product4 "hello world" "two_times" "three" "four"
        decode (encode p) `shouldBe` Just p

      it "empty strings" $ do
        decode (encode $ Str "") `shouldBe` Just (Str "")

      it "special characters" $ do
        let str = "hello+world \"bob_lives\""
        decode (encode $ Str str) `shouldBe` Just (Str str)

      it "encodes lists`" $ do
        let l = List ["hello, world", "", "+,|<[]"]
        decode @Sum (encode l) `shouldBe` Just l

      it "encodes newlines in strings" $ do
        let s = Str "hello\nworld"
        decode @Sum (encode s) `shouldBe` Just s

      -- Regression tests for https://github.com/seanhess/hyperbole/issues/187
      -- A ViewId (or state) containing a list with newline characters must
      -- encode/decode correctly.  Previously, desanitizeParamText blindly
      -- replaced the JSON escape sequence "\\n" with a real newline, corrupting
      -- the JSON and causing "No Handler for Event viewId".
      it "list with newline character round-trips correctly (issue #187)" $ do
        decode @Sum (encode (List ["\n"])) `shouldBe` Just (List ["\n"])

      it "list with newline in multiple elements" $ do
        decode @Sum (encode (List ["\n", "hello\nworld", "plain"])) `shouldBe` Just (List ["\n", "hello\nworld", "plain"])

      it "strings" $ do
        decode @Sum (encode (Str "")) `shouldBe` pure (Str "")
        decode @Sum (encode (Str " ")) `shouldBe` pure (Str " ")
        decode @Sum (encode (Str "_")) `shouldBe` pure (Str "_")
        decode @Sum (encode (Str "~")) `shouldBe` pure (Str "~")
        decode @Sum (encode (Str "+")) `shouldBe` pure (Str "+")
        decode @Sum (encode (Str "hello world")) `shouldBe` pure (Str "hello world")
        decode @Sum (encode (Str "hello_world")) `shouldBe` pure (Str "hello_world")