packages feed

grapesy-1.0.0: test-grapesy/Test/Regression/Issue238.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Tests for <https://github.com/well-typed/grapesy/issues/238>
module Test.Regression.Issue238 (tests) where

import Control.Exception
import Data.ByteString.Lazy (ByteString)
import Data.Text qualified as Text
import Test.Tasty
import Test.Tasty.HUnit

import Network.GRPC.Client (rpc)
import Network.GRPC.Client qualified as Client
import Network.GRPC.Client.StreamType.IO qualified as Client
import Network.GRPC.Common
import Network.GRPC.Common.Protobuf
import Network.GRPC.Server qualified as Server
import Network.GRPC.Server.Protobuf qualified as Server
import Network.GRPC.Server.Run qualified as Server
import Network.GRPC.Server.StreamType qualified as Server

import Proto.API.RouteGuide
import Proto.API.Trivial

{-------------------------------------------------------------------------------
  Top-level
-------------------------------------------------------------------------------}

tests :: TestTree
tests = testGroup "Issue238" [
      testGroup "Trivial" [
          testCase "nonStreaming1" test_trivial_nonStreaming1
        , testCase "nonStreaming2" test_trivial_nonStreaming2
        ]
    , testGroup "LowLevel" [
          testCase "nonStreaming1" test_lowLevel_nonStreaming1
        , testCase "nonStreaming2" test_lowLevel_nonStreaming2
        ]
    , testGroup "RouteGuide" [
          testCase "nonStreaming1" test_routeGuide_nonStreaming1
        , testCase "nonStreaming2" test_routeGuide_nonStreaming2
        , testCase "nonStreaming3" test_routeGuide_nonStreaming3
        ]
    ]

{-------------------------------------------------------------------------------
  Without Protobuf
-------------------------------------------------------------------------------}

-- | Undefined handler body
test_trivial_nonStreaming1 :: Assertion
test_trivial_nonStreaming1 =
    testWith handlers client
  where
    handlers :: [Server.SomeRpcHandler IO]
    handlers = [Server.someRpcHandler $ Server.mkRpcHandler @Trivial undefined]

    client :: Client.Connection -> IO ByteString
    client conn = Client.nonStreaming conn (rpc @Trivial) mempty

-- | Like 'test_trivial_nonStreaming1', but without the call to @mkRpcHandler@
--
-- This matters, because 'mkRpcHandler' sets the initial metadata.
test_trivial_nonStreaming2 :: Assertion
test_trivial_nonStreaming2 =
    testWith handlers client
  where
    handlers :: [Server.SomeRpcHandler IO]
    handlers = [Server.someRpcHandler @Trivial undefined]

    client :: Client.Connection -> IO ByteString
    client conn = Client.nonStreaming conn (rpc @Trivial) mempty

{-------------------------------------------------------------------------------
  Low-level API

  The ticket specifically says "when client uses high-level API". In this
  section we therefore compare the behaviour of the @test_trivial@ tests (which
  use the high-level API) to the corresponding behaviour with the low-level API.
-------------------------------------------------------------------------------}

-- | Direct equivalent of 'test_trivial_nonStreaming2'
test_lowLevel_nonStreaming1 :: Assertion
test_lowLevel_nonStreaming1 =
    testWith handlers client
  where
    handlers :: [Server.SomeRpcHandler IO]
    handlers = [Server.someRpcHandler @Trivial undefined]

    client :: Client.Connection -> IO ByteString
    client conn =
        Client.withRPC conn def (Proxy @Trivial) $ \call -> do
          Client.sendFinalInput call mempty
          fst <$> Client.recvFinalOutput call

-- | Like 'test_lowLevel_nonStreaming1, but without sending the input
test_lowLevel_nonStreaming2 :: Assertion
test_lowLevel_nonStreaming2 =
    testWith handlers client
  where
    handlers :: [Server.SomeRpcHandler IO]
    handlers = [Server.someRpcHandler @Trivial undefined]

    client :: Client.Connection -> IO ByteString
    client conn =
        Client.withRPC conn def (Proxy @Trivial) $ \call -> do
          fst <$> Client.recvFinalOutput call

{-------------------------------------------------------------------------------
  With Protobuf

  Crucially, this uses 'fromMethods', which is not unlikely to have undefineds
  in it during development.

  The @test_routeGuide_nonStreaming<N>@ tests all use @undefined@ somewhere in
  the declaration of the methods, but differ on how much is defined.
-------------------------------------------------------------------------------}

-- | Completely @undefined@ 'Methods'
--
-- This is different from 'test_routeGuide_nonStreaming2', where the /skeleton/
-- is defined but the individual handlers are not: here we cannot even construct
-- the list without triggering an exception, forcing us to be very careful with
-- exception handling during lookup.
test_routeGuide_nonStreaming1 :: Assertion
test_routeGuide_nonStreaming1 =
    testWith (Server.fromMethods methods) client
  where
    methods :: Server.Methods IO (Server.ProtobufMethodsOf RouteGuide)
    methods = undefined

    client :: Client.Connection -> IO (Proto Feature)
    client conn = Client.nonStreaming conn (rpc @GetFeature) defMessage

test_routeGuide_nonStreaming2 :: Assertion
test_routeGuide_nonStreaming2 =
    testWith (Server.fromMethods methods) client
  where
    methods :: Server.Methods IO (Server.ProtobufMethodsOf RouteGuide)
    methods =
          Server.Method undefined
        $ Server.Method undefined
        $ Server.Method undefined
        $ Server.Method undefined
        $ Server.NoMoreMethods

    client :: Client.Connection -> IO (Proto Feature)
    client conn = Client.nonStreaming conn (rpc @GetFeature) defMessage

test_routeGuide_nonStreaming3 :: Assertion
test_routeGuide_nonStreaming3 =
    testWith (Server.fromMethods methods) client
  where
    methods :: Server.Methods IO (Server.ProtobufMethodsOf RouteGuide)
    methods =
          Server.Method (Server.mkNonStreaming undefined)
        $ Server.Method undefined
        $ Server.Method undefined
        $ Server.Method undefined
        $ Server.NoMoreMethods

    client :: Client.Connection -> IO (Proto Feature)
    client conn = Client.nonStreaming conn (rpc @GetFeature) defMessage

{-------------------------------------------------------------------------------
  Auxiliary: test setup

  We don't use the test clients/server infrastructure here, since this issue
  is about exception handling, and the test harnass does quite a bit of
  exception processing.
-------------------------------------------------------------------------------}

testWith ::
     [Server.SomeRpcHandler IO]
  -> (Client.Connection -> IO a)
  -> Assertion
testWith handlers client = do
    server <- Server.mkGrpcServer serverParams handlers
    Server.forkServer def serverConfig server $ \runningServer -> do
      serverPort <- Server.getServerPort runningServer

      let serverAddr = Client.ServerInsecure $ Client.Address {
           addressHost      = "127.0.0.1"
         , addressPort      = serverPort
         , addressAuthority = Nothing
         }
      Client.withConnection def serverAddr $ \conn ->
        checkClientReceivesUndefined $ client conn
  where
    serverConfig :: Server.ServerConfig
    serverConfig = Server.ServerConfig {
          serverInsecure = Just $ Server.InsecureConfig (Just "127.0.0.1") 0
        , serverSecure   = Nothing
        }

    serverParams :: Server.ServerParams
    serverParams = def {
          Server.serverTopLevel = \handler unmask req resp -> do
            _result :: Either SomeException () <- try $ handler unmask req resp
            -- Ignore any exceptions
            return ()
        }

-- | Verify that the client  is notified of the undefined handler
checkClientReceivesUndefined ::
     HasCallStack
  => IO a -> Assertion
checkClientReceivesUndefined k = do
    result <- try k
    case result of
      Right _ ->
        assertFailure "Unexpected successful response"
      Left err ->
        case grpcErrorMessage err of
          Just msg ->
            assertBool (show (Text.unpack msg) ++ " contains \"undefined\"") $
              "undefined" `Text.isInfixOf` msg
          Nothing ->
            assertFailure "Missing error message"