packages feed

yesod-core-1.7.0.0: test/YesodCoreTest/ParameterizedSubDispatchRuntime.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- | Runtime coverage for the backwards-compatible (opt-out) inline
-- nested-route codegen.
--
-- The modules "YesodCoreTest.ParameterizedSubData" /
-- "YesodCoreTest.ParameterizedSubDispatch" only exercise the parameterized
-- /opt-out/ path at *compile time* (data + dispatch instances, no runtime
-- assertions). This module gives the same shape a concrete instantiation and
-- actually exercises it at runtime:
--
--   * 'parseRoute' . 'renderRoute' round-trips for every constructor, and
--   * WAI dispatch returns the right status + body for each path,
--
-- covering the static, dynamic, and (dynamically-keyed) nested-parent inline
-- arms — the path-piece ordering, parent-constructor nesting and @dyns@
-- handling that previously had no runtime guard.
module YesodCoreTest.ParameterizedSubDispatchRuntime
    ( specs
    ) where

import Data.Text (Text)
import Test.Hspec
import qualified Data.ByteString.Lazy as L
import qualified Network.HTTP.Types as H
import Yesod.Core
import Yesod.Core.Dispatch (toWaiApp)

import YesodCoreTest.ParameterizedSubData
import YesodCoreTest.ParameterizedSubDispatch ()
import YesodCoreTest.ParameterizedSubDispatchRuntime.Data
import YesodCoreTest.RuntimeHarness (assertRequestFor)

-- | A concrete subsite for 'ParamSubsite'. Its associated 'AssocType' is
-- 'Text', so the dynamically-keyed nested parent (@!/#{AssocType subsite}@)
-- becomes a plain @Text@ path piece at runtime.
data ConcreteSub = ConcreteSub

instance ParamSubsiteClass ConcreteSub App where
    type AssocType ConcreteSub = Text
    getSubsiteValue _ _ = "concrete"

-- | parseRoutes can only embed a single-token subsite type, so we alias the
-- fully-applied subsites here.
type ConcreteParamSub = ParamSubsite ConcreteSub
type ConcreteBigSub = BigSub ()

data App = App

mkYesod "App" [parseRoutes|
/ HomeR GET
/sub ParamSubR ConcreteParamSub getSub
/big BigSubR ConcreteBigSub getBig
|]

instance Yesod App where
    messageLoggerSource = mempty

getHomeR :: Handler Text
getHomeR = pure "home"

getSub :: App -> ConcreteParamSub
getSub _ = ParamSubsite ConcreteSub

getBig :: App -> ConcreteBigSub
getBig _ = BigSub ()

-- Dispatch + handlers for the BigSub / ChildSub subsites.
instance YesodSubDispatch (BigSub a) master where
    yesodSubDispatch = $(mkYesodSubDispatch resourcesBigSub)

instance YesodSubDispatch ChildSub master where
    yesodSubDispatch = $(mkYesodSubDispatch resourcesChildSub)

getBigItemR :: Int -> SubHandlerFor (BigSub a) master Text
getBigItemR _ = pure "bigItem"

getBigMultiR :: [Text] -> SubHandlerFor (BigSub a) master Text
getBigMultiR _ = pure "bigMulti"

getWrapHomeR :: Int -> SubHandlerFor (BigSub a) master Text
getWrapHomeR _ = pure "wrapHome"

getWrapDetailR :: Int -> Int -> SubHandlerFor (BigSub a) master Text
getWrapDetailR _ _ = pure "wrapDetail"

getChildHomeR :: SubHandlerFor ChildSub master Text
getChildHomeR = pure "childHome"

getChildXR :: Int -> SubHandlerFor ChildSub master Text
getChildXR _ = pure "childX"

-- | Every constructor of the parameterized subsite's route, instantiated at
-- the concrete subsite.
sampleRoutes :: [Route ConcreteParamSub]
sampleRoutes =
    [ ParamSubHomeR
    , ParamSubItemR 7
    , NestedR "abc" NestedHomeR
    , NestedR "abc" (NestedDetailR 9)
    ]

-- | Routes for 'BigSub', covering a multipiece leaf and a subsite leaf nested
-- under a 'ResourceParent'.
bigSampleRoutes :: [Route ConcreteBigSub]
bigSampleRoutes =
    [ BigItemR 3
    , BigMultiR ["a", "b", "c"]
    , BigMultiR []
    , WrapR 5 WrapHomeR
    , WrapR 5 (WrapDetailR 9)
    , EmbedParentR (ChildR ChildHomeR)
    , EmbedParentR (ChildR (ChildXR 9))
    ]

testRequestIO
    :: HasCallStack
    => Int               -- ^ expected http status code
    -> [Text]            -- ^ pathInfo
    -> H.Method          -- ^ request method
    -> Maybe L.ByteString -- ^ expected body (Nothing = don't check)
    -> IO ()
testRequestIO status path method mexpected =
    assertRequestFor App method status path mexpected

specs :: Spec
specs = describe "opt-out (backwards-compat) parameterized nested routes" $ do
    describe "parseRoute . renderRoute round-trips" $ do
        mapM_
            (\r -> it (show r) $
                parseRoute (renderRoute r) `shouldBe` Just r)
            sampleRoutes
        mapM_
            (\r -> it (show r) $
                parseRoute (renderRoute r) `shouldBe` Just r)
            bigSampleRoutes

    describe "WAI dispatch (subsite mounted at /sub)" $ do
        it "static leaf (ParamSubHomeR)" $
            testRequestIO 200 ["sub"] "GET" (Just "paramSubHome")
        it "dynamic leaf (ParamSubItemR)" $
            testRequestIO 200 ["sub", "item", "7"] "GET" (Just "paramSubItem")
        it "nested parent home (NestedR _ NestedHomeR)" $
            testRequestIO 200 ["sub", "abc", "nested"] "GET" (Just "nestedHome")
        it "nested parent dynamic leaf (NestedR _ (NestedDetailR _))" $
            testRequestIO 200 ["sub", "abc", "nested", "detail", "9"] "GET" (Just "nestedDetail")
        it "404 on unknown nested suffix" $
            testRequestIO 404 ["sub", "abc", "nested", "oops"] "GET" Nothing
        it "405 on wrong method for nested leaf" $
            testRequestIO 405 ["sub", "abc", "nested"] "POST" Nothing
        it "404 on malformed dynamic leaf piece (#Int given non-integer)" $
            testRequestIO 404 ["sub", "item", "notanint"] "GET" Nothing
        it "404 on malformed dynamic nested-leaf piece (NestedDetailR #Int)" $
            testRequestIO 404 ["sub", "abc", "nested", "detail", "notanint"] "GET" Nothing

    describe "WAI dispatch (BigSub mounted at /big)" $ do
        it "dynamic leaf (BigItemR)" $
            testRequestIO 200 ["big", "item", "3"] "GET" (Just "bigItem")
        it "multipiece leaf (BigMultiR)" $
            testRequestIO 200 ["big", "multi", "a", "b", "c"] "GET" (Just "bigMulti")
        it "multipiece leaf, empty tail (BigMultiR [])" $
            testRequestIO 200 ["big", "multi"] "GET" (Just "bigMulti")
        it "nested parent home (WrapR _ WrapHomeR)" $
            testRequestIO 200 ["big", "wrap", "5"] "GET" (Just "wrapHome")
        it "nested parent dynamic leaf (WrapR _ (WrapDetailR _))" $
            testRequestIO 200 ["big", "wrap", "5", "detail", "9"] "GET" (Just "wrapDetail")
        it "subsite leaf nested under parent, home (EmbedParentR (ChildR ChildHomeR))" $
            testRequestIO 200 ["big", "embed", "sub"] "GET" (Just "childHome")
        it "subsite leaf nested under parent, dynamic (EmbedParentR (ChildR (ChildXR _)))" $
            testRequestIO 200 ["big", "embed", "sub", "x", "9"] "GET" (Just "childX")
        it "404 on malformed dynamic parent key (WrapR #Int given non-integer)" $
            testRequestIO 404 ["big", "wrap", "notanint"] "GET" Nothing