haskoin-store-1.9.0: test/Haskoin/Store/Web/RouterSpec.hs
module Haskoin.Store.Web.RouterSpec (spec) where
import Control.Exception (try)
import Data.ByteString.Builder (toLazyByteString)
import Data.ByteString.Lazy.Char8 qualified as L8
import Data.IORef (modifyIORef', newIORef, readIORef, writeIORef)
import Data.Maybe (fromMaybe)
import Data.Text qualified as T
import Haskoin.Store.Web.Router
import Network.HTTP.Types (methodGet, parseQuery, status200, status404, statusCode)
import Network.Wai
import Network.Wai.Internal (Response (..), ResponseReceived (..))
import Test.Hspec
spec :: Spec
spec = describe "router" $ do
it "answers with the route whose path matches, with its captures" $
call "/block/abc" `shouldReturn` (200, "block abc")
it "gives the parameters that the route accepts to its handler" $
call "/block/abc?notx=true" `shouldReturn` (200, "block abc notx=true")
it "refuses a parameter that the route does not accept, without running it" $
call "/block/abc?limt=5" `shouldReturn` (404, "unknown limt")
it "names every unknown parameter once" $
call "/block/abc?a=1&b=2&a=3" `shouldReturn` (404, "unknown a,b")
it "tries the next route when a handler passes, keeping its parameters" $
call "/block/best?notx=false" `shouldReturn` (200, "best notx=false")
it "falls back when no route has the path" $
call "/nothing" `shouldReturn` (404, "no route")
it "fails when a handler reads a parameter that its route does not declare" $ do
r <- try (call "/sloppy")
either (\(Undeclared n) -> n) (const "") r `shouldBe` "secret"
it "answers inside the scope of a request, and makes one for each route that it tries" $ do
events <- newIORef []
let note e = modifyIORef' events (<> [e])
scoped answer = note "open" >> answer routes <* note "close"
req = defaultRequest {requestMethod = methodGet, pathInfo = ["block", "best"]}
_ <- routerScoped routes scoped (\_ _ -> fail "no route") req $ \_ ->
note "respond" >> return ResponseReceived
-- the first route passes the request on, the second answers it
readIORef events `shouldReturn` ["open", "close", "open", "respond", "close" :: String]
-- | Routes for the tests: the first matches any block but passes on
-- "best", for the second to answer.
routes :: [Route]
routes =
[ get "/block/:block" ["notx"] $ \req -> do
b <- capture req "block"
if b == "best" then next else notx req >>= \n -> ok (T.unwords (["block", b] <> n)),
get "/block/best" ["notx"] $ \req -> notx req >>= \n -> ok (T.unwords ("best" : n)),
get "/sloppy" [] $ \req -> param req "secret" >>= ok . fromMaybe ""
]
where
notx req = maybe [] (\v -> ["notx=" <> v]) <$> param req "notx"
ok t = return (responseLBS status200 [] (L8.pack (T.unpack t)))
-- | Status and body of the answer to a GET of a path with a query string.
call :: String -> IO (Int, String)
call url = do
let (path, query) = break (== '?') url
req =
defaultRequest
{ requestMethod = methodGet,
pathInfo = filter (not . T.null) (T.splitOn "/" (T.pack path)),
queryString = parseQuery (L8.toStrict (L8.pack query))
}
out <- newIORef Nothing
_ <- router routes miss req $ \res -> writeIORef out (Just res) >> return ResponseReceived
readIORef out >>= \case
Just (ResponseBuilder s _ b) -> return (statusCode s, L8.unpack (toLazyByteString b))
_ -> fail "no response"
where
miss _ NoRoute = return (responseLBS status404 [] "no route")
miss _ (Unknown ks) =
return (responseLBS status404 [] (L8.pack ("unknown " <> T.unpack (T.intercalate "," ks))))