packages feed

grapesy-1.2.1: test-grapesy/Test/Meta/FrameLevelServer.hs

{-# OPTIONS_GHC -Wno-orphans #-}

module Test.Meta.FrameLevelServer (tests) where

import Data.ByteString.Lazy qualified as BS.Lazy
import Data.ByteString.Lazy qualified as Lazy (ByteString)
import Data.ByteString.Lazy.Char8 qualified as BS.Lazy.Char8
import Data.Word
import Test.Tasty
import Test.Tasty.HUnit

import Network.GRPC.Client qualified as Client
import Network.GRPC.Common
import Network.GRPC.Common.Binary

import Test.Util.FrameLevelServer (Script, Frame(..), FrameHeader(..))
import Test.Util.FrameLevelServer qualified as Frame

{-------------------------------------------------------------------------------
  List of tests
-------------------------------------------------------------------------------}

tests :: TestTree
tests = testGroup "Test.Meta.FrameLevelServer" [
      testCase "trailersOnly" test_trailersOnly
    , testCase "echo"         test_echo
    ]

{-------------------------------------------------------------------------------
  Tests proper
-------------------------------------------------------------------------------}

-- | Simplest exchange: the response is a single Trailers-Only frame
test_trailersOnly :: Assertion
test_trailersOnly =
    Frame.withScript Nothing trailersOnlyScript $ \server getHandlerResults -> do
      _trailers <-
        Client.withConnection def server $ \conn ->
          Client.withRPC conn def (Proxy @TestRpc) $ \call -> do
            Client.sendFinalInput call BS.Lazy.empty
            Client.recvTrailers call
      results <- getHandlerResults
      assertEqual "handler results" [()] results

-- | Full response (headers, message, trailers), echoing the request message
test_echo :: Assertion
test_echo =
    Frame.withScript Nothing echoScript $ \server getHandlerResults -> do
      (output, _trailers) <-
        Client.withConnection def server $ \conn ->
          Client.withRPC conn def (Proxy @TestRpc) $ \call -> do
            Client.sendFinalInput call (ascii "ping")
            Client.recvFinalOutput call
      assertEqual "output" (ascii "ping") output
      results <- getHandlerResults
      assertEqual "handler results" [()] results

{-------------------------------------------------------------------------------
  Test RPC
-------------------------------------------------------------------------------}

type TestRpc = RawRpc "FrameLevelServer" "ping"

type instance RequestMetadata          TestRpc = [CustomMetadata]
type instance ResponseInitialMetadata  TestRpc = [CustomMetadata]
type instance ResponseTrailingMetadata TestRpc = [CustomMetadata]

{-------------------------------------------------------------------------------
  Scripts
-------------------------------------------------------------------------------}

-- | Receive the request, respond with a single Trailers-Only frame
trailersOnlyScript :: Script ()
trailersOnlyScript = do
    Frame.handshake
    recvRequestHeaders
    _msg <- Frame.recvUntilEndStream 1
    Frame.send $ Frame.mkFrame 0x1 0x5 1 trailersOnly  -- END_STREAM | END_HEADERS

-- | Receive the request, respond with headers, the same message, and trailers
--
-- The gRPC length-prefixed message format is the same in both directions, so
-- the request message can be sent back verbatim. This relies on the request
-- being uncompressed; we never advertise @grpc-accept-encoding@, so the client
-- has no reason to compress.
echoScript :: Script ()
echoScript = do
    Frame.handshake
    recvRequestHeaders
    msg <- Frame.recvUntilEndStream 1
    Frame.send $ Frame.mkFrame 0x1 0x4 1 responseHeaders   -- END_HEADERS
    Frame.send $ Frame.mkFrame 0x0 0x0 1 msg
    Frame.send $ Frame.mkFrame 0x1 0x5 1 responseTrailers  -- END_STREAM | END_HEADERS

-- | Request headers on stream 1 (not decoded)
recvRequestHeaders :: Script ()
recvRequestHeaders = Frame.recv $ \frame ->
    case frameHeader frame of
      FrameHeader{frameType = 0x1, frameStreamId = 1} -> Right ()
      _otherwise -> Left $ "Expected HEADERS on stream 1, got " ++ show frame

{-------------------------------------------------------------------------------
  Header blocks

  Only static-table references and literals without indexing, so none of these
  responses changes the client's dynamic table.
-------------------------------------------------------------------------------}

-- | Trailers-Only: status, content-type and trailers in a single block
trailersOnly :: Lazy.ByteString
trailersOnly = responseHeaders <> responseTrailers

responseHeaders :: Lazy.ByteString
responseHeaders = mconcat [
      bytes [0x88]                                  -- :status 200 (static index 8)
    , bytes [0x00, 0x0c], ascii "content-type"      -- literal, no indexing, new name
    , bytes [0x14],       ascii "application/grpc+raw"
    ]

responseTrailers :: Lazy.ByteString
responseTrailers = mconcat [
      bytes [0x00, 0x0b], ascii "grpc-status"       -- literal, no indexing, new name
    , bytes [0x01],       ascii "0"
    ]

{-------------------------------------------------------------------------------
  Internal auxiliary
-------------------------------------------------------------------------------}

bytes :: [Word8] -> Lazy.ByteString
bytes = BS.Lazy.pack

ascii :: String -> Lazy.ByteString
ascii = BS.Lazy.Char8.pack