packages feed

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 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++[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)+[![Build Status](https://travis-ci.org/nshimaza/webex-teams-api.svg?branch=master)](https://travis-ci.org/nshimaza/webex-teams-api)+[![Hackage](https://img.shields.io/hackage/v/webex-teams-pipes.svg?style=flat)](https://hackage.haskell.org/package/webex-teams-pipes)+[![Stackage Nightly](http://stackage.org/package/webex-teams-pipes/badge/nightly)](http://stackage.org/nightly/package/webex-teams-pipes)+[![Stackage LTS](http://stackage.org/package/webex-teams-pipes/badge/lts)](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