webex-teams-pipes (empty) → 0.2.0.0
raw patch · 9 files changed
+1422/−0 lines, 9 filesdep +aesondep +asyncdep +basesetup-changed
Dependencies added: aeson, async, base, bytestring, data-default, hspec, http-client, http-conduit, http-types, network-uri, optparse-applicative, pipes, text, utf8-string, wai, warp, webex-teams-api, webex-teams-pipes
Files
- ChangeLog.md +3/−0
- LICENSE +21/−0
- README.md +37/−0
- Setup.hs +2/−0
- app/Main.hs +361/−0
- src/Network/WebexTeams/Pipes.hs +65/−0
- test/Network/WebexTeams/PipesSpec.hs +845/−0
- test/Spec.hs +1/−0
- webex-teams-pipes.cabal +87/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+## webex-teams-pipes-0.2.0.0++* Initial release.
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2017,2018 Naoto Shimazaki++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,37 @@+# webex-teams-pipes++[](https://opensource.org/licenses/MIT)+[](https://travis-ci.org/nshimaza/webex-teams-api)+[](https://hackage.haskell.org/package/webex-teams-pipes)+[](http://stackage.org/nightly/package/webex-teams-pipes)+[](http://stackage.org/lts/package/webex-teams-pipes)++pipes wrapper of webex-teams-api.++Webex-teams-pipes is thin wrappers of list API. It transform chunky response+from list API into seamless stream of elements.++A Haskell bindings for Cisco Webex Teams (formerly Cisco Spark) API++This package also provides some sample usage in command line application style.+See source under app directory of the source package.++### Sample Usage++Following example is calling List Membership API which returns membership between+Rooms and users (Person). You can extract each Membership from+pipes pipe. The `streamListWithFilter`, provided by webex-teams-pipes,+automatically performs pagenation when it is asked more element and last+response had link of subsequent page in HTTP Link Header.++```haskell+ let auth = Authorization "your authorization token"+ filter = MembershipFilter yourRoomId Nothing Nothing+ runEffect $ streamListWithFilter auth def filter .| takeC 200 .| mapM_C print (FIXME)+```++You can find more examples in app/Main.hs.++### Limitation++- Relative reference in Link Header is not recognized as next page
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,361 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.ByteString.Char8 as BC8 (pack)+import Data.Default (def)+import Data.Semigroup ((<>))+import Data.Text as T (pack)+import Network.HTTP.Client (responseBody)+import Options.Applicative+import Pipes (for, lift, runEffect, (>->))+import qualified Pipes.Prelude as P (take)+import System.Environment (lookupEnv)+import System.IO (hPutStrLn, stderr)++import Network.WebexTeams hiding (streamTeamList)+import Network.WebexTeams.Pipes++main :: IO ()+main = lookupEnv "WEBEXTEAMS_AUTH" >>= runIfEnvFound+ where+ runIfEnvFound Nothing = hPutStrLn stderr "Missing WEBEXTEAMS_AUTH. Set Webex Teams authorization string to WEBEXTEAMS_AUTH environment variable."+ runIfEnvFound (Just s) = do+ opts <- execParser programOptions+ print opts+ run (Authorization $ BC8.pack s) opts++run :: Authorization -> Command -> IO ()+run auth (PersonListCommand count filter) =+ runEffect $ for (streamListWithFilter auth def filter >-> P.take count) (lift . print)++run auth (RoomListCommand count filter) =+ runEffect $ for (streamListWithFilter auth def filter >-> P.take count) (lift . print)++run auth (MembershipListCommand count filter) =+ runEffect $ for (streamListWithFilter auth def filter >-> P.take count) (lift . print)++run auth (MessageListCommand count filter) =+ runEffect $ for (streamListWithFilter auth def filter >-> P.take count) (lift . print)++run auth (TeamMembershipListCommand count filter) =+ runEffect $ for (streamListWithFilter auth def filter >-> P.take count) (lift . print)++run auth (TeamListCommand count) =+ runEffect $ for (streamTeamList auth def >-> P.take count) (lift . print)++run auth (PersonDetailCommand personId) =+ getDetail auth def personId >>= print . responseBody++run auth (RoomDetailCommand roomId) =+ getDetail auth def roomId >>= print . responseBody++run auth (MembershipDetailCommand membershipId) =+ getDetail auth def membershipId >>= print . responseBody++run auth (MessageDetailCommand messageId) =+ getDetail auth def messageId >>= print . responseBody++run auth (CreateMessageCommand roomId messageText) =+ createEntity auth def message >>= print . responseBody+ where+ message = CreateMessage (Just roomId) Nothing Nothing (Just messageText) Nothing Nothing++run auth (DeleteMessageCommand messageId) =+ deleteMessage auth def messageId >>= print . responseBody++run auth (TeamDetailCommand teamId) =+ getDetail auth def teamId >>= print . responseBody++run auth (TeamMembershipDetailCommand teamMembershipId) =+ getDetail auth def teamMembershipId >>= print . responseBody++run auth (CreateRoomCommand createRoom) =+ createEntity auth def createRoom >>= print . responseBody++run auth (DeleteRoomCommand roomId) =+ deleteRoom auth def roomId >>= print . responseBody++{-+ Command line parser+-}+data Command+ = PersonListCommand Int PersonFilter+ | PersonDetailCommand PersonId+ | RoomListCommand Int RoomFilter+ | RoomDetailCommand RoomId+ | CreateRoomCommand CreateRoom+ | DeleteRoomCommand RoomId+ | MembershipListCommand Int MembershipFilter+ | MembershipDetailCommand MembershipId+ | MessageListCommand Int MessageFilter+ | MessageDetailCommand MessageId+ | CreateMessageCommand RoomId MessageText+ | DeleteMessageCommand MessageId+ | TeamListCommand Int+ | TeamDetailCommand TeamId+ | TeamMembershipListCommand Int TeamMembershipFilter+ | TeamMembershipDetailCommand TeamMembershipId+ deriving (Show)+++{-+ Common command line option parsers+-}+countParser :: Int -> Parser Int+countParser defaultCount = option auto+ ( long "count"+ <> short 'c'+ <> metavar "MAX_ITEMS"+ <> value (if defaultCount == def then maxBound else defaultCount)+ <> help "Maximum number of items to print"+ )++roomIdParser :: Parser RoomId+roomIdParser = RoomId . T.pack <$> strArgument+ ( metavar "ROOM_ID"+ <> help "Identifier of a room")++emailOptParser :: Parser Email+emailOptParser = Email . T.pack <$> strOption+ ( long "email"+ <> short 'e'+ <> metavar "EMAIL_ADDRESS"+ <> help "An email address to filter result"+ )++teamIdOptParser :: Parser TeamId+teamIdOptParser = TeamId . T.pack <$> strOption+ ( long "team"+ <> short 't'+ <> metavar "TEAM_ID"+ <> help "Identifier of a team"+ )++{-+ Person specific command line option parsers+-}+displayNameParser :: Parser DisplayName+displayNameParser = DisplayName . T.pack <$> strOption+ ( long "name"+ <> short 'n'+ <> metavar "DISPLAY_NAME"+ <> help "Display name of the person to look up"+ )++organizationIdOptParser :: Parser OrganizationId+organizationIdOptParser = OrganizationId . T.pack <$> strOption+ ( long "org"+ <> short 'o'+ <> metavar "ORGANIZATION_ID"+ <> help "Identifier of a organization to be searched"+ )++personListOptParser :: Parser Command+personListOptParser = PersonListCommand <$> countParser def+ <*> (PersonFilter <$> optional emailOptParser+ <*> optional displayNameParser+ <*> optional organizationIdOptParser)++personDetailOptParser :: Parser Command+personDetailOptParser = PersonDetailCommand <$> (PersonId . T.pack <$> strArgument+ ( metavar "PERSON_ID"+ <> help "Identifier of a person"+ ))++{-+ Room specific command line option parsers+-}+roomTypeParser :: Parser (Maybe RoomType)+roomTypeParser+ = flag Nothing (Just RoomTypeDirect)+ ( long "direct"+ <> short 'd'+ <> help "Filter only one-to-one space")+ <|> flag Nothing (Just RoomTypeGroup)+ ( long "group"+ <> short 'g'+ <> help "Filter only group space")++roomSortByParser :: Parser (Maybe RoomFilterSortBy)+roomSortByParser+ = flag Nothing (Just RoomFilterSortById)+ ( long "sort-by-id"+ <> help "Sort by room ID")+ <|> flag Nothing (Just RoomFilterSortByLastActivity)+ ( long "sort-by-last-activity"+ <> help "Sort by most recent activity")+ <|> flag Nothing (Just RoomFilterSortByCreated)+ ( long "sort-by-created"+ <> help "Sort by most recentlly created")++roomTitleParser :: Parser RoomTitle+roomTitleParser = RoomTitle . T.pack <$> strArgument+ ( metavar "ROOM_TITLE"+ <> help "A user-friendly name for the room")++roomListOptParser :: Parser Command+roomListOptParser = RoomListCommand <$> countParser def+ <*> (RoomFilter <$> optional teamIdOptParser+ <*> roomTypeParser+ <*> roomSortByParser)++roomDetailOptParser :: Parser Command+roomDetailOptParser = RoomDetailCommand <$> roomIdParser++createRoomOptParser :: Parser Command+createRoomOptParser = CreateRoomCommand <$> (CreateRoom <$> roomTitleParser+ <*> optional teamIdOptParser)+deleteRoomOptParser :: Parser Command+deleteRoomOptParser = DeleteRoomCommand <$> roomIdParser+++{-+ Membership specific command line option parsers+-}+roomIdOptParser :: Parser RoomId+roomIdOptParser = RoomId . T.pack <$> strOption+ ( long "room"+ <> short 'r'+ <> metavar "ROOM_ID"+ <> help "Identifier of a room to be searched"+ )++personIdOptParser :: Parser PersonId+personIdOptParser = PersonId . T.pack <$> strOption+ ( long "person"+ <> short 'p'+ <> metavar "PERSON_ID"+ <> help "Identifier of a person to filter result"+ )++membershipListOptParser :: Parser Command+membershipListOptParser = MembershipListCommand <$> countParser def+ <*> (MembershipFilter <$> optional roomIdOptParser+ <*> optional personIdOptParser+ <*> optional emailOptParser)++membershipDetailOptParser :: Parser Command+membershipDetailOptParser = MembershipDetailCommand <$> (MembershipId . T.pack <$> strArgument+ ( metavar "MEMBERSHIP_ID"+ <> help "Identifier of a membership"+ ))++{-+ Message specific command line option parsers+-}+mentionedPeopleParser :: Parser (Maybe MentionedPeople)+mentionedPeopleParser+ = flag Nothing (Just MentionedPeopleMe)+ ( long "me"+ <> help "List messages where the caller is mentioned"+ )+ <|> optional (MentionedPeople . PersonId . T.pack <$> strOption+ ( long "mentioned-person"+ <> short 'p'+ <> metavar "PERSON_ID"+ <> help "List messages where the specified persionId is mentioned"+ )+ )++beforeOptParser :: Parser Timestamp+beforeOptParser = Timestamp . T.pack <$> strOption+ ( long "before"+ <> short 'b'+ <> metavar "TIMESTAMP"+ <> help "List messages sent before a date and time, in ISO8601 format"+ )++beforeMessageOptParser :: Parser MessageId+beforeMessageOptParser = MessageId . T.pack <$> strOption+ ( long "before-message"+ <> short 'm'+ <> metavar "MESSAGE_ID"+ <> help "List messages sent before a message, by ID"+ )++messageListOptParser :: Parser Command+messageListOptParser = MessageListCommand <$> countParser 10+ <*> (MessageFilter <$> roomIdParser+ <*> mentionedPeopleParser+ <*> optional beforeOptParser+ <*> optional beforeMessageOptParser+ )++messageIdParser :: Parser MessageId+messageIdParser = MessageId . T.pack <$> strArgument+ ( metavar "MESSAGE_ID"+ <> help "Identifier of a message")++messageDetailOptParser :: Parser Command+messageDetailOptParser = MessageDetailCommand <$> messageIdParser++messageTextParser :: Parser MessageText+messageTextParser = MessageText . T.pack <$> strArgument+ ( metavar "MESSAGE_TEXT"+ <> help "Body of message in text"+ )++createMessageOptParser :: Parser Command+createMessageOptParser = CreateMessageCommand <$> roomIdParser <*> messageTextParser++deleteMessageOptParser :: Parser Command+deleteMessageOptParser = DeleteMessageCommand <$> messageIdParser++{-+ Team specific command line option parsers+-}+teamIdParser :: Parser TeamId+teamIdParser = TeamId . T.pack <$> strArgument+ ( metavar "TEAM_ID"+ <> help "Identifier of a team"+ )++teamListOptParser :: Parser Command+teamListOptParser = TeamListCommand <$> countParser def++teamDetailOptParser :: Parser Command+teamDetailOptParser = TeamDetailCommand <$> teamIdParser++{-+ Team membership specific command line option parsers+-}+teamMembershipListOptParser :: Parser Command+teamMembershipListOptParser = TeamMembershipListCommand <$> countParser def+ <*> (TeamMembershipFilter <$> teamIdParser)++teamMembershipDetailOptParser :: Parser Command+teamMembershipDetailOptParser = TeamMembershipDetailCommand <$> (TeamMembershipId . T.pack <$> strArgument+ ( metavar "TEAM_MEMBERSHIP_ID"+ <> help "Identifier of a team membership"+ ))++{-+ Top level parsers+-}+commandSubParser :: Parser Command+commandSubParser = hsubparser+ ( command "person-list" (info personListOptParser (progDesc "List people"))+ <> command "person-detail" (info personDetailOptParser (progDesc "Get detail for a person by ID"))+ <> command "room-list" (info roomListOptParser (progDesc "List belonging spaces"))+ <> command "room-detail" (info roomDetailOptParser (progDesc "Get detail for a team by ID"))+ <> command "create-room" (info createRoomOptParser (progDesc "Create a room"))+ <> command "delete-room" (info deleteRoomOptParser (progDesc "Delete a room"))+ <> command "membership-list" (info membershipListOptParser (progDesc "List memberships of authenticated user"))+ <> command "membership-detail" (info membershipDetailOptParser (progDesc "Get detail for a membership by ID"))+ <> command "message-list" (info messageListOptParser (progDesc "List messages in a room"))+ <> command "message-detail" (info messageDetailOptParser (progDesc "Get detail for a message by ID"))+ <> command "create-message" (info createMessageOptParser (progDesc "Send a message to a room"))+ <> command "delete-message" (info deleteMessageOptParser (progDesc "Delete a message"))+ <> command "team-list" (info teamListOptParser (progDesc "List belonging teams"))+ <> command "team-detail" (info teamDetailOptParser (progDesc "Get detail for a team by ID"))+ <> command "team-membership-list" (info teamMembershipListOptParser (progDesc "List team memberships of authenticated user"))+ <> command "team-membership-detail" (info teamMembershipDetailOptParser (progDesc "Get detail for a team membership by ID"))+ )++programOptions :: ParserInfo Command+programOptions = info (commandSubParser <**> helper)+ ( fullDesc+ <> progDesc "Sample porgram demonstrating how to use webex-teams-pipes"+ <> header "webex-teams-pipes-exe -- Sample porgram demonstrating how to use webex-teams-pipes"+ )
+ src/Network/WebexTeams/Pipes.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RankNTypes #-}++{-|+Module : Network.WebexTeams.Pipes+Copyright : (c) Naoto Shimazaki 2018+License : MIT (see the file LICENSE)++Maintainer : https://github.com/nshimaza+Stability : experimental++This module provides Pipes wrapper for Cisco Webex Teams list APIs.++-}+module Network.WebexTeams.Pipes+ (+ -- * Functions+ streamListWithFilter+ , streamTeamList+ , streamOrganizationList+ , streamRoleList+ ) where++import Control.Monad (unless)+import Data.Foldable (traverse_)+import Pipes (MonadIO, Producer', liftIO, yield)++import Network.WebexTeams hiding (streamOrganizationList,+ streamRoleList, streamTeamList)+++{-|+ Common worker function for List APIs.+ It accesses List API with given 'Request', unwrap result into list of items, stream them to Pipes pipe+ and finally it automatically accesses next page designated via HTTP Link header if available.+-}+readerToProducer :: MonadIO m => ListReader i -> Producer' i m ()+readerToProducer reader = go+ where+ go = do+ xs <- liftIO reader+ unless (null xs) $ do+ traverse_ yield xs+ go++-- | Get list of entities with query parameter and stream it into Pipes. It automatically performs pagination.+streamListWithFilter :: (MonadIO m, WebexTeamsFilter filter, WebexTeamsListItem (ToResponse filter))+ => Authorization -- ^ Authorization string against Webex Teams API.+ -> WebexTeamsRequest -- ^ Predefined part of 'Request' commonly used for Webex Teams API.+ -> filter -- ^ Filter criteria of the request. Type of filter automatically determines+ -- item type in response.+ -> Producer' (ToResponse filter) m ()+streamListWithFilter auth base param = getListWithFilter auth base param >>= readerToProducer++-- | List of 'Team' and stream it into Pipes. It automatically performs pagination.+streamTeamList :: MonadIO m => Authorization -> WebexTeamsRequest -> Producer' Team m ()+streamTeamList auth base = getTeamList auth base >>= readerToProducer++-- | Filter list of 'Organization' and stream it into Pipes. It automatically performs pagination.+streamOrganizationList :: MonadIO m => Authorization -> WebexTeamsRequest -> Producer' Organization m ()+streamOrganizationList auth base = getOrganizationList auth base >>= readerToProducer++-- | List of 'Role' and stream it into Pipes. It automatically performs pagination.+streamRoleList :: MonadIO m => Authorization -> WebexTeamsRequest -> Producer' Role m ()+streamRoleList auth base = getRoleList auth base >>= readerToProducer
+ test/Network/WebexTeams/PipesSpec.hs view
@@ -0,0 +1,845 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.WebexTeams.PipesSpec where++import Control.Concurrent.Async (withAsync)+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar,+ takeMVar)+import Control.Monad (void)+import Data.Aeson (encode)+import qualified Data.ByteString as S (ByteString)+import qualified Data.ByteString.Char8 as C8 (pack, unpack)+import qualified Data.ByteString.Lazy as L (ByteString)+import Data.Default (def)+import Data.List (sort)+import Data.Maybe (fromJust)+import Data.Monoid ((<>))+import Data.Text (pack)+import Network.HTTP.Simple as C (addRequestHeader,+ defaultRequest,+ getResponseBody, httpJSON,+ parseRequest, setRequestHost,+ setRequestMethod,+ setRequestPath,+ setRequestPort,+ setRequestSecure)+import Network.HTTP.Types (Header, status200)+import Network.URI (URIAuth (..))+import Network.Wai (Application, queryString,+ rawPathInfo, requestHeaders,+ requestMethod, responseLBS)+import Network.Wai.Handler.Warp (defaultSettings, runSettings,+ setBeforeMainLoop, setPort)+import Pipes (runEffect)+import qualified Pipes.Prelude as P (toListM)++import Test.Hspec++import Network.WebexTeams hiding (streamOrganizationList,+ streamRoleList, streamTeamList)+import Network.WebexTeams.Internal (getNextUrl)+import Network.WebexTeams.Pipes+++listenPort = 3002+listenPortBS = (C8.pack . show) listenPort++mockBaseRequestRequest+ = C.addRequestHeader "Content-Type" "application/json; charset=utf-8"+ $ C.setRequestPort listenPort+ $ C.setRequestHost "127.0.0.1"+ $ C.setRequestSecure False+ $ C.defaultRequest++mockBaseRequest :: WebexTeamsRequest+mockBaseRequest = WebexTeamsRequest mockBaseRequestRequest "http:" $+ URIAuth "" "127.0.0.1" (":" <> show listenPort)++dummyAuth :: Authorization+dummyAuth = Authorization "dummyAuth"++extractRight :: Show err => Either err r -> r+extractRight (Right r) = r+extractRight (Left err) = error $ show err++withMockServer :: Application -> IO () -> IO ()+withMockServer app inner = do+ readyToConnect <- newEmptyMVar+ let set = setBeforeMainLoop (putMVar readyToConnect ()) $ setPort listenPort defaultSettings+ withAsync (runSettings set app) (\_ -> takeMVar readyToConnect >> inner)++helloApp :: Application+helloApp _ respond = respond $ responseLBS status200 [] "hello, world"++contentType :: Header+contentType = ("Content-Type", "application/json; charset=utf-8")++simpleApp :: L.ByteString -> Application+simpleApp res _ respond = respond $ responseLBS status200 [contentType] res++paginationApp :: [L.ByteString] -> Application+paginationApp ress req respond = do+ let (cTypes, body) = dispatch $ rawPathInfo req+ respond $ responseLBS status200 cTypes body+ where+ dispatch "/1" = ([contentType, ("Link", "<http://127.0.0.1:" <> listenPortBS <> "/2>; rel=\"next\"")], ress !! 1)+ dispatch "/2" = ([contentType, ("Link", "<http://127.0.0.1:" <> listenPortBS <> "/3>; rel=\"next\"")], ress !! 2)+ dispatch "/3" = ([contentType], ress !! 3)+ dispatch _ = ([contentType, ("Link", "<http://127.0.0.1:" <> listenPortBS <> "/1>; rel=\"next\"")], ress !! 0)++invalidPaginationApp :: [L.ByteString] -> Application+invalidPaginationApp ress req respond = do+ let (cTypes, body) = dispatch $ rawPathInfo req+ respond $ responseLBS status200 cTypes body+ where+ dispatch "/1" = ([contentType, ("Link", "<http://127.0.0.1:8888/2>; rel=\"next\"")], ress !! 1)+ dispatch "/2" = ([contentType, ("Link", "<http://127.0.0.1:" <> listenPortBS <> "/3>; rel=\"next\"")], ress !! 2)+ dispatch "/3" = ([contentType], ress !! 3)+ dispatch _ = ([contentType, ("Link", "<http://127.0.0.1:" <> listenPortBS <> "/1>; rel=\"next\"")], ress !! 0)+++teamGen :: String -> Team+teamGen i = Team { teamId = TeamId . pack $ "teamId" <> i+ , teamErrors = Nothing+ , teamName = Just . TeamName . pack $ "teamName" <> i+ , teamCreatorId = Just . PersonId . pack $ "teamCreatorId" <> i+ , teamCreated = Just . Timestamp . pack $ "teamCreated" <> i+ }++teamList :: String -> [Team]+teamList j = [ teamGen $ j <> show i | i <- [1..3] ]++teamListList :: [[Team]]+teamListList = [ teamList [c] | c <- ['a'..'d'] ]++readAllList :: ListReader i -> IO [i]+readAllList reader = go []+ where+ go xs = reader >>= \chunk -> case chunk of+ [] -> pure xs+ ys -> go (xs <> ys)++spec :: Spec+spec = do+ describe "Mock Applications" $ do+ it "simple mock app returns list of team" $ do+ receivedReqMVar <- newEmptyMVar+ let (WebexTeamsRequest baseReq _ _) = mockBaseRequest+ req = setRequestPath "/v1/teams"+ $ setRequestMethod "GET"+ $ baseReq+ testData = TeamList $ teamList ['Z']++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode testData) req respond+ ) $ do+ res <- getResponseBody <$> httpJSON req+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ requestMethod receivedReq `shouldBe` "GET"+ rawPathInfo receivedReq `shouldBe` "/v1/teams"+ (lookup "Content-Type" . requestHeaders) receivedReq `shouldBe` Just "application/json; charset=utf-8"++ it "pagenation mock app returns list of team and Link header" $ do+ receivedReqMVar <- newEmptyMVar+ let (WebexTeamsRequest baseReq _ _) = mockBaseRequest+ req = setRequestPath "/v1/teams"+ $ setRequestMethod "GET"+ $ baseReq+ testData = encode . TeamList <$> teamListList++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ paginationApp testData req respond+ ) $ do+ res1 <- httpJSON req+ getResponseBody res1 `shouldBe` TeamList (teamListList !! 0)++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/teams"++ let path = getNextUrl res1+ path `shouldBe` (Just $ "http://127.0.0.1:" <> listenPortBS <> "/1")++ req2 <- parseRequest $ "GET " <> C8.unpack (fromJust path)+ res2 <- httpJSON req2+ getResponseBody res2 `shouldBe` TeamList (teamListList !! 1)++ describe "People" $ do+ let personJson1 = "{\+ \ \"id\" : \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY\",\+ \ \"emails\" : [ \"johnny.chang@foomail.com\", \"jchang@barmail.com\" ],\+ \ \"displayName\" : \"John Andersen\",\+ \ \"firstName\" : \"John\",\+ \ \"lastName\" : \"Andersen\",\+ \ \"avatar\" : \"https://1efa7a94ed21783e352-c62266528714497a17239ececf39e9e2.ssl.cf1.rackcdn.com/V1~54c844c89e678e5a7b16a306bc2897b9~wx29yGtlTpilEFlYzqPKag==~1600\",\+ \ \"orgId\" : \"Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi85NmFiYzJhYS0zZGNjLTExZTUtYTE1Mi1mZTM0ODE5Y2RjOWE\",\+ \ \"roles\" : [ \"Y2lzY29zcGFyazovL3VzL1JPT00vOGNkYzQwYzQtZjA5ZS0zY2JhLThjMjYtZGQwZTcwYWRlY2Iy\", \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX\" ],\+ \ \"licenses\" : [ \"Y2lzY29zcGFyazovL3VzL1JPT00vOGNkYzQwYzQtZjA5ZS0zY2JhLThjMjYtZGQwZTcwYWRlY2Iy\", \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX\" ],\+ \ \"created\" : \"2015-10-18T14:26:16.000Z\",\+ \ \"timezone\" : \"America/Denver\",\+ \ \"lastActivity\" : \"2015-10-18T14:26:16.028Z\",\+ \ \"status\" : \"active\",\+ \ \"invitePending\" : false,\+ \ \"loginEnabled\" : true\+ \}"+ person1 = Person { personId = PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY"+ , personErrors = Nothing+ , personEmails = Just [Email "johnny.chang@foomail.com", Email "jchang@barmail.com"]+ , personDisplayName = Just $ DisplayName "John Andersen"+ , personNickName = Nothing+ , personFirstName = Just $ FirstName "John"+ , personLastName = Just $ LastName "Andersen"+ , personAvatar = Just $ AvatarUrl "https://1efa7a94ed21783e352-c62266528714497a17239ececf39e9e2.ssl.cf1.rackcdn.com/V1~54c844c89e678e5a7b16a306bc2897b9~wx29yGtlTpilEFlYzqPKag==~1600"+ , personOrgId = Just $ OrganizationId "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi85NmFiYzJhYS0zZGNjLTExZTUtYTE1Mi1mZTM0ODE5Y2RjOWE"+ , personRoles = Just [ RoleId "Y2lzY29zcGFyazovL3VzL1JPT00vOGNkYzQwYzQtZjA5ZS0zY2JhLThjMjYtZGQwZTcwYWRlY2Iy"+ , RoleId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX"]+ , personLicenses = Just [ LicenseId "Y2lzY29zcGFyazovL3VzL1JPT00vOGNkYzQwYzQtZjA5ZS0zY2JhLThjMjYtZGQwZTcwYWRlY2Iy"+ , LicenseId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX"]+ , personCreated = Just $ Timestamp "2015-10-18T14:26:16.000Z"+ , personTimezone = Just $ Timezone "America/Denver"+ , personLastActivity = Just $ Timestamp "2015-10-18T14:26:16.028Z"+ , personStatus = Just PersonStatusActive+ , personInvitePending = Just False+ , personLoginEnabled = Just True+ , personType = Nothing+ }+ personGen i = Person { personId = PersonId . pack $ "PersonId" <> i+ , personErrors = Nothing+ , personEmails = Just [Email . pack $ "email" <> i <> "@foomail.com", Email . pack $ "email" <> i <> "@barmail.com"]+ , personDisplayName = Just . DisplayName . pack $ "John Andersen" <> i+ , personNickName = Nothing+ , personFirstName = Just $ FirstName . pack $ "John" <> i+ , personLastName = Just $ LastName . pack $ "Andersen" <> i+ , personAvatar = Just $ AvatarUrl . pack $ "https://AvatarUrl" <> i+ , personOrgId = Just . OrganizationId . pack $ "OrganizationId" <> i+ , personRoles = Just [ RoleId . pack $ "RoleIdA" <> i+ , RoleId . pack $ "RoleIdB" <> i]+ , personLicenses = Just [ LicenseId . pack $ "LicenseIdX" <> i+ , LicenseId . pack $ "LicenseIdY" <> i]+ , personCreated = Just . Timestamp . pack $ "Created" <> i+ , personTimezone = Just $ Timezone . pack $ "Timezone" <> i+ , personLastActivity = Just $ Timestamp . pack $ "LastActivity" <> i+ , personStatus = Just PersonStatusActive+ , personInvitePending = Just False+ , personLoginEnabled = Just True+ , personType = Nothing+ }+ personList j = [ personGen $ j <> show i | i <- [1..3] ]+ personListList = [ personList [c] | c <- ['a'..'d'] ]+ newPerson = CreatePerson { createPersonEmails = Just $ [Email "johnny.chang@foomail.com", Email "jchang@barmail.com"]+ , createPersonDisplayName = Just $ DisplayName "John Andersen"+ , createPersonFirstName = Just $ FirstName "John"+ , createPersonLastName = Just $ LastName "Andersen"+ , createPersonAvatar = Just $ AvatarUrl "https://1efa7a94ed21783e352-c62266528714497a17239ececf39e9e2.ssl.cf1.rackcdn.com/V1~54c844c89e678e5a7b16a306bc2897b9~wx29yGtlTpilEFlYzqPKag==~1600"+ , createPersonOrgId = Just $ OrganizationId "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi85NmFiYzJhYS0zZGNjLTExZTUtYTE1Mi1mZTM0ODE5Y2RjOWE"+ , createPersonRoles = Just [ RoleId "Y2lzY29zcGFyazovL3VzL1JPT00vOGNkYzQwYzQtZjA5ZS0zY2JhLThjMjYtZGQwZTcwYWRlY2Iy"+ , RoleId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX"]+ , createPersonLicenses = Just [ LicenseId "Y2lzY29zcGFyazovL3VzL1JPT00vOGNkYzQwYzQtZjA5ZS0zY2JhLThjMjYtZGQwZTcwYWRlY2Iy"+ , LicenseId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX"]+ }+ updatePerson = UpdatePerson { updatePersonDisplayName = Just $ DisplayName "New John Andersen"+ , updatePersonFirstName = Just $ FirstName "New John"+ , updatePersonLastName = Just $ LastName "New Andersen"+ , updatePersonAvatar = Just $ AvatarUrl "https://example.com/newAvatarUrl"+ , updatePersonOrgId = Just $ OrganizationId "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi85NmFiYzJhYS0zZGNjLTExZTUtYTE1Mi1mZTM0ODE5Y2RjOWE"+ , updatePersonRoles = Just $ [ RoleId "newRoleId1", RoleId "newRoleId2" ]+ , updatePersonLicenses = Just $ [ LicenseId "newLicenseId1", LicenseId "newLicenseId2" ]+ }++ it "streamPersonList streams Person" $ do+ let testData = personList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (PersonList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: PersonFilter)+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/people"+ queryString receivedReq `shouldBe` []++ it "streamPersonList passes query strings build from PersonFilter to server" $ do+ let testData = personList ['Z']+ personFilter = PersonFilter (Just $ Email "person@filter.com")+ (Just $ DisplayName "DisplayNameFilter")+ (Just $ OrganizationId "OrgIdFilter")++ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (PersonList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest personFilter+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/people"+ (sort . queryString) receivedReq `shouldBe` sort [ ("orgId", Just "OrgIdFilter")+ , ("displayName", Just "DisplayNameFilter")+ , ("email", Just "person@filter.com") ]++ it "streamPersonList streams Person with automatic pagination" $ do+ withMockServer (paginationApp $ encode . PersonList <$> personListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: PersonFilter)+ res `shouldBe` concat personListList++ it "streamPersonList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . PersonList <$> personListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: PersonFilter)+ res `shouldBe` concat (take 2 personListList)++ describe "Team" $ do+ let teamJson = "{\+ \ \"id\" : \"Y2lzY29zcGFyazovL3VzL1RFQU0vMTNlMThmNDAtNDJmYy0xMWU2LWE5ZDgtMjExYTBkYzc5NzY5\",\+ \ \"name\" : \"Build Squad\",\+ \ \"creatorId\" : \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY\",\+ \ \"created\" : \"2015-10-18T14:26:16+00:00\"\+ \}"+ team = Team { teamId = TeamId "Y2lzY29zcGFyazovL3VzL1RFQU0vMTNlMThmNDAtNDJmYy0xMWU2LWE5ZDgtMjExYTBkYzc5NzY5"+ , teamErrors = Nothing+ , teamName = Just $ TeamName "Build Squad"+ , teamCreatorId = Just $ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY"+ , teamCreated = Just $ Timestamp "2015-10-18T14:26:16+00:00"+ }+ newTeam = CreateTeam $ TeamName "Build Squad"+ updateTeam = UpdateTeam $ TeamName "updatedTeamName"++ it "streamTeamList streams Team" $ do+ let testData = teamList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (TeamList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamTeamList dummyAuth mockBaseRequest+ res `shouldBe` testData+ path <- rawPathInfo <$> takeMVar receivedReqMVar+ path `shouldBe` "/v1/teams"++ it "streamTeamList streams Team with automatic pagination" $ do+ withMockServer (paginationApp $ encode . TeamList <$> teamListList) $ do+ res <- runEffect . P.toListM $ streamTeamList dummyAuth mockBaseRequest+ res `shouldBe` concat teamListList++ it "streamTeamList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . TeamList <$> teamListList) $ do+ res <- runEffect . P.toListM $ streamTeamList dummyAuth mockBaseRequest+ res `shouldBe` concat (take 2 teamListList)++ describe "TeamMemberShip" $ do+ let teamMembershipJson = "{\+ \ \"id\" : \"Y2lzY29zcGFyazovL3VzL1RFQU1fTUVNQkVSU0hJUC8wZmNmYTJiOC1hZGNjLTQ1ZWEtYTc4Mi1lNDYwNTkyZjgxZWY6MTNlMThmNDAtNDJmYy0xMWU2LWE5ZDgtMjExYTBkYzc5NzY5\",\+ \ \"teamId\" : \"Y2lzY29zcGFyazovL3VzL1RFQU0vMTNlMThmNDAtNDJmYy0xMWU2LWE5ZDgtMjExYTBkYzc5NzY5\",\+ \ \"personId\" : \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY\",\+ \ \"personEmail\" : \"john.andersen@example.com\",\+ \ \"personDisplayName\" : \"John Andersen\",\+ \ \"personOrgId\" : \"Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi85NmFiYzJhYS0zZGNjLTExZTUtYTE1Mi1mZTM0ODE5Y2RjOWE\",\+ \ \"isModerator\" : true,\+ \ \"created\" : \"2015-10-18T14:26:16.057Z\"\+ \}"+ teamMembership = TeamMembership { teamMembershipId = TeamMembershipId "Y2lzY29zcGFyazovL3VzL1RFQU1fTUVNQkVSU0hJUC8wZmNmYTJiOC1hZGNjLTQ1ZWEtYTc4Mi1lNDYwNTkyZjgxZWY6MTNlMThmNDAtNDJmYy0xMWU2LWE5ZDgtMjExYTBkYzc5NzY5"+ , teamMembershipErrors = Nothing+ , teamMembershipTeamId = Just $ TeamId "Y2lzY29zcGFyazovL3VzL1RFQU0vMTNlMThmNDAtNDJmYy0xMWU2LWE5ZDgtMjExYTBkYzc5NzY5"+ , teamMembershipPersonId = Just $ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY"+ , teamMembershipPersonEmail = Just $ Email "john.andersen@example.com"+ , teamMembershipPersonDisplayName = Just $ DisplayName "John Andersen"+ , teamMembershipPersonOrgId = Just $ OrganizationId "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi85NmFiYzJhYS0zZGNjLTExZTUtYTE1Mi1mZTM0ODE5Y2RjOWE"+ , teamMembershipIsModerator = Just True+ , teamMembershipCreated = Just $ Timestamp "2015-10-18T14:26:16.057Z"+ }+ teamMembershipGen i = TeamMembership { teamMembershipId = TeamMembershipId . pack $ "teamMembershipId" <> i+ , teamMembershipErrors = Nothing+ , teamMembershipTeamId = Just . TeamId . pack $ "teamId" <> i+ , teamMembershipPersonId = Just . PersonId . pack $ "personId" <> i+ , teamMembershipPersonEmail = Just . Email . pack $ "email" <> i <> "@example.com"+ , teamMembershipPersonDisplayName = Just . DisplayName . pack $ "DisplayName" <> i+ , teamMembershipPersonOrgId = Just . OrganizationId . pack $ "OrganizationId" <> i+ , teamMembershipIsModerator = Just True+ , teamMembershipCreated = Just . Timestamp . pack $ "Timestamp" <> i+ }+ teamMembershipList j = [ teamMembershipGen $ j <> show i | i <- [1..3] ]+ teamMembershipListList = [ teamMembershipList [c] | c <- ['a'..'d'] ]+ defFilter = defaultTeamMembershipFilter $ TeamId "dummyTeamId"+ newTeamMembership = CreateTeamMembership { createTeamMembershipTeamId = TeamId "targetTeam"+ , createTeamMembershipPersonId = Just $ PersonId "addedPerson"+ , createTeamMembershipPersonEmail = Just $ Email "added@example.com"+ , createTeamMembershipIsModerator = Just True+ }+ updateTeamMembership = UpdateTeamMembership False++ it "streamTeamMembershipList streams TeamMembership" $ do+ let testData = teamMembershipList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (TeamMembershipList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest defFilter+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/team/memberships"+ queryString receivedReq `shouldBe` [ ("teamId", Just "dummyTeamId") ]++ it "streamMembershipList passes query strings build from TeamMembershipFilter to server" $ do+ let testData = teamMembershipList ['Z']+ teamMembershipFilter = TeamMembershipFilter $ TeamId "DummyTeamId"++ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (TeamMembershipList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest teamMembershipFilter+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/team/memberships"+ queryString receivedReq `shouldBe` [ ("teamId", Just "DummyTeamId") ]++ it "streamTeamMembershipList streams TeamMembership with automatic pagination" $ do+ withMockServer (paginationApp $ encode . TeamMembershipList <$> teamMembershipListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest defFilter+ res `shouldBe` concat teamMembershipListList++ it "streamTeamMembershipList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . TeamMembershipList <$> teamMembershipListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest defFilter+ res `shouldBe` concat (take 2 teamMembershipListList)++ describe "Room" $ do+ let roomJson = "{\+ \ \"id\" : \"Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0\",\+ \ \"title\" : \"Project Unicorn - Sprint 0\",\+ \ \"type\" : \"group\",\+ \ \"isLocked\" : true,\+ \ \"sipAddress\" : \"01234567890@meet.ciscospark.com\",\+ \ \"teamId\" : \"Y2lzY29zcGFyazovL3VzL1JPT00vNjRlNDVhZTAtYzQ2Yi0xMWU1LTlkZjktMGQ0MWUzNDIxOTcz\",\+ \ \"lastActivity\" : \"2016-04-21T19:12:48.920Z\",\+ \ \"creatorId\": \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY\",\+ \ \"created\" : \"2016-04-21T19:01:55.966Z\"\+ \}"+ room = Room { roomId = RoomId "Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0"+ , roomErrors = Nothing+ , roomTitle = Just $ RoomTitle "Project Unicorn - Sprint 0"+ , roomType = Just RoomTypeGroup+ , roomIsLocked = Just True+ , roomSipAddress = Just $ SipAddr "01234567890@meet.ciscospark.com"+ , roomLastActivity = Just $ Timestamp "2016-04-21T19:12:48.920Z"+ , roomTeamId = Just $ TeamId "Y2lzY29zcGFyazovL3VzL1JPT00vNjRlNDVhZTAtYzQ2Yi0xMWU1LTlkZjktMGQ0MWUzNDIxOTcz"+ , roomCreatorId = Just $ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY"+ , roomCreated = Just $ Timestamp "2016-04-21T19:01:55.966Z"+ }+ roomGen i = Room { roomId = RoomId . pack $ "roomId" <> i+ , roomErrors = Nothing+ , roomTitle = Just . RoomTitle . pack $ "roomTitle" <> i+ , roomType = Just RoomTypeGroup+ , roomIsLocked = Just True+ , roomSipAddress = Just $ SipAddr . pack $ "rooomSipAddress" <> i <> "@meet.ciscospark.com"+ , roomLastActivity = Just . Timestamp . pack $ "roomLastActivity" <> i+ , roomTeamId = Just $ TeamId . pack $ "roomTeamId" <> i+ , roomCreatorId = Just . PersonId . pack $ "personId" <> i+ , roomCreated = Just .Timestamp .pack $ "roomCreated" <> i+ }+ roomList j = [ roomGen $ j <> show i | i <- [1..3] ]+ roomListList = [ roomList [c] | c <- ['a'..'d'] ]+ newRoom = CreateRoom (RoomTitle "New Room") (Just $ TeamId "belongingTeam")+ updateRoom = UpdateRoom $ RoomTitle "updatedRoomTitle"++ it "streamRoomList streams Room" $ do+ let testData = roomList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (RoomList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: RoomFilter)+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/rooms"+ queryString receivedReq `shouldBe` []++ it "streamRoomList passes query strings build from RoomFilter to server" $ do+ let testData = roomList ['Z']+ roomFilter = RoomFilter (Just $ TeamId "dummyTeamId")+ (Just RoomTypeGroup)+ (Just RoomFilterSortByLastActivity)++ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (RoomList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest roomFilter+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/rooms"+ (sort . queryString) receivedReq `shouldBe` sort [ ("type", Just "group")+ , ("sortBy", Just "lastactivity")+ , ("teamId", Just "dummyTeamId") ]++ it "streamRoomList streams Room with automatic pagination" $ do+ withMockServer (paginationApp $ encode . RoomList <$> roomListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: RoomFilter)+ res `shouldBe` concat roomListList++ it "streamRoomList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . RoomList <$> roomListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: RoomFilter)+ res `shouldBe` concat (take 2 roomListList)++ describe "Membership" $ do+ let membershipJson = "{\+ \ \"id\" : \"Y2lzY29zcGFyazovL3VzL01FTUJFUlNISVAvMGQwYzkxYjYtY2U2MC00NzI1LWI2ZDAtMzQ1NWQ1ZDExZWYzOmNkZTFkZDQwLTJmMGQtMTFlNS1iYTljLTdiNjU1NmQyMjA3Yg\",\+ \ \"roomId\" : \"Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0\",\+ \ \"personId\" : \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY\",\+ \ \"personEmail\" : \"john.andersen@example.com\",\+ \ \"personDisplayName\" : \"John Andersen\",\+ \ \"personOrgId\" : \"Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi85NmFiYzJhYS0zZGNjLTExZTUtYTE1Mi1mZTM0ODE5Y2RjOWE\",\+ \ \"isModerator\" : true,\+ \ \"isMonitor\" : true,\+ \ \"created\" : \"2015-10-18T14:26:16.203Z\"\+ \}"+ membership = Membership { membershipId = MembershipId "Y2lzY29zcGFyazovL3VzL01FTUJFUlNISVAvMGQwYzkxYjYtY2U2MC00NzI1LWI2ZDAtMzQ1NWQ1ZDExZWYzOmNkZTFkZDQwLTJmMGQtMTFlNS1iYTljLTdiNjU1NmQyMjA3Yg"+ , membershipErrors = Nothing+ , membershipRoomId = Just $ RoomId "Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0"+ , membershipPersonId = Just $ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY"+ , membershipPersonEmail = Just $ Email "john.andersen@example.com"+ , membershipPersonDisplayName = Just $ DisplayName "John Andersen"+ , membershipPersonOrgId = Just $ OrganizationId "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi85NmFiYzJhYS0zZGNjLTExZTUtYTE1Mi1mZTM0ODE5Y2RjOWE"+ , membershipIsModerator = Just True+ , membershipIsMonitor = Just True+ , membershipCreated = Just $ Timestamp "2015-10-18T14:26:16.203Z"+ }+ membershipGen i = Membership { membershipId = MembershipId . pack $ "membershipId" <> i+ , membershipErrors = Nothing+ , membershipRoomId = Just . RoomId . pack $ "roomId" <> i+ , membershipPersonId = Just . PersonId . pack $ "personId" <> i+ , membershipPersonEmail = Just . Email . pack $ "email" <> i <> "@example.com"+ , membershipPersonDisplayName = Just . DisplayName . pack $ "displayName" <> i+ , membershipPersonOrgId = Just . OrganizationId . pack $ "orgId" <> i+ , membershipIsModerator = Just True+ , membershipIsMonitor = Just True+ , membershipCreated = Just . Timestamp . pack $ "timestamp" <> i+ }+ membershipList j = [ membershipGen $ j <> show i | i <- [1..3] ]+ membershipListList = [ membershipList [c] | c <- ['a'..'d'] ]+ newMembership = CreateMembership { createMembershipRoomId = RoomId "Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0"+ , createMembershipPersonId = Just $ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY"+ , createMembershipPersonEmail = Just $ Email "john.andersen@example.com"+ , createMembershipIsModerator = Just $ True+ }+ updateMembership = UpdateMembership False++ it "streamMembershipList streams Membership" $ do+ let testData = membershipList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (MembershipList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: MembershipFilter)+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/memberships"+ queryString receivedReq `shouldBe` []++ it "streamMembershipList passes query strings build from MembershipFilter to server" $ do+ let testData = membershipList ['Z']+ membershipFilter = MembershipFilter (Just $ RoomId "dummyRoomId")+ (Just $ PersonId "personIdFilter")+ (Just $ Email "personEmailFilter")++ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (MembershipList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest membershipFilter+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/memberships"+ (sort . queryString) receivedReq `shouldBe` sort [ ("personId", Just "personIdFilter")+ , ("personEmail", Just "personEmailFilter")+ , ("roomId", Just "dummyRoomId") ]++ it "streamMembershipList streams Membership with automatic pagination" $ do+ withMockServer (paginationApp $ encode . MembershipList <$> membershipListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: MembershipFilter)+ res `shouldBe` concat membershipListList++ it "streamMembershipList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . MembershipList <$> membershipListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: MembershipFilter)+ res `shouldBe` concat (take 2 membershipListList)++ describe "Message" $ do+ let messageJson = "{\+ \ \"id\" : \"Y2lzY29zcGFyazovL3VzL01FU1NBR0UvOTJkYjNiZTAtNDNiZC0xMWU2LThhZTktZGQ1YjNkZmM1NjVk\",\+ \ \"roomId\" : \"Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0\",\+ \ \"roomType\" : \"group\",\+ \ \"toPersonId\" : \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX\",\+ \ \"toPersonEmail\" : \"julie@example.com\",\+ \ \"text\" : \"PROJECT UPDATE - A new project plan has been published on Box: http://box.com/s/lf5vj. The PM for this project is Mike C. and the Engineering Manager is Jane W.\",\+ \ \"html\" : \"<h1>HTML formatted message goes here</h1><p>when the message was posted in markdown format.</p>\",\+ \ \"files\" : [ \"http://www.example.com/images/media.png\" ],\+ \ \"personId\" : \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY\",\+ \ \"personEmail\" : \"matt@example.com\",\+ \ \"created\" : \"2015-10-18T14:26:16+00:00\",\+ \ \"mentionedPeople\" : [ \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS8yNDlmNzRkOS1kYjhhLTQzY2EtODk2Yi04NzllZDI0MGFjNTM\", \"Y2lzY29zcGFyazovL3VzL1BFT1BMRS83YWYyZjcyYy0xZDk1LTQxZjAtYTcxNi00MjlmZmNmYmM0ZDg\" ]\+ \}"+ message = Message { messageId = MessageId "Y2lzY29zcGFyazovL3VzL01FU1NBR0UvOTJkYjNiZTAtNDNiZC0xMWU2LThhZTktZGQ1YjNkZmM1NjVk"+ , messageErrors = Nothing+ , messageRoomId = Just $ RoomId "Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0"+ , messageRoomType = Just $ RoomTypeGroup+ , messageToPersonId = Just $ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX"+ , messageToPersonEmail = Just $ Email "julie@example.com"+ , messageText = Just $ MessageText "PROJECT UPDATE - A new project plan has been published on Box: http://box.com/s/lf5vj. The PM for this project is Mike C. and the Engineering Manager is Jane W."+ , messageHtml = Just $ MessageHtml "<h1>HTML formatted message goes here</h1><p>when the message was posted in markdown format.</p>"+ , messageFiles = Just [ FileUrl "http://www.example.com/images/media.png" ]+ , messagePersonId = Just $ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY"+ , messagePersonEmail = Just $ Email "matt@example.com"+ , messageCreated = Just $ Timestamp "2015-10-18T14:26:16+00:00"+ , messageMentionedPeople = Just [ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS8yNDlmNzRkOS1kYjhhLTQzY2EtODk2Yi04NzllZDI0MGFjNTM"+ , PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS83YWYyZjcyYy0xZDk1LTQxZjAtYTcxNi00MjlmZmNmYmM0ZDg" ]+ }+ messageGen i = Message { messageId = MessageId . pack $ "messageId" <> i+ , messageErrors = Nothing+ , messageRoomId = Just . RoomId . pack $ "roomId" <> i+ , messageRoomType = Just RoomTypeGroup+ , messageToPersonId = Just . PersonId . pack $ "toPersonId" <> i+ , messageToPersonEmail = Just . Email . pack $ "julie" <> i <> "@example.com"+ , messageText = Just . MessageText . pack $ "messageText" <> i+ , messageHtml = Just . MessageHtml . pack $ "messageHtml" <> i+ , messageFiles = Just [ FileUrl . pack $ "http://www.example.com/images/media" <> i <> ".png" ]+ , messagePersonId = Just . PersonId . pack $ "personId" <> i+ , messagePersonEmail = Just . Email . pack $ "matt" <> i <> "@example.com"+ , messageCreated = Just . Timestamp . pack $ "created" <> i+ , messageMentionedPeople = Just [ PersonId . pack $ "memtionedPeople1-" <> i+ , PersonId . pack $ "memtionedPeople2-" <> i ]+ }+ messageList j = [ messageGen $ j <> show i | i <- [1..3] ]+ messageListList = [ messageList [c] | c <- ['a'..'d'] ]+ defFilter = defaultMessageFilter $ RoomId "dummyRoomId"+ newMessage = CreateMessage { createMessageRoomId = Just $ RoomId "Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0"+ , createMessageToPersonId = Just $ PersonId "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mMDZkNzFhNS0wODMzLTRmYTUtYTcyYS1jYzg5YjI1ZWVlMmX"+ , createMessageToPersonEmail = Just $ Email "julie@example.com"+ , createMessageText = Just $ MessageText "PROJECT UPDATE - A new project plan has been published on Box: http://box.com/s/lf5vj. The PM for this project is Mike C. and the Engineering Manager is Jane W."+ , createMessageMarkdown = Just $ MessageMarkdown "**PROJECT UPDATE** A new project plan has been published [on Box](http://box.com/s/lf5vj). The PM for this project is <@personEmail:mike@example.com> and the Engineering Manager is <@personEmail:jane@example.com>."+ , createMessageFiles = Just $ [ FileUrl "http://www.example.com/images/media.png" ]+ }++ it "streamMessageList streams Message" $ do+ let testData = messageList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (MessageList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest defFilter+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/messages"+ queryString receivedReq `shouldBe` [ ("roomId", Just "dummyRoomId") ]++ it "streamMessageList passes query strings build from MessageFilter to server" $ do+ let testData = messageList ['Z']+ messageFilter = MessageFilter (RoomId "dummyRoomId")+ (Just $ MentionedPeopleMe)+ (Just $ Timestamp "beforeFilter")+ (Just $ MessageId "beforeMessageFilter")++ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (MessageList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest messageFilter+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/messages"+ (sort . queryString) receivedReq `shouldBe` sort [ ("roomId", Just "dummyRoomId")+ , ("mentionedPeople", Just "me")+ , ("beforeMessage", Just "beforeMessageFilter")+ , ("before", Just "beforeFilter") ]++ it "streamMessageList streams Message with automatic pagination" $ do+ withMockServer (paginationApp $ encode . MessageList <$> messageListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest defFilter+ res `shouldBe` concat messageListList++ it "streamMessageList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . MessageList <$> messageListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest defFilter+ res `shouldBe` concat (take 2 messageListList)++ describe "Organization" $ do+ let organizationJson = "{\+ \ \"id\" : \"OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh\",\+ \ \"displayName\" : \"Cisco, Inc.\",\+ \ \"created\" : \"2015-10-18T14:26:16+00:00\"\+ \}"+ organization = Organization { organizationId = OrganizationId "OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh"+ , organizationErrors = Nothing+ , organizationDisplayName = Just $ OrganizationDisplayName "Cisco, Inc."+ , organizationCreated = Just $ Timestamp "2015-10-18T14:26:16+00:00"+ }+ organizationGen i = Organization { organizationId = OrganizationId . pack $ "organizationId" <> i+ , organizationErrors = Nothing+ , organizationDisplayName = Just $ OrganizationDisplayName . pack $ "displayName" <> i+ , organizationCreated = Just $ Timestamp . pack $ "timestamp" <> i+ }+ organizationList j = [ organizationGen $ j <> show i | i <- [1..3] ]+ organizationListList = [ organizationList [c] | c <- ['a'..'d'] ]++ it "streamOrganizationList streams Organization" $ do+ let testData = organizationList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (OrganizationList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamOrganizationList dummyAuth mockBaseRequest+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/organizations"+ queryString receivedReq `shouldBe` []++ it "streamOrganizationList streams Organization with automatic pagination" $ do+ withMockServer (paginationApp $ encode . OrganizationList <$> organizationListList) $ do+ res <- runEffect . P.toListM $ streamOrganizationList dummyAuth mockBaseRequest+ res `shouldBe` concat organizationListList++ it "streamOrganizationList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . OrganizationList <$> organizationListList) $ do+ res <- runEffect . P.toListM $ streamOrganizationList dummyAuth mockBaseRequest+ res `shouldBe` concat (take 2 organizationListList)++ describe "License" $ do+ let licenseJson = "{\+ \ \"id\" : \"OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh\",\+ \ \"name\" : \"Spark Calling\",\+ \ \"totalUnits\" : 42,\+ \ \"consumedUnits\" : 8\+ \}"+ license = License { licenseId = LicenseId "OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh"+ , licenseErrors = Nothing+ , licenseName = Just $ LicenseName "Spark Calling"+ , licenseTotalUnits = Just $ LicenseUnit 42+ , licenseConsumedUnits = Just $ LicenseUnit 8+ }+ licenseGen i = License { licenseId = LicenseId . pack $ "licenseId" <> i+ , licenseErrors = Nothing+ , licenseName = Just $ LicenseName . pack $ "licenseName" <> i+ , licenseTotalUnits = Just $ LicenseUnit 42+ , licenseConsumedUnits = Just $ LicenseUnit 8+ }+ licenseList j = [ licenseGen $ j <> show i | i <- [1..3] ]+ licenseListList = [ licenseList [c] | c <- ['a'..'d'] ]++ it "streamLicenseList streams License" $ do+ let testData = licenseList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (LicenseList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: LicenseFilter)+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/licenses"+ queryString receivedReq `shouldBe` []++ it "streamLicenseList passes query strings build from LicenseFilter to server" $ do+ let testData = licenseList ['Z']+ licenseFilter = LicenseFilter (Just $ OrganizationId "orgIdFilter")++ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (LicenseList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest licenseFilter+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/licenses"+ queryString receivedReq `shouldBe` [ ("orgId", Just "orgIdFilter") ]++ it "streamLicenseList streams License with automatic pagination" $ do+ withMockServer (paginationApp $ encode . LicenseList <$> licenseListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: LicenseFilter)+ res `shouldBe` concat licenseListList++ it "streamLicenseList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . LicenseList <$> licenseListList) $ do+ res <- runEffect . P.toListM $ streamListWithFilter dummyAuth mockBaseRequest (def :: LicenseFilter)+ res `shouldBe` concat (take 2 licenseListList)++ describe "Role" $ do+ let roleJson = "{\+ \ \"id\" : \"OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh\",\+ \ \"name\" : \"Full Administrator\"\+ \}"+ role = Role { roleId = RoleId "OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh"+ , roleErrors = Nothing+ , roleName = Just $ RoleName "Full Administrator"+ }+ roleGen i = Role { roleId = RoleId . pack $ "roleId" <> i+ , roleErrors = Nothing+ , roleName = Just . RoleName . pack $ "roleName" <> i+ }+ roleList j = [ roleGen $ j <> show i | i <- [1..3] ]+ roleListList = [ roleList [c] | c <- ['a'..'d'] ]++ it "streamRoleList streams Role" $ do+ let testData = roleList ['Z']+ receivedReqMVar <- newEmptyMVar++ withMockServer (\req respond -> do+ putMVar receivedReqMVar req+ simpleApp (encode (RoleList testData)) req respond+ ) $ do+ res <- runEffect . P.toListM $ streamRoleList dummyAuth mockBaseRequest+ res `shouldBe` testData++ receivedReq <- takeMVar receivedReqMVar+ rawPathInfo receivedReq `shouldBe` "/v1/roles"+ queryString receivedReq `shouldBe` []++ it "streamRoleList streams Role with automatic pagination" $ do+ withMockServer (paginationApp $ encode . RoleList <$> roleListList) $ do+ res <- runEffect . P.toListM $ streamRoleList dummyAuth mockBaseRequest+ res `shouldBe` concat roleListList++ it "streamRoleList stops pagination at invalid Link Header" $ do+ withMockServer (invalidPaginationApp $ encode . RoleList <$> roleListList) $ do+ res <- runEffect . P.toListM $ streamRoleList dummyAuth mockBaseRequest+ res `shouldBe` concat (take 2 roleListList)
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ webex-teams-pipes.cabal view
@@ -0,0 +1,87 @@+-- This file has been generated from package.yaml by hpack version 0.28.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: cdafad5854c98fccbdc2c3e2432e95eda012e8291e85f775cc481ff56ffba9b9++name: webex-teams-pipes+version: 0.2.0.0+synopsis: Pipes wrapper of Webex Teams List API+description: Please see the README on Github at <https://github.com/nshimaza/webex-teams-api#readme>+category: Web+homepage: https://github.com/nshimaza/webex-teams-api#readme+bug-reports: https://github.com/nshimaza/webex-teams-api/issues+author: Naoto Shimazaki+maintainer: Naoto.Shimazaki@gmail.com+copyright: 2017,2018 Naoto Shimazaki+license: MIT+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10+extra-source-files:+ ChangeLog.md+ README.md++source-repository head+ type: git+ location: https://github.com/nshimaza/webex-teams-api++library+ exposed-modules:+ Network.WebexTeams.Pipes+ other-modules:+ Paths_webex_teams_pipes+ hs-source-dirs:+ src+ build-depends:+ base >=4.7 && <5+ , pipes+ , webex-teams-api+ default-language: Haskell2010++executable webex-teams-pipes-exe+ main-is: Main.hs+ other-modules:+ Paths_webex_teams_pipes+ hs-source-dirs:+ app+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.7 && <5+ , bytestring+ , data-default+ , http-client+ , optparse-applicative+ , pipes+ , text+ , utf8-string+ , webex-teams-api+ , webex-teams-pipes+ default-language: Haskell2010++test-suite webex-teams-conduit-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Network.WebexTeams.PipesSpec+ Paths_webex_teams_pipes+ hs-source-dirs:+ test+ ghc-options: -rtsopts+ build-depends:+ aeson+ , async+ , base >=4.7 && <5+ , bytestring+ , data-default+ , hspec+ , http-conduit+ , http-types+ , network-uri+ , pipes+ , text+ , wai+ , warp+ , webex-teams-api+ , webex-teams-pipes+ default-language: Haskell2010