packages feed

wai-csrf-0.1: test/Main.hs

{-# LANGUAGE RecordWildCards #-}

module Main (main) where

import Control.Exception qualified as Ex
import Control.Monad
import Control.Monad.IO.Class
import Data.ByteString qualified as B
import Data.Maybe
import Data.String
import Network.HTTP.Types qualified as HT
import Network.Wai qualified as W
import Network.Wai.Test qualified as WT
import Web.Cookie qualified as WC

import Wai.CSRF qualified as WCC

main :: IO ()
main = do
   testToken
   testCookies
   testReject
   putStrLn "TESTS OK"

testReject :: IO ()
testReject = do
   tok1 <- WCC.randomToken
   let sc1 = WCC.setCookie WCC.defaultConfig tok1
       app = WCC.middleware WCC.defaultConfig \yt _ respond ->
         respond $
            W.responseLBS HT.status200 [] $
               fromString (show (yt == Just tok1))

   tok2 <- WCC.randomToken
   let sc2 = WCC.setCookie WCC.defaultConfig tok2

   WT.withSession app do
      -- Make sure the cookie is set
      WT.assertNoClientCookieExists "t0-a" "CSRF-TOKEN"
      WT.setClientCookie sc1

      -- Request succeeds because it is GET
      sres1 <- WT.request WT.defaultRequest
      WT.assertStatus 200 sres1
      WT.assertBody "True" sres1
      WT.assertClientCookieExists "t0-b" "CSRF-TOKEN"

      -- Request fails because it is POST and there is no CSRF header
      sres2 <- WT.request WT.defaultRequest{W.requestMethod = HT.methodPost}
      WT.assertStatus 403 sres2
      WT.assertBody "CSRF" sres2

      -- Request succeeds because it is POST and there is matching CSRF header
      sres3 <-
         WT.request
            WT.defaultRequest
               { W.requestMethod = HT.methodPost
               , W.requestHeaders = [("X-CSRF-TOKEN", WC.setCookieValue sc1)]
               }
      WT.assertStatus 200 sres3
      WT.assertBody "True" sres3

      -- Request succeeds because it is GET. The request header doesn't match,
      -- but it doesn't matter.
      sres4 <- do
         WT.request
            WT.defaultRequest
               { W.requestHeaders = [("X-CSRF-TOKEN", WC.setCookieValue sc2)]
               }
      WT.assertStatus 200 sres4
      WT.assertBody "True" sres4

      -- Request fails because it is POST, but there is no matching request header
      sres5 <- do
         WT.request
            WT.defaultRequest
               { W.requestMethod = HT.methodPost
               , W.requestHeaders = [("X-CSRF-TOKEN", WC.setCookieValue sc2)]
               }
      WT.assertStatus 403 sres5
      WT.assertBody "CSRF" sres5

testToken :: IO ()
testToken = do
   t0 <- WCC.randomToken
   t1 <- WCC.randomToken
   when (t0 == t1) $ fail "e0"

   mt0 <- WCC.randomMaskToken t0
   mt1 <- WCC.randomMaskToken t0
   when (mt0 == mt1) $ fail "e1"

   when (WCC.unmaskToken mt0 /= t0) $ fail "e3"

   let mt064 = WCC.maskedTokenToBase64UU mt0
   when (WCC.maskedTokenFromBase64UU mt064 /= Just mt0) $ fail "e4"

testCookies :: IO ()
testCookies = do
   let c = WCC.defaultConfig

   let fapp1 :: (Maybe WCC.Token -> Maybe (Maybe WCC.Token)) -> W.Application
       fapp1 = \g -> WCC.middleware c (app1 c g)

   -- keeping cookies untouched
   WT.withSession (fapp1 \_ -> Nothing) do
      WT.assertNoClientCookieExists "t0-a" "CSRF-TOKEN"
      sres1 <- WT.request WT.defaultRequest
      WT.assertBody "Nothing" sres1
      WT.assertNoClientCookieExists "t0-b" "CSRF-TOKEN"
      sres1 <- WT.request WT.defaultRequest
      WT.assertBody "Nothing" sres1
      WT.assertNoClientCookieExists "t0-c" "CSRF-TOKEN"

   -- explicitly deleting cookie
   WT.withSession (fapp1 \_ -> Just Nothing) do
      WT.assertNoClientCookieExists "t1-a" "CSRF-TOKEN"
      sres1 <- WT.request WT.defaultRequest
      WT.assertBody "Nothing" sres1
      WT.assertClientCookieExists "t1-b" "CSRF-TOKEN"
      sres1 <- WT.request WT.defaultRequest
      WT.assertBody "Nothing" sres1
      WT.assertClientCookieExists "t1-c" "CSRF-TOKEN"

   -- explicitely setting cookie
   tok1 <- WCC.randomToken
   ck0 <- WT.withSession (fapp1 \_ -> Just (Just tok1)) do
      WT.assertNoClientCookieExists "t2-a" "CSRF-TOKEN"
      sres1 <- WT.request WT.defaultRequest
      WT.assertBody "Nothing" sres1
      WT.assertClientCookieExists "t2-b" "CSRF-TOKEN"
      sres2 <- WT.request WT.defaultRequest
      WT.assertBody (fromString (show (Just tok1))) sres2
      WT.assertClientCookieExists "t2-c" "CSRF-TOKEN"
      WT.getClientCookies

   -- modify and explicitly delete
   WT.withSession (fapp1 \_ -> Just Nothing) do
      WT.assertNoClientCookieExists "t3-a" "CSRF-TOKEN"
      WT.modifyClientCookies \_ -> ck0
      WT.assertClientCookieExists "t3-b" "CSRF-TOKEN"
      sres1 <- WT.request WT.defaultRequest
      WT.assertBody (fromString (show (Just tok1))) sres1
      WT.assertClientCookieExists "t3-c" "CSRF-TOKEN"
      sres2 <- WT.request WT.defaultRequest
      WT.assertBody "Nothing" sres2
      WT.assertClientCookieExists "t3-d" "CSRF-TOKEN"
      WT.assertClientCookieValue "t3-e" "CSRF-TOKEN" ""

app1
   :: WCC.Config
   -> (Maybe WCC.Token -> Maybe (Maybe WCC.Token))
   -> Maybe WCC.Token
   -> W.Application
app1 c g yold = \req respond -> do
   let ysc :: Maybe WC.SetCookie =
         case g yold of
            Nothing -> Nothing
            Just Nothing -> Just $ WCC.expireCookie c
            Just (Just new) -> Just $ WCC.setCookie c new
   respond
      $ W.responseLBS
         HT.status200
         ( fmap
            (\sc -> ("Set-Cookie", WC.renderSetCookieBS sc))
            (maybeToList ysc)
         )
      $ fromString (show yold)