diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+## webex-teams-pipes-0.2.0.0
+
+* Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -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"
+    )
diff --git a/src/Network/WebexTeams/Pipes.hs b/src/Network/WebexTeams/Pipes.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/WebexTeams/Pipes.hs
@@ -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
diff --git a/test/Network/WebexTeams/PipesSpec.hs b/test/Network/WebexTeams/PipesSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Network/WebexTeams/PipesSpec.hs
@@ -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)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/webex-teams-pipes.cabal b/webex-teams-pipes.cabal
new file mode 100644
--- /dev/null
+++ b/webex-teams-pipes.cabal
@@ -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
