packages feed

plexus-synapse-3.10.1: test/WebSocketRaw.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Minimal WebSocket test - does exactly what websocat does
--
-- Usage: cabal run websocket-raw-test
--
-- This test replicates the exact behavior of:
--   echo '{"jsonrpc":"2.0","id":1,"method":"ping_pong","params":{"message":"test"}}' | websocat ws://127.0.0.1:4444

module Main (main) where

import qualified Data.Aeson as Aeson
import Data.Aeson (object, (.=))
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Network.WebSockets as WS
import Control.Monad (forever, void)
import Control.Concurrent (threadDelay, forkIO)
import Control.Exception (catch, SomeException)
import System.Exit (exitSuccess, exitFailure)

main :: IO ()
main = do
  TIO.putStrLn "WebSocket Raw Test - Connecting to ws://127.0.0.1:4444"

  result <- catch runTest handleError

  case result of
    True -> do
      TIO.putStrLn "\n[OK] Test PASSED"
      exitSuccess
    False -> do
      TIO.putStrLn "\n[ERR] Test FAILED"
      exitFailure

runTest :: IO Bool
runTest = WS.runClient "127.0.0.1" 4444 "/" $ \conn -> do
  TIO.putStrLn "[OK] WebSocket connection established"

  -- Send the exact JSON-RPC request that websocat sends
  let request = object
        [ "jsonrpc" .= ("2.0" :: T.Text)
        , "id" .= (1 :: Int)
        , "method" .= ("ping_pong" :: T.Text)
        , "params" .= object
            [ "message" .= ("test" :: T.Text)
            ]
        ]

  let requestBS = Aeson.encode request
  TIO.putStrLn $ "> Sending: " <> T.pack (show request)
  WS.sendTextData conn requestBS

  -- Receive messages (subscription ID, data, done)
  TIO.putStrLn "\nWaiting for responses..."

  -- Message 1: Subscription ID
  msg1 <- WS.receiveData conn
  TIO.putStrLn $ "< Received 1: " <> T.pack (show (msg1 :: LBS.ByteString))

  case Aeson.decode msg1 of
    Nothing -> do
      TIO.putStrLn "[ERR] Failed to decode message 1 as JSON"
      return False
    Just (Aeson.Object obj) -> do
      case KM.lookup "result" obj of
        Nothing -> do
          TIO.putStrLn "[ERR] Message 1 missing 'result' field (subscription ID)"
          return False
        Just (Aeson.Number subId) -> do
          TIO.putStrLn $ "[OK] Got subscription ID: " <> T.pack (show subId)

          -- Message 2: Data event
          msg2 <- WS.receiveData conn
          TIO.putStrLn $ "\n< Received 2: " <> T.pack (show (msg2 :: LBS.ByteString))

          case Aeson.decode msg2 of
            Nothing -> do
              TIO.putStrLn "[ERR] Failed to decode message 2 as JSON"
              return False
            Just (Aeson.Object obj2) -> do
              case KM.lookup "params" obj2 of
                Nothing -> do
                  TIO.putStrLn "[ERR] Message 2 missing 'params' field"
                  return False
                Just (Aeson.Object params) -> do
                  case KM.lookup "result" params of
                    Nothing -> do
                      TIO.putStrLn "[ERR] Message 2 params missing 'result' field"
                      return False
                    Just (Aeson.Object result) -> do
                      case KM.lookup "type" result of
                        Just (Aeson.String "data") -> do
                          TIO.putStrLn "[OK] Got data event"

                          -- Message 3: Done event
                          msg3 <- WS.receiveData conn
                          TIO.putStrLn $ "\n< Received 3: " <> T.pack (show (msg3 :: LBS.ByteString))

                          case Aeson.decode msg3 of
                            Nothing -> do
                              TIO.putStrLn "[ERR] Failed to decode message 3 as JSON"
                              return False
                            Just (Aeson.Object obj3) -> do
                              case KM.lookup "params" obj3 of
                                Nothing -> do
                                  TIO.putStrLn "[ERR] Message 3 missing 'params' field"
                                  return False
                                Just (Aeson.Object params3) -> do
                                  case KM.lookup "result" params3 of
                                    Nothing -> do
                                      TIO.putStrLn "[ERR] Message 3 params missing 'result' field"
                                      return False
                                    Just (Aeson.Object result3) -> do
                                      case KM.lookup "type" result3 of
                                        Just (Aeson.String "done") -> do
                                          TIO.putStrLn "[OK] Got done event"
                                          TIO.putStrLn "\n[OK] All messages received successfully"
                                          return True
                                        _ -> do
                                          TIO.putStrLn "[ERR] Message 3 type is not 'done'"
                                          return False
                                    _ -> do
                                      TIO.putStrLn "[ERR] Message 3 result is not an object"
                                      return False
                                _ -> do
                                  TIO.putStrLn "[ERR] Message 3 params is not an object"
                                  return False
                            _ -> do
                              TIO.putStrLn "[ERR] Message 3 is not an object"
                              return False
                        _ -> do
                          TIO.putStrLn "[ERR] Message 2 type is not 'data'"
                          return False
                    _ -> do
                      TIO.putStrLn "[ERR] Message 2 result is not an object"
                      return False
                _ -> do
                  TIO.putStrLn "[ERR] Message 2 params is not an object"
                  return False
            _ -> do
              TIO.putStrLn "[ERR] Message 2 is not an object"
              return False
        _ -> do
          TIO.putStrLn "[ERR] Message 1 result is not a number"
          return False
    _ -> do
      TIO.putStrLn "[ERR] Message 1 is not an object"
      return False

handleError :: SomeException -> IO Bool
handleError e = do
  TIO.putStrLn $ "[ERR] Exception: " <> T.pack (show e)
  return False