packages feed

wai-route-1.0.0: test/Test/Network/Wai/Route.hs

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.

{-# LANGUAGE
    OverloadedStrings
  , TypeApplications
#-}

module Test.Network.Wai.Route (tests) where

import Control.DeepSeq
import Control.Monad.State.Strict
import Data.Foldable (toList)
import Data.Sequence (Seq (..), (<|))
import Data.Semigroup ((<>))
import Data.Text (Text)
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Internal (ResponseReceived (..))
import Network.Wai.Route
import Test.Tasty
import Test.Tasty.QuickCheck

import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.Trie.Pattern as Trie

tests :: TestTree
tests = testGroup "Network.Wai.Route"
    [ testProperty "route" checkRouting
    , testProperty "parsePath (1/2)" checkParsePath1
    , testProperty "parsePath (2/2)" checkParsePath2
    , testProperty "compileRoutes" checkCompileRoutes
    , testProperty "path equivalence (=~=)" checkPathEquiv
    ]

checkRouting :: Property
checkRouting = forAll genTestRoutes check
  where
    check rs =
        let f = route $ Trie.fromAssocList (map unTestRoute rs)
            k = const $ return ResponseReceived
        in conjoin . flip map rs $ \(TestRoute (p, _)) ->
            forAll (genRequest p) $ \(params, rq) ->
                let s = execState (f noRoute rq k) (Seq.empty, Seq.empty)
                in s == (p, params)

    noRoute _rq k = k $ responseLBS status404 [] mempty

checkParsePath1 :: Property
checkParsePath1 = forAll (genPath Nothing) $ \(SomePath p) ->
    let captures = Seq.replicate (pathVarsLen p) joker
    in case parsePath p captures of
        Left  e -> counterexample (show e) False
        Right _ -> property True

checkParsePath2 :: Property
checkParsePath2 = forAll ((,) <$> genText <*> arbitrary) $ \(t, i) ->
    let
        p0 = str t ./ end
        p1 = str t ./ some @Int ./ end
        c  = Seq.singleton (Capture (Text.pack (show i)))
        c' = Seq.singleton (Capture "NaN")
    in
        parsePath p0 c         == Right Nil &&
        parsePath p0 Seq.empty == Right Nil &&
        parsePath p1 c         == Right (i ::: Nil) &&
        parsePath p1 Seq.empty == Left PathMissingParams &&
        case parsePath p1 c' of
            Left (PathInvalidParam e) -> invalidParamValue e == "NaN"
            _                         -> False

-- | Compile a list of (non-overlapping) routes into a routing
-- trie and check that all routes have been preserved by running
-- all handlers.
checkCompileRoutes :: Property
checkCompileRoutes = forAll genRoutes $ \rs ->
    let maxLen = maximum (map (Seq.length . routePattern) rs)
        captures = Seq.replicate maxLen joker
        rt = force $ run captures <$> compileRoutes rs
    in  Set.fromList (map Trie.matchOrd (toList rt))
        ==
        Set.fromList (map (Trie.matchOrd . routePattern) rs)
  where
    routePattern Route{routePath = p} = pathPattern p

    run captures h =
        let k = const $ return ResponseReceived
            s = h captures defaultRequest k
        in execState s Seq.empty

checkPathEquiv :: Property
checkPathEquiv = forAll ((,) <$> genPath Nothing <*> genPath Nothing) $
    \(SomePath p, SomePath p') ->
        let pp  = pathPattern p
            pp' = pathPattern p'
        in  p =~= p'  ==> pp == pp'
            &&
            pp == pp' ==> p =~= p'

-------------------------------------------------------------------------------
-- Generators & helpers

type TestState = State (Pattern Text, Seq (Capture Text))

-- | An (untyped) test route.
newtype TestRoute = TestRoute
    { unTestRoute :: (Pattern Text, Handler TestState) }

instance Show TestRoute where
    show (TestRoute (p, _)) = "<test-route; " ++ show p ++ ">"

-- Generate a matcher for a pattern. All 'EqStr' matchers are prefixed
-- with a @0@ character, to avoid accidental clashes with generated
-- capture values (see 'genRequest').
genMatcher :: Gen (Matcher Text)
genMatcher = oneof [genStr, genVar]
  where
    genStr = EqStr . ("0" <>) <$> genText
    genVar = pure AnyStr

genPattern :: Gen (Pattern Text)
genPattern = do
    n <- choose (1, 10)
    Seq.fromList <$> vectorOf n genMatcher

genPath :: Maybe Text -> Gen SomePath
genPath prefix = oneof
    [ return (SomePath (maybe id str prefix $ end))
    , do SomePath p <- genPath prefix
         SomePath <$> (str <$> genText <*> pure p)
    , do SomePath p <- genPath prefix
         b <- elements [True,False]
         return $ if b
            then SomePath (some @Text p)
            else SomePath (some @Int  p)
    ]

-- | A capture value that is valid for all types in a path
-- generated by 'genPath'.
joker :: Capture Text
joker = Capture "42"

genTestRoute :: Gen TestRoute
genTestRoute = do
    p <- genPattern
    return $ TestRoute (p, \params _ k -> do
        put (p, params)
        k $ responseLBS status200 [] mempty)

genTestRoutes :: Gen [TestRoute]
genTestRoutes = do
    n <- choose (0, 100)
    replicateM n genTestRoute

genRoute :: Maybe Text -> Gen (Route (State (Pattern Text)))
genRoute prefix = do
    SomePath p <- genPath prefix
    return $ defRoute p $ \_params _rq k -> do
        put (pathPattern p)
        k $ responseLBS status200 [] mempty

-- | Generate 1-100 non-overlapping routes.
genRoutes :: Gen [Route (State (Pattern Text))]
genRoutes = do
    n <- choose (0 :: Int, 100)
    mapM (genRoute . Just . Text.pack . show) [1..n]

-- Generate a request with a path matching the given pattern,
-- generating capture values for capture segments. Returns the
-- generated captures and request.
genRequest :: Pattern Text -> Gen (Seq (Capture Text), Request)
genRequest pat = do
    -- Pair each segment of the route with a potential parameter value.
    -- Those values which are paired with a capture are the actual parameters.
    values <- vectorOf (Seq.length pat) genValue
    let segs = toList pat `zip` values
    return (mkParams segs, mkReq segs)
  where
    mkReq segs = defaultRequest
          { pathInfo = map mkSeg segs
          }

    mkSeg (AnyStr, val) = val
    mkSeg (EqStr s,  _) = s

    mkParams = foldr go Seq.empty
      where
        go (AnyStr, val) ps = Capture val <| ps
        go (EqStr _,  _) ps = ps

-- Generate a value for a capture. All values are prefixed with
-- a '1' character, to avoid accidentally generating a value that is
-- an exact match for an 'EqStr' matcher in another pattern. If that
-- were to happen for all matchers of a path (unlikely but possible),
-- it would result in a spurious test failure.
genValue :: Gen Text
genValue = ("1" <>) <$> genText

genText :: Gen Text
genText = Text.pack <$> listOf arbitrary