mattermost-api-50200.14.0: test/Tests/Util.hs
module Tests.Util
( mmTestCase
, print_
, getConnection
, getSession
, getTeams
, createChannel
, deleteChannel
, joinChannel
, leaveChannel
-- , getMoreChannels
, getChannels
, getChannelMembers
, getUserByName
, getConfig
, getClientConfig
, saveConfig
, teamAddUser
, reportJSONExceptions
, adminAccount
, createAdminAccount
, loginAccount
, loginAdminAccount
, createAccount
, createTeam
, findChannel
, connectFromConfig
, getMe
-- * Testing Websocket Events
, expectWSEvent
, expectWSDone
-- * Websocket Event Predicates
, hasWSEventType
, forUser
, forChannel
, isStatusChange
, isPost
, isNewUserEvent
, isChannelCreatedEvent
, isChannelDeleteEvent
, isAddedToTeam
, isUserJoin
, isUserLeave
, isViewedChannel
, wsHas
, (&&&)
)
where
import qualified Control.Exception as E
import qualified Control.Concurrent.STM as STM
import Control.Concurrent (forkIO)
import Control.Concurrent.MVar
import Control.Monad (join, when)
import Control.Monad.IO.Class (MonadIO(..))
import qualified Data.Foldable as F
import Data.Functor (void)
import Data.Monoid ((<>))
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import Test.Tasty (TestTree)
import Test.Tasty.HUnit (testCaseSteps)
import Control.Monad.State.Lazy (evalStateT, gets, modify)
import System.Timeout (timeout)
import Network.Mattermost (ConnectionData)
import Network.Mattermost.Endpoints
import Network.Mattermost.Types
import Network.Mattermost.Types.Config
import Network.Mattermost.WebSocket
import Network.Mattermost.Exceptions
import Network.Mattermost.Util
import Tests.Types
mmTestCase :: String -> TestConfig -> TestM () -> TestTree
mmTestCase testName cfg act =
testCaseSteps testName $ \prnt -> do
cd <- connectFromConfig cfg
wsChan <- STM.atomically STM.newTChan
mv <- newEmptyMVar
let initState = TestState { tsPrinter = prnt
, tsConfig = cfg
, tsConnectionData = cd
, tsSession = Nothing
, tsDebug = False
, tsWebsocketChan = wsChan
, tsDone = mv
}
(reportJSONExceptions $ evalStateT act initState) `E.finally`
(putMVar mv ())
print_ :: String -> TestM ()
print_ s = do
dbg <- gets tsDebug
printFunc <- gets tsPrinter
when dbg $ liftIO $ printFunc s
-- This only exists because tasty will call `show` on the exception that
-- we give it. If we directly output the exception first then we avoid
-- an unnecessary level of quotation in the output. We still throw the
-- exception though so that tasty reports the correct exception type.
-- This results in some redundancy but we only see it when there are
-- failures, so it seems acceptable.
reportJSONExceptions :: IO a -> IO a
reportJSONExceptions io = io
`E.catch` \e@(JSONDecodeException msg badJson) -> do
putStrLn $ "\nException: JSONDecodeException: " ++ msg
putStrLn badJson
E.throwIO e
adminAccount :: TestConfig -> UsersCreate
adminAccount cfg =
UsersCreate { usersCreateEmail = configEmail cfg
, usersCreatePassword = configPassword cfg
, usersCreateUsername = configUsername cfg
, usersCreateAllowMarketing = True
}
createAdminAccount :: TestM User
createAdminAccount = do
cd <- getConnection
cfg <- gets tsConfig
u <- liftIO $ mmInitialUser cd $ adminAccount cfg
print_ "Admin Account created"
return u
loginAccount :: Login -> TestM ()
loginAccount login = do
cd <- getConnection
(session, _mmUser) <- liftIO $ join (hoistE <$> mmLogin cd login)
print_ $ "Authenticated as " ++ T.unpack (username login)
chan <- gets tsWebsocketChan
doneMVar <- gets tsDone
printFunc <- gets tsPrinter
void $ liftIO $ forkIO $ mmWithWebSocket session
(either printFunc (either (\_ -> pure()) (STM.atomically . STM.writeTChan chan)))
(const $ takeMVar doneMVar)
modify $ \ts -> ts { tsSession = Just session }
hasWSEventType :: WebsocketEventType -> WebsocketEvent -> Bool
hasWSEventType = wsHas weEvent
wsHas :: (Eq a) => (WebsocketEvent -> a) -> a -> WebsocketEvent -> Bool
wsHas f expected e = f e == expected
(&&&) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
(&&&) f g a = f a && g a
-- | Expect the websocket event channel to contain an event that matches
-- the specified predicate.
expectWSEvent :: String
-- ^ A human-readable label for this test in case it
-- fails.
-> (WebsocketEvent -> Bool)
-- ^ The predicate to apply.
-> TestM ()
expectWSEvent name match = do
chan <- gets tsWebsocketChan
let timeoutAmount = 10 * 1000 * 1000
mEv <- liftIO $ timeout timeoutAmount $
STM.atomically $ STM.readTChan chan
case mEv of
Nothing -> do
let msg = "Expected a websocket event for " <> show name <>
" but timed out waiting"
print_ msg
error msg
Just ev -> when (not $ match ev) $ do
let msg = "Expected a websocket event for " <> show name <>
" but got " <> show ev
print_ msg
error msg
-- | Does the websocket correspond to the specified user?
forUser :: User -> WebsocketEvent -> Bool
forUser u =
wsHas (wepUserId . weData) (Just $ userId u)
-- | Does the websocket correspond to the specified channel?
forChannel :: Channel -> WebsocketEvent -> Bool
forChannel ch =
wsHas (wepChannelId . weData) (Just $ channelId ch)
-- | Is this websocket event a status change message?
isStatusChange :: User
-- ^ The user whose status changed
-> T.Text
-- ^ The new status
-> WebsocketEvent
-> Bool
isStatusChange u s =
hasWSEventType WMStatusChange &&&
forUser u &&&
wsHas (wepStatus . weData) (Just s)
-- | Is the websocket event indicating that a new user was added to the
-- server?
isNewUserEvent :: User
-- ^ The user that was added
-> WebsocketEvent
-> Bool
isNewUserEvent u =
hasWSEventType WMNewUser &&& forUser u
isViewedChannel :: WebsocketEvent -> Bool
isViewedChannel = hasWSEventType WMChannelViewed
-- | Is the websocket event indicating that a new user was added to the
-- team
isAddedToTeam :: User
-- ^ The user that was added
-> Team
-- ^ The team to which the user was added
-> WebsocketEvent
-> Bool
isAddedToTeam u _ =
hasWSEventType WMAddedToTeam &&& forUser u
isChannelCreatedEvent :: Channel
-> WebsocketEvent
-> Bool
isChannelCreatedEvent c =
hasWSEventType WMChannelCreated &&& forChannel c
-- | Is the websocket event indicating that a channel was deleted?
isChannelDeleteEvent :: Channel -> WebsocketEvent -> Bool
isChannelDeleteEvent ch =
forChannel ch &&& hasWSEventType WMChannelDeleted
-- | Is the websocket event indicating that a user joined a channel?
isUserJoin :: User
-- ^ The user that joined a channel
-> Channel
-- ^ The channel that was joined
-> WebsocketEvent
-> Bool
isUserJoin u ch =
hasWSEventType WMUserAdded &&&
forUser u &&&
wsHas (webChannelId . weBroadcast) (Just $ channelId ch)
-- | Is the websocket event indicating that a user left a channel?
isUserLeave :: User
-- ^ The user that left a channel
-> Channel
-- ^ The channel that the user left
-> WebsocketEvent
-> Bool
isUserLeave u ch =
hasWSEventType WMUserRemoved &&&
forChannel ch &&&
wsHas (webUserId . weBroadcast) (Just $ userId u)
-- | Is the websocket event indicating that a new message was posted to
-- a channel?
isPost :: User
-- ^ The user who posted
-> Channel
-- ^ The channel to which the new post was added
-> UserText
-- ^ The content of the new post
-> WebsocketEvent
-> Bool
isPost u ch msg =
hasWSEventType WMPosted &&&
wsHas (\e -> postMessage <$> (wepPost $ weData e))
(Just msg) &&&
wsHas (\e -> postChannelId <$> (wepPost $ weData e))
(Just $ channelId ch) &&&
wsHas (\e -> postUserId =<< (wepPost $ weData e))
(Just $ userId u)
-- | Timeout in seconds for expectWSDone to wait before concluding that
-- no new websocket events are available.
emptyWSTimeout :: Int
emptyWSTimeout = 2
-- | Expect that the websocket event channel is empty. Waits up to
-- emptyWSTimeout seconds. Succeeds if no events are received; fails
-- otherwise.
expectWSDone :: TestM ()
expectWSDone = do
chan <- gets tsWebsocketChan
let timeoutAmount = emptyWSTimeout * 1000 * 1000
mEv <- liftIO $ timeout timeoutAmount $
STM.atomically $ STM.readTChan chan
case mEv of
Nothing -> return ()
Just ev -> do
let msg = "Expected no websocket events but got " <> show ev
print_ msg
error msg
loginAdminAccount :: TestM ()
loginAdminAccount = do
cfg <- gets tsConfig
let admin = Login { username = configUsername cfg
, password = configPassword cfg
, otpToken = Nothing
}
loginAccount admin
createAccount :: UsersCreate -> TestM User
createAccount account = do
session <- getSession
newUser <- liftIO $ mmCreateUser account session
print_ $ "account created for " <> (T.unpack $ usersCreateUsername account)
return newUser
createTeam :: TeamsCreate -> TestM Team
createTeam tc = do
session <- getSession
team <- liftIO $ mmCreateTeam tc session
print_ $ "Team created: " <> (T.unpack $ teamsCreateName tc)
return team
findChannel :: Channels -> UserText -> Channel
findChannel chans name =
let result = Seq.viewl (Seq.filter nameMatches chans)
nameMatches c = name `elem` [ channelName c
, channelDisplayName c
]
in case result of
chan Seq.:< _ -> chan
_ ->
let namePairs = mkPair <$> chans
mkPair c = (channelName c, channelDisplayName c)
in error $ "Expected to find channel by name " <>
show name <> " but got " <> show namePairs
connectFromConfig :: TestConfig -> IO ConnectionData
connectFromConfig cfg =
initConnectionData (configHostname cfg) (fromIntegral (configPort cfg)) T.empty ConnectHTTP
defaultConnectionPoolConfig
getConnection :: TestM ConnectionData
getConnection = gets tsConnectionData
getSession :: TestM Session
getSession = do
val <- gets tsSession
case val of
Just s -> return s
Nothing -> error "Expected authentication token but none was present"
getTeams :: TestM (Seq.Seq Team)
getTeams = do
session <- getSession
liftIO $ mmGetUsersTeams UserMe session
getMe :: TestM User
getMe = do
session <- getSession
liftIO $ mmGetUser UserMe session
getUserByName :: T.Text -> TestM (Maybe User)
getUserByName uname = do
session <- getSession
let query = defaultUserQuery
{ userQueryPage = Just 0
, userQueryPerPage = Just 10000
}
allUserMap <- liftIO $ mmGetUsers query session
-- Find the user matching the username and get its ID
let matches = Seq.filter matchingUser allUserMap
matchingUser u = userUsername u == uname
case Seq.viewl matches of
user Seq.:< _ -> do
let uId = userId user
-- Then load the User record
Just <$> (liftIO $ mmGetUser (UserById uId) session)
_ -> return Nothing
createChannel :: MinChannel -> TestM Channel
createChannel mc = do
session <- getSession
liftIO $ mmCreateChannel mc session
deleteChannel :: Channel -> TestM ()
deleteChannel ch = do
session <- getSession
liftIO $ mmDeleteChannel (channelId ch) session
joinChannel :: User -> Channel -> TestM ()
joinChannel user chan = do
session <- getSession
let member = MinChannelMember
{ minChannelMemberUserId = userId user
, minChannelMemberChannelId = channelId chan
}
liftIO $ void $ mmAddUser (channelId chan) member session
leaveChannel :: Channel -> TestM ()
leaveChannel chan = do
session <- getSession
liftIO $ mmRemoveUserFromChannel (channelId chan) UserMe session
getChannelMembers :: Channel -> TestM [User]
getChannelMembers chan = do
session <- getSession
let query = defaultUserQuery
{ userQueryPage = Just 0
, userQueryPerPage = Just 10000
, userQueryInChannel = Just (channelId chan)
}
F.toList <$> (liftIO $ mmGetUsers query session)
getChannels :: Team -> TestM Channels
getChannels team = do
session <- getSession
liftIO $ mmGetPublicChannels (teamId team) Nothing Nothing session
getConfig :: TestM ServerConfig
getConfig = do
session <- getSession
liftIO $ mmGetConfiguration session
getClientConfig :: TestM ClientConfig -- A.Value
getClientConfig = do
session <- getSession
liftIO $ mmGetClientConfiguration (Just (T.pack "old")) session
saveConfig :: ServerConfig -> TestM ()
saveConfig newConfig = do
session <- getSession
liftIO $ void $ mmUpdateConfiguration newConfig session
teamAddUser :: Team -> User -> TestM ()
teamAddUser team user = do
session <- getSession
let member = TeamMember
{ teamMemberUserId = userId user
, teamMemberTeamId = teamId team
, teamMemberRoles = T.empty
}
liftIO $ void $ mmAddUserToTeam (teamId team) member session