packages feed

project-m36-1.0.2: test/Server/WebSocket.hs

{-# LANGUAGE CPP, TypeApplications #-}
-- test the websocket server
import Test.HUnit
import qualified Network.WebSockets as WS
import ProjectM36.Server.WebSocket
import ProjectM36.Server.Config
import ProjectM36.Server
import ProjectM36.Client

import Network.Socket
import Control.Exception
import Control.Concurrent
import System.Exit
import Data.Typeable
import Data.Text hiding (map)
import Data.Aeson
import qualified Data.ByteString.Lazy as BS
import ProjectM36.Relation
import Control.Monad (void)
import Data.UUID.V4 (nextRandom)

--start the websocket server
-- run some tutoriald against it

launchTestServer :: IO (PortNumber, DatabaseName)
launchTestServer = do
  addressMVar <- newEmptyMVar
  let config = defaultServerConfig { databaseName = testDatabaseName, 
                                     bindPort = 0,
                                     checkFS = False}
      testDatabaseName = "test"
  -- start normal server
  void $ forkIO (void $ launchServer config (Just addressMVar))
  (SockAddrInet dbPort _) <- takeMVar addressMVar
  let wsServerHost = "127.0.0.1"
      wsServerPort = 8889
      dbHost = "127.0.0.1"
  -- start websocket server proxy -- runServer doesn't support returning an arbitrary socket
  _ <- forkIO (WS.runServer wsServerHost wsServerPort (websocketProxyServer (fromIntegral dbPort) dbHost))
  --wait for socket to be listening
  waitForListenSocket 5 (fromIntegral wsServerPort)
  pure (fromIntegral wsServerPort, testDatabaseName)
  
data TestException = WaitForSocketListenException
                   deriving (Show, Typeable)
                            
instance Exception TestException                            

waitForListenSocket :: Int -> PortNumber -> IO ()  
waitForListenSocket secondsToTry port = 
  if secondsToTry <= 0 then
    throw WaitForSocketListenException
    else do
    --hostaddr <- inet_addr "127.0.0.1"
    hostaddr:_ <- getAddrInfo Nothing (Just "127.0.0.1") (Just (show port))
    sock <- socket AF_INET Stream defaultProtocol
    let handler :: IOException -> IO ()
        handler _ = do
          threadDelay 1000000
          waitForListenSocket (secondsToTry - 1) port
    catch (connect sock (addrAddress hostaddr)) handler
  
main :: IO ()
main = do
  (port, dbname) <- launchTestServer
  tcounts <- runTestTT (testList port dbname)
  if errors tcounts + failures tcounts > 0 then exitFailure else exitSuccess

testList :: PortNumber -> DatabaseName -> Test
testList port dbName = TestList $ map (\f -> f port dbName) [testBasicConnection,
                                                             testTutorialD]
                          
basicConnection :: PortNumber -> WS.ClientApp () -> IO ()
basicConnection port = WS.runClient "127.0.0.1" (fromIntegral port) "/"

basicConnectionWithDatabase :: PortNumber -> DatabaseName -> WS.ClientApp () -> IO ()
basicConnectionWithDatabase port dbname block = basicConnection port
  (\conn -> do
      --setup connection
      let connectMsg = encode (ConnectionSetupRequest dbname)
      WS.sendTextData conn connectMsg
      setupResponse <- WS.receiveData conn
      case eitherDecode setupResponse of
        Left err -> error (show err)
        Right ConnectionSetupResponse{} -> pure ()
        Right _other -> error (show setupResponse)
      block conn)
    
testBasicConnection :: PortNumber -> DatabaseName -> Test
testBasicConnection port _ = TestCase $ basicConnection port (\conn -> WS.sendClose conn (""::Text))

testTutorialD :: PortNumber -> DatabaseName -> Test
testTutorialD port dbname = TestCase $ basicConnectionWithDatabase port dbname testtutd
  where
    discardPromptInfo conn = do
      response <- WS.receiveData conn :: IO BS.ByteString
      case eitherDecode @Response response of
        Right _promptInfo -> pure ()
        Left err -> assertFailure ("failed to decode prompt info: " ++ err ++ ": " ++ show response)
      
    testtutd conn = do
      -- create new session at master
      reqId <- RequestId <$> nextRandom
      let createSessionMsg = encode (CreateSessionAtHeadRequest reqId "master")
      WS.sendTextData conn createSessionMsg
      sessionResponse <- WS.receiveData conn :: IO BS.ByteString
      sessionId <- case eitherDecode sessionResponse of
                     Left err -> assertFailure err
                     Right (CreateSessionAtHeadResponse reqId' sid) -> do
                       assertEqual "request ID round-trip" reqId' reqId
                       pure sid
                     Right _other -> assertFailure ("expected session response but got: " <> show sessionResponse)
      -- send tutd message
      reqIdB <- RequestId <$> nextRandom
      let tutdMsg = encode (ExecuteTutorialDRequest reqIdB sessionId jsonOnlyPresentation (TutorialDText ":showexpr true"))
      WS.sendTextData conn tutdMsg
      discardPromptInfo conn
      
      --receive relation response
      response <- WS.receiveData conn :: IO BS.ByteString      
      let decoded = decode @Response response 
      case decoded of 
        Nothing -> assertFailure "failed to decode"
        Just (RelationResponse _ trueRel _) -> do
            assertEqual "round-trip true relation" trueRel relationTrue 
            WS.sendClose conn ("" :: Text)
        Just _other -> assertFailure ("unexpected response: " <> show response)