packages feed

servant-py-0.1.0.1: test/Servant/PY/InternalSpec.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies        #-}
{-# LANGUAGE TypeOperators       #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Servant.PY.InternalSpec where

import           Control.Lens
import           Data.Aeson
import qualified Data.ByteString.Char8                      as B
import           Data.Monoid
import           Data.Proxy
import           Data.Text                                  (Text)
import qualified Data.Text                                  as T
import           GHC.Generics
import           Prelude                                    ()
import           Prelude.Compat
import           Test.Hspec                                 hiding
                                                             (shouldContain,
                                                             shouldNotContain)
import           Test.QuickCheck                            (Arbitrary (..),
                                                             choose, listOf,
                                                             property)

import           Servant.API.ContentTypes
import           Servant.API.Internal.Test.ComprehensiveAPI
import           Servant.Foreign

import           Servant.PY.Internal


data SomeJson = SomeJson
 { uvalue       :: !T.Text
 , pvalue       :: !T.Text
 , otherMissing :: Maybe T.Text
 } deriving (Eq, Show, Generic)
instance ToJSON SomeJson

-- * Our API type
type TestApi = "counter-req-header" :> Post '[JSON] SomeJson
          :<|> "counter-queryparam"
            :> QueryParam "sortby" T.Text
            :> Header "Some-Header" T.Text :> Get '[JSON] SomeJson
          :<|> "login-queryflag" :> QueryFlag "published" :> Get '[JSON] SomeJson
          :<|> "login-params-authors-with-reqBody"
            :> QueryParams "authors" T.Text
            :> ReqBody '[JSON] SomeJson :> Post '[JSON] SomeJson

testApi :: Proxy TestApi
testApi = Proxy


type CaptureApi = "login-with-path-var-and-header"
                  :> Capture "id" Int
                  :> Capture "Name" T.Text
                  :> Capture "hungrig" Bool
                  :> ReqBody '[JSON] SomeJson
                  :> Post '[JSON] (Headers '[Header "test-head" B.ByteString] SomeJson)
captureApi :: Proxy CaptureApi
captureApi = Proxy

customOptions :: CommonGeneratorOptions
customOptions = defCommonGeneratorOptions
 { urlPrefix = "urlForRequesting:9000"
 , returnMode = RawResponse
 }

spec :: Spec
spec = describe "Servant.PY.Internal" internalSpec

shouldContain :: Text -> Text -> Expectation
a `shouldContain` b  = shouldSatisfy a (T.isInfixOf b)

shouldNotContain :: Text -> Text -> Expectation
a `shouldNotContain` b  = shouldNotSatisfy a (T.isInfixOf b)

newtype ASCII = ASCII {getASCII :: T.Text} deriving (Show)

instance Arbitrary ASCII where
   -- Our arbitrary instance is generating only ASCII, since the language-ecmascript's lexer
   -- is currently (October 2016) still a bit naïve
   arbitrary = fmap (ASCII . T.pack) $ listOf $ choose (minBound, '\127')
   shrink xs = (ASCII . T.pack) <$> shrink (T.unpack $ getASCII xs)


internalSpec :: Spec
internalSpec = describe "Internal" $ do
  describe "pure text functions" $ do
    it "should only indent using whitespace" $
      property $ \n -> indenter n indent == mconcat (replicate n (T.pack " "))

    it "should generate a valid python identifier when supplied with hyphens, unicode whitespace, non-bmp unicode" $
      toValidFunctionName "a_--a\66352b\6158c\65075" `shouldBe` "a_abc\65075"

    it "should produce PyDicts where the key is a quoted version of the variable name" $ do
      let dict = toPyDict "  " ["forty", "one", "people"]
      dict `shouldBe` "{\"forty\": forty,\n  \"one\": one,\n  \"people\": people}"

  describe "functions that operate on Req objects" $ do
    let captureList = listFromAPI (Proxy :: Proxy NoTypes) (Proxy :: Proxy NoContent) captureApi
    it "should correctly find captures" $ do
      let captured = captures . head $ captureList
      captured `shouldBe` ["id", "Name", "hungrig"]

    let reqList = listFromAPI (Proxy :: Proxy NoTypes) (Proxy :: Proxy NoContent) testApi
    it "should not incorrectly find captures" $ do
      let captured = captures . head $ reqList
      captured `shouldBe` []

    let req = head captureList
    let pathParts = req ^.. reqUrl.path.traverse
    it "should correctly find captures as a list" $
      capturesToFormatArgs pathParts `shouldBe` ["id", "Name", "hungrig"]

    it "should correctly format captures" $
      withFormattedCaptures "  " pathParts `shouldBe` ".format(\n  id=parse.quote(str(id)),\n"
                                                    <> "  Name=parse.quote(str(Name)),\n  "
                                                    <> "hungrig=parse.quote(str(hungrig)))"

    it "should build a formatted val with parse.quote and str" $
      property $ \s -> T.isInfixOf "=parse.quote(str(" $ formatBuilder $ T.pack s
    it "should build a formatted val that ends with parens" $
      property $ \s -> T.isSuffixOf (T.pack s <> "))") $ formatBuilder $ T.pack s

    it "should build urls properly with / separator" $ do
      let pyUrl = makePyUrl customOptions req " "
      pyUrl `shouldBe` "\"urlForRequesting:9000/login-with-path-var-and-header/{id}/{Name}/{hungrig}\""
                       <> withFormattedCaptures " " pathParts

    it "should do segment-to-str as a plain string for Static" $
      segmentToStr (head pathParts) == "login-with-path-var-and-header"
    it "should do segment-to-str in formatting braces for a capture" $
      segmentToStr (last pathParts) == "{hungrig}"
    it "should build a doctstring that looks like a regular Python docstring" $ do
      let docstring = buildDocString req customOptions
      docstring `shouldContain` "POST"
      docstring `shouldContain` makePyUrl' pathParts
      docstring `shouldContain` "Args:"
      docstring `shouldContain` "Returns:"