packages feed

wai-route-0.2: 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    #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

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

import Control.Arrow (first)
import Control.Applicative ((<$>))
import Control.Monad.State.Strict
import Data.ByteString (ByteString)
import Data.List (nub)
import Data.Monoid ((<>))
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.ByteString.Char8 as C
import qualified Data.ByteString.Lazy  as L

tests :: TestTree
tests = testProperty "Routing" checkRouting

checkRouting :: Property
checkRouting = forAll genRoutes check
  where
    check routes =
        let h1  = route $ map (fmap unHandler) routes
            rsv = routes >>= C.split '/' . fst
            res = const $ return ResponseReceived
        in conjoin . flip map routes $ \(r, TestHandler h2) ->
            forAll (genReq r rsv) $ \(params2, rq) ->
                let result1 = h1 rq res    -- routed
                    result2 = h2 [] rq res -- direct
                    (hId1, params1) = execState result1 (-1, [])
                    (hId2,       _) = execState result2 (-1, [])
                in hId1 == hId2 && params1 == params2

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

newtype TestHandler = TestHandler
    { unHandler :: Handler (State (Int, [(ByteString, ByteString)])) }

instance Show TestHandler where
    show _ = "<test-handler>"

handler :: Int -> TestHandler
handler i = TestHandler $ \p _ k -> do
    put (i, p)
    k $ responseLBS status200 [] L.empty

genDir :: Gen ByteString
genDir = C.pack <$> listOf1 arbitrary `suchThat` f
  where
    f d = head d /= ':' && '/' `notElem` d

genCapture :: Gen ByteString
genCapture =  (":"<>) . C.pack <$> listOf1 arbitrary `suchThat` notElem '/'

genRoute :: Gen ByteString
genRoute = do
    n <- choose (1, 10)
    s <- vectorOf n (oneof [genDir, genCapture])
    return $ C.intercalate "/" s

-- Generates random routing tables without ambiguous routes.
-- Two routes are ambiguous if they have an equal number of segments and
-- equal static segments appear in the same positions.
genRoutes :: Gen [(ByteString, TestHandler)]
genRoutes = do
    n <- choose (0, 10)
    r <- vectorOf n genRoute `suchThat` noAmbiguity
    return $ r `zip` map handler [0..n]
  where
    noAmbiguity rs = let rs' = map normalize rs
                     in length (nub rs') == length rs'
    normalize = filter ((/=':') . C.head . snd)
              . zip ([0..] :: [Int])
              . C.split '/'

-- Generate a request with a path matching the given route.
genReq :: ByteString   -- ^ Route
       -> [ByteString] -- ^ Reserved names
       -> Gen ([(ByteString, ByteString)], Request)
genReq r reserved = do
    values <- vectorOf (length segs) genDir `suchThat` all (`notElem` reserved)
    let zipped = segs `zip` values
        params = reverse . map (first C.tail) . filter ((==':') . C.head . fst) $ zipped
        rq = defaultRequest { rawPathInfo = C.intercalate "/"  $ map toSeg zipped }
    return (params, rq)
  where
    segs = C.split '/' r
    toSeg (s, v) | C.head s == ':' = urlEncode False v
                 | otherwise       = urlEncode False s

instance Show Request where
    show = show . rawPathInfo