{-# LANGUAGE RankNTypes,OverloadedStrings,DeriveDataTypeable #-} {-# OPTIONS_GHC -F -pgmF htfpp #-} module Web.MangoPay.TestUtils where import Web.MangoPay import Data.ByteString.Lazy as BS hiding (map,any,null) import Network.HTTP.Conduit as H import Data.Maybe import Test.Framework import Network.Wai as W import Network.Wai.Handler.Warp import Network.HTTP.Types (status200) import Blaze.ByteString.Builder (copyByteString) import Data.Aeson as A import Control.Concurrent (forkIO, ThreadId, threadDelay) import Control.Concurrent.MVar (MVar, newMVar, putMVar, takeMVar) import Control.Monad (when, void, liftM) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Resource (ResourceT, runResourceT) import Data.Text (Text) import Data.List import Data.Typeable import Control.Applicative import Test.HUnit (Assertion) import Data.Default (def) import Data.IORef import System.IO.Unsafe (unsafePerformIO) import Control.Monad.Logger -- | a test card testCardInfo1 :: CardInfo testCardInfo1 = CardInfo "4970100000000154" "1220" "123" -- | test MangoPay API call, logging in with the client credentials -- expects a file called client.test.conf containing the JSON of client credentials -- in the current directory testMP :: forall b. (AccessToken -> MangoPayT (LoggingT (ResourceT IO)) b) -> IO b testMP f= do ior<-readIORef testState let mgr=tsManager ior let at=tsAccessToken ior let cred=tsCredentials ior runResourceT $ runStdoutLoggingT $ runMangoPayT cred mgr Sandbox $ f at -- | the test state as a top level global variable -- -- since HTF does not let us thread a parameter through tests testState :: IORef TestState {-# NOINLINE testState #-} testState = unsafePerformIO (newIORef $ TestState zero zero zero zero zero) where zero=error "testState has not been initialized yet" -- | the test state we keep over all tests data TestState=TestState { tsAccessToken :: AccessToken -- ^ the access token if we're logged in ,tsCredentials :: Credentials -- ^ the credentials ,tsManager :: H.Manager -- ^ the http manager ,tsHookEndPoint :: HookEndPoint -- ^ the end point for notifications ,tsReceivedEvents :: ReceivedEvents -- ^ the received events } -- | read end point information from hook.test.conf in current folder getHookEndPoint :: IO HookEndPoint getHookEndPoint = do js<-BS.readFile "hook.test.conf" let mhook=decode js assertBool (isJust mhook) return $ fromJust mhook -- | simple configuration to tell the tests what endpoint to use for notifications data HookEndPoint = HookEndPoint{ hepUrl :: Text ,hepPort :: Int } deriving (Show,Read,Eq,Ord,Typeable) -- | to json instance ToJSON HookEndPoint where toJSON h=object ["Url" .= hepUrl h,"Port" .= hepPort h] -- | from json instance FromJSON HookEndPoint where parseJSON (Object v) =HookEndPoint <$> v .: "Url" <*> v .: "Port" parseJSON _=fail "HookEndPoint" -- | the events received via the notification hook -- uses a MVar for storing events data ReceivedEvents=ReceivedEvents{ events::MVar [Either EventResult Event] } -- | creates the new ReceivedEvents newReceivedEvents :: IO ReceivedEvents newReceivedEvents=do mv<-newMVar [] return $ ReceivedEvents mv -- | test an event, checking the resource id and the event type testEvent :: Maybe Text -> EventType -> Event -> Bool testEvent tid et evt= tid == (Just $ eResourceId evt) && et == eEventType evt -- | check that we're receiving events of the given type with the resource id returned by the passed test testEventTypes :: [EventType] -> IO (Maybe Text) -> Assertion testEventTypes evtTs =void . testEventTypes' evtTs -- | check that we're receiving events of the given type with the resource id returned by the passed test -- returns the result of the inner test testEventTypes' :: [EventType] -> IO (Maybe Text) -> IO (Maybe Text) testEventTypes' evtTs ops=do res<-liftM tsReceivedEvents $ readIORef testState a<-ops mapM_ (testSearchEvent a) evtTs er<-waitForEvent res (map (testEvent a) evtTs) 5 assertEqual EventsOK er return a -- | assert that we find an event for the given resource id and event type testSearchEvent :: Maybe Text -> EventType -> Assertion testSearchEvent tid evtT=do es<-testMP $ searchEvents (def{espEventType=Just evtT}) assertBool (not $ null es) assertBool (any ((tid ==) . Just . eResourceId) es) -- | create a hook for a given event type createHook :: EventType -> Assertion createHook evtT= do hook<-liftM tsHookEndPoint $ readIORef testState h<-testMP $ storeHook (Hook Nothing Nothing Nothing (hepUrl hook) Enabled Nothing evtT) assertBool (isJust $ hId h) h2<-testMP $ fetchHook (fromJust $ hId h) assertEqual (hId h) (hId h2) assertEqual (Just Valid) (hValidity h) -- | run a test with the notification server running testEvents :: IO a -- ^ the test, returning a value -> [a -> Event -> Bool] -- ^ the test on the events, taking into account the returned value -> Assertion testEvents ops tests=do res<-liftM tsReceivedEvents $ readIORef testState a<-ops er<-waitForEvent res (map ($ a) tests) 30 assertEqual EventsOK er -- | result of waiting for event data EventResult = Timeout -- ^ didn't receive all expected events | EventsOK -- ^ OK: everything expected received, nothing unexpected | ExtraEvent Event -- ^ unexpected | UnhandledNotification String -- ^ notification we couldn't parse deriving (Show,Eq,Ord,Typeable) -- | wait till we receive all the expected events, and none other, for a maximum number of seconds waitForEvent :: ReceivedEvents -> [Event -> Bool] -- ^ function on the expected event -> Integer -- ^ delay in seconds -> IO EventResult waitForEvent _ _ del | del<=0=return Timeout waitForEvent rc fs del=do mevt<-popReceivedEvent rc case (mevt,fs) of (Nothing,[])->return EventsOK -- nothing left to process (Just (Left er),_)->return er -- some notification we didn't understand (Just (Right evt),[])->return $ ExtraEvent evt -- an event that doesn't match (Nothing,_)->do -- no event yet threadDelay 1000000 waitForEvent rc fs (del-1) (Just (Right evt),_)-> -- an event, does it match case Data.List.foldl' (match1 evt) ([],False) fs of (_,False)->return $ ExtraEvent evt -- doesn't match (fs2,_)-> waitForEvent rc fs2 del -- matched, either we have more to do or we need to check no unexpected event was found where -- | match the first event function and return all the non matching function, and a flag indicating if we matched match1 :: Event -> ([Event -> Bool],Bool) -> (Event -> Bool) -> ([Event -> Bool],Bool) match1 evt (nfs,False) f | f evt=(nfs,True) | otherwise=(f:nfs,False) match1 _ (nfs,True) f=(f:nfs,True) -- | get one received event (and remove it from the underlying storage) popReceivedEvent :: ReceivedEvents -> IO (Maybe (Either EventResult Event)) popReceivedEvent (ReceivedEvents mv)=do evts<-takeMVar mv case evts of []->do putMVar mv [] return Nothing (e:es)->do putMVar mv es return $ Just e -- | get all received events (and remove them from the underlying storage) popReceivedEvents :: ReceivedEvents -> IO [Either EventResult Event] popReceivedEvents (ReceivedEvents mv)=do evts<-takeMVar mv putMVar mv [] return evts -- | add a new event pushReceivedEvent :: ReceivedEvents -> Either EventResult Event -> IO () pushReceivedEvent (ReceivedEvents mv) evt=do evts' <-takeMVar mv -- we're getting events in duplicate ??? let ns = if evt `Prelude.elem` evts' then evts' else evt:evts' putMVar mv ns -- | start a HTTP server listening on the given port -- if the path info is "mphook", then we'll push the received event startHTTPServer :: Port -> ReceivedEvents -> IO ThreadId startHTTPServer p revts= forkIO $ run p app where app req = do when (pathInfo req == ["mphook"]) $ do let mevt=eventFromQueryString $ W.queryString req liftIO $ case mevt of Just evt->do pushReceivedEvent revts $ Right evt print evt Nothing->pushReceivedEvent revts $ Left $ UnhandledNotification $ show $ W.queryString req return $ W.responseBuilder status200 [("Content-Type", "text/plain")] $ copyByteString "noop"