packages feed

polysemy-webserver-0.2.0.0: test/Spec.hs

{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where

import Polysemy
import Polysemy.Final
import Polysemy.Error
import Polysemy.WebServer
import qualified Data.ByteString.Lazy as LBS
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.HTTP.Types.Status as HTTP
import Network.HTTP.Simple
import Polysemy.Async
import Test.Hspec
import qualified Network.WebSockets as WS
import qualified Network.WebSockets.Connection as WS
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Encoding as TE
import Network.WebSockets.Client as WSC
import Control.Monad
import Text.Read (readMaybe)
import Data.ByteString.Lazy.Char8 as LBS8
import Control.Concurrent

eitherRedistrib :: Either (Either a b) c -> Either a (Either b c)
eitherRedistrib (Left (Left a)) = Left a
eitherRedistrib (Left (Right b)) = Right (Left b)
eitherRedistrib (Right c) = Right (Right c)

testServerApp :: WebServer `Member` r =>
  Wai.Request -> PendingWebRequest -> Sem r Wai.ResponseReceived
testServerApp req pendingReq
  | Wai.pathInfo req == [] = respondWebRequest pendingReq (
  Wai.responseLBS HTTP.status200 [] "Success")
  | Wai.pathInfo req == ["ws"] = do
      maybeResp <- upgradeToWebSocketsResponse WS.defaultConnectionOptions
        testWSApp req
      let resp = maybe (Wai.responseLBS HTTP.status400 [] "Bad request")
            id maybeResp
      respondWebRequest pendingReq resp
  | otherwise = respondWebRequest pendingReq (
      Wai.responseLBS HTTP.status404 [] "Not Found")
testWSApp :: forall r. WebServer `Member` r =>
  WS.PendingConnection -> Sem r ()
testWSApp pendConn =
  void $ runError @WS.ConnectionException $
    runError @WS.HandshakeException $ do
      conn <- fromEither =<< fromEither . eitherRedistrib =<<
                acceptPendingWebSocketConnection pendConn WS.defaultAcceptRequest
      whilePingingWebSocket conn 30 (go conn)
  where
    go :: (WebServer `Member` r2,
           Error WS.ConnectionException `Member` r2) =>
          WS.Connection -> Sem r2 ()
    go conn = do
      msg <- fromEither =<< receiveWebSocketDataMessage conn
      let nextResp = case msg of
                       WS.Text bs _
                         | Just n <- readMaybe . LBS8.unpack $ bs ->
                             T.pack $ show (n + 1)
                       _ -> "Invalid message: " <> (T.pack . show $ msg)
      sendWebSocketDataMessages conn [WS.Text (LBS.fromStrict $ TE.encodeUtf8 nextResp) Nothing]
      go conn
main :: IO ()
main = runFinal . runWebServerFinal . asyncToIOFinal $ do
  -- With the WebServer effect, start a new server...
  async $ startWebServer 8123 testServerApp

  -- To do: Ideally provide a better way to know when it's ready...
  embedFinal $ threadDelay 5000000
  embedFinal . hspec $ do
    describe "Web Server" $ do
      it "should respond to normal requests" $ do
        -- Use a client library to test...
        req <- parseRequest "http://127.0.0.1:8123/"
        resp <- httpLBS req
        getResponseStatusCode resp `shouldBe` 200
        getResponseBody resp `shouldBe` "Success"
      it "should support upgrading to WebSockets" $ do
        result <- WSC.runClient "127.0.0.1" 8123 "/ws" $ \conn -> do
          WS.sendTextData conn ("42" :: T.Text)
          r1 <- WS.receiveData conn
          WS.sendTextData conn (r1 :: LBS.ByteString)
          WS.receiveData conn
        result `shouldBe` ("44" :: T.Text)