@@ -5,6 +5,12 @@ module Network.Wai.Test
55 ( -- * Session
66 Session
77 , runSession
8+ -- * Client Cookies
9+ , ClientCookies
10+ , getClientCookies
11+ , modifyClientCookies
12+ , setClientCookie
13+ , deleteClientCookie
814 -- * Requests
915 , request
1016 , srequest
@@ -20,13 +26,18 @@ module Network.Wai.Test
2026 , assertBodyContains
2127 , assertHeader
2228 , assertNoHeader
29+ , assertClientCookieExists
30+ , assertNoClientCookieExists
31+ , assertClientCookieValue
2332 , WaiTestFailure (..)
2433 ) where
2534
2635import Network.Wai
2736import Network.Wai.Internal (ResponseReceived (ResponseReceived))
37+import Control.Applicative ((<$>))
2838import Control.Monad.IO.Class (liftIO)
29−import Control.Monad.Trans.State (StateT, evalStateT)
39+import Control.Monad.Trans.Class (lift)
40+import qualified Control.Monad.Trans.State as ST
3041import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask)
3142import Control.Monad (unless)
3243import Control.DeepSeq (deepseq)
@@ -34,9 +45,10 @@ import Control.Exception (throwIO, Exception)
3445import Data.Typeable (Typeable)
3546import Data.Map (Map)
3647import qualified Data.Map as Map
48+import qualified Web.Cookie as Cookie
3749import Data.ByteString (ByteString)
3850import qualified Data.ByteString.Char8 as S8
39−import Blaze.ByteString.Builder (toLazyByteString)
51+import Blaze.ByteString.Builder (toLazyByteString, toByteString)
4052import qualified Blaze.ByteString.Builder as B
4153import qualified Data.ByteString.Lazy as L
4254import qualified Data.ByteString.Lazy.Char8 as L8
@@ -47,18 +59,39 @@ import qualified Data.Text as T
4759import qualified Data.Text.Encoding as TE
4860import Data.IORef
4961import Data.Monoid (mempty, mappend)
62+import Data.Time.Clock (getCurrentTime)
5063
51−type Session = ReaderT Application (StateT ClientState IO)
64+type Session = ReaderT Application (ST.StateT ClientState IO)
65+
66+type ClientCookies = Map ByteString Cookie.SetCookie
5267
5368data ClientState = ClientState
54− { _clientCookies :: Map ByteString ByteString
69+ { clientCookies :: ClientCookies
5570 }
5671
72+getClientCookies :: Session ClientCookies
73+getClientCookies = clientCookies <$> lift ST.get
74+
75+modifyClientCookies :: (ClientCookies -> ClientCookies) -> Session ()
76+modifyClientCookies f =
77+ lift (ST.modify (\cs -> cs { clientCookies = f $ clientCookies cs }))
78+
79+setClientCookie :: Cookie.SetCookie -> Session ()
80+setClientCookie c =
81+ modifyClientCookies
82+ (Map.union
83+ (Map.singleton (Cookie.setCookieName c) c))
84+
85+deleteClientCookie :: ByteString -> Session ()
86+deleteClientCookie cookieName =
87+ modifyClientCookies
88+ (Map.delete cookieName)
89+
5790initState :: ClientState
5891initState = ClientState Map.empty
5992
6093runSession :: Session a -> Application -> IO a
61−runSession session app = evalStateT (runReaderT session app) initState
94+runSession session app = ST.evalStateT (runReaderT session app) initState
6295
6396data SRequest = SRequest
6497 { simpleRequest :: Request
@@ -93,6 +126,40 @@ setRawPathInfo r rawPinfo =
93126 dropFrontSlash ("":path) = path
94127 dropFrontSlash path = path
95128
129+addCookiesToRequest :: Request -> Session Request
130+addCookiesToRequest req = do
131+ oldClientCookies <- getClientCookies
132+ let requestPath = "/" `T.append` T.intercalate "/" (pathInfo req)
133+ currentUTCTime <- liftIO getCurrentTime
134+ let cookiesForRequest =
135+ Map.filter
136+ (\c -> checkCookieTime currentUTCTime c
137+ && checkCookiePath requestPath c)
138+ oldClientCookies
139+ let cookiePairs = [ (Cookie.setCookieName c, Cookie.setCookieValue c)
140+ | c <- map snd $ Map.toList cookiesForRequest
141+ ]
142+ let cookieValue = toByteString $ Cookie.renderCookies cookiePairs
143+ return $ req { requestHeaders = ("Cookie", cookieValue):requestHeaders req }
144+ where checkCookieTime t c =
145+ case Cookie.setCookieExpires c of
146+ Nothing -> True
147+ Just t' -> t < t'
148+ checkCookiePath p c =
149+ case Cookie.setCookiePath c of
150+ Nothing -> True
151+ Just p' -> p' `S8.isPrefixOf` TE.encodeUtf8 p
152+
153+extractSetCookieFromSResponse :: SResponse -> Session SResponse
154+extractSetCookieFromSResponse response = do
155+ let setCookieHeaders =
156+ filter (("Set-Cookie"==) . fst) $ simpleHeaders response
157+ let newClientCookies = map (Cookie.parseSetCookie . snd) setCookieHeaders
158+ modifyClientCookies
159+ (Map.union
160+ (Map.fromList [(Cookie.setCookieName c, c) | c <- newClientCookies ]))
161+ return response
162+
96163srequest :: SRequest -> Session SResponse
97164srequest (SRequest req bod) = do
98165 app <- ask
@@ -103,12 +170,12 @@ srequest (SRequest req bod) = do
103170 [] -> ([], S.empty)
104171 x:y -> (y, x)
105172 }
106− liftIO $ do
173+ req'' <- addCookiesToRequest req'
174+ response <- liftIO $ do
107175 ref <- newIORef $ error "runResponse gave no result"
108− ResponseReceived <- app req' (runResponse ref)
176+ ResponseReceived <- app req'' (runResponse ref)
109177 readIORef ref
110− -- FIXME cookie processing
111− --return sres
178+ extractSetCookieFromSResponse response
112179
113180runResponse :: IORef SResponse -> Response -> IO ResponseReceived
114181runResponse ref res = do
@@ -212,3 +279,33 @@ assertNoHeader header SResponse{simpleHeaders = h} =
212279 , " containing "
213280 , show s
214281 ]
282+
283+assertClientCookieExists :: String -> ByteString -> Session ()
284+assertClientCookieExists s cookieName = do
285+ cookies <- getClientCookies
286+ assertBool s $ Map.member cookieName cookies
287+
288+assertNoClientCookieExists :: String -> ByteString -> Session ()
289+assertNoClientCookieExists s cookieName = do
290+ cookies <- getClientCookies
291+ assertBool s $ not $ Map.member cookieName cookies
292+
293+assertClientCookieValue :: String -> ByteString -> ByteString -> Session ()
294+assertClientCookieValue s cookieName cookieValue = do
295+ cookies <- getClientCookies
296+ case Map.lookup cookieName cookies of
297+ Nothing ->
298+ assertFailure (s ++ " (cookie does not exist)")
299+ Just c ->
300+ assertBool
301+ (concat
302+ [ s
303+ , " (actual value "
304+ , show $ Cookie.setCookieValue c
305+ , " expected value "
306+ , show cookieValue
307+ , ")"
308+ ]
309+ )
310+ (Cookie.setCookieValue c == cookieValue)
311+