mcp-0.3.0.1: test/MCP/Integration.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{- |
Module: MCP.Integration
License: MPL-2.0
Maintainer: <matti@dpella.io>, <lobo@dpella.io>
Integration tests for Model Context Protocol (MCP) server implementation
following the Servant testing cookbook approach
-}
module MCP.Integration where
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (FromJSON, object, toJSON, (.=))
import Data.Aeson qualified as Aeson
import Data.Attoparsec.ByteString.Char8 as C
import Data.Attoparsec.ByteString.Lazy as P
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as LBS
import Data.Map qualified as Map
import Data.Text (Text)
import MCP.Protocol
import MCP.TestServer (
availablePrompts,
availableResourceTemplates,
availableResources,
availableTools,
completionValues,
createTestAppWithJWT,
csvTestData,
imageTestData,
jsonTestData,
)
import MCP.TestUtils
import MCP.Types
import Network.HTTP.Types qualified as HTTP
import Network.Wai.Test (SResponse (..))
import Servant
import Servant.Auth.Server qualified as AuthServer
import Test.Hspec
import Test.Hspec.Wai (
WaiSession,
get,
post,
request,
shouldRespondWith,
withState,
)
-- * Integration Test Specifications
-- | Complete integration test suite using hspec-wai
integrationSpec :: Spec
integrationSpec = describe "MCP Integration Tests" $ do
withState createTestAppWithJWT $ do
serverLifecycleSpec
authenticationSpec
protocolFlowSpec
endpointsSpec
errorHandlingSpec
preInitializationSpec
protocolMethodsSpec
toolErrorSpec
resourceErrorSpec
promptErrorSpec
resourceTemplateSpec
completionSpec
unimplementedHandlerSpec
nullParamsSpec
-- | Test server startup and basic HTTP behavior
serverLifecycleSpec :: SpecWith (AuthServer.JWTSettings, Application)
serverLifecycleSpec = describe "Server Lifecycle" $ do
let ping_request =
toJSON $
createJSONRPCRequest Nothing (1 :: Int) "ping" Aeson.Null
it "responds with 401 for unauthenticated GET requests" $
get "/mcp" `shouldRespondWith` 401
it "responds with 401 for unauthenticated POST requests" $ do
let headers = [("Content-Type", "application/json")]
mcpPostRequestExpects headers ping_request 401
it "initialization handshake works with proper JWT authentication" $
withAuthenticatedRequest $ \headers -> do
-- the client sends the initialize request
let init_req = toJSON createInitializeRequest
resp_init <- mcpPostRequest headers init_req
withValidJSONRPCResponse resp_init 1 validateInitializationResponse
-- after successful initialization, the client sends notification to
-- indicate readiness
let notify_request = toJSON createInitializedNotification
mcpPostRequestOk headers notify_request
-- | Test JWT authentication scenarios
authenticationSpec :: SpecWith (AuthServer.JWTSettings, Application)
authenticationSpec = describe "Authentication" $ do
let ping_request = toJSON createPingRequest
it "rejects requests without JWT token" $ do
let headers = [("Content-Type", "application/json")]
mcpPostRequestExpects headers ping_request 401
it "rejects requests with invalid JWT token" $ do
let headers =
[ ("Content-Type", "application/json")
, ("Authorization", "Bearer invalid-token")
]
mcpPostRequestExpects headers ping_request 401
it "rejects requests with malformed Authorization header" $ do
let headers =
[ ("Content-Type", "application/json")
, ("Authorization", "NotBearer token")
]
mcpPostRequestExpects headers ping_request 401
-- | Test MCP protocol flows (with proper JWT authentication)
protocolFlowSpec :: SpecWith (AuthServer.JWTSettings, Application)
protocolFlowSpec = describe "Protocol Flow" $ do
it "rejects authenticated requests with missing jsonrpc field" $ do
withAuthenticatedRequest $ \headers -> do
let invalid_request = object ["id" .= (1 :: Int), "method" .= ("ping" :: Text)]
mcpPostRequestExpects headers invalid_request 400
it "rejects authenticated request with wrong jsonrpc version" $ do
withAuthenticatedRequest $ \headers -> do
let invalid_request =
toJSON $
createJSONRPCRequest (Just "1.0") (1 :: Int) "ping" Aeson.Null
mcpPostRequestExpects headers invalid_request 400
it "rejects authenticated request with wrong id" $ do
withAuthenticatedRequest $ \headers -> do
let invalid_id = object ["unexpected" .= ("value" :: Text)]
let invalid_request =
toJSON $
createJSONRPCRequest Nothing invalid_id "ping" Aeson.Null
mcpPostRequestExpects headers invalid_request 400
-- | Test various MCP endpoints to ensure they respond correctly
endpointsSpec :: SpecWith (AuthServer.JWTSettings, Application)
endpointsSpec = describe "Endpoint Health Check" $ do
it "handles list/tools request successfully" $ do
withInitializedServer $ \headers -> do
let req_id = 2
let expected_tools = ["addition-tool", "constant-msg-tool"]
let list_tool_req = toJSON $ createListToolsRequest req_id
-- the client sends the list tools request
resp_list_tool <- mcpPostRequest headers list_tool_req
-- verify response's id and listed tools
withValidJSONRPCResponse resp_list_tool req_id $
validateToolsListResponse expected_tools
it "handles tool/call requests successfully" $ do
withInitializedServer $ \headers -> do
-- the client sends a tool/call request to the addition tool
let req_id1 = 3
let call_req1 =
toJSON $
createCallToolRequest req_id1 "addition-tool" [("arg1", toJSON (5 :: Int)), ("arg2", toJSON (7 :: Int))]
resp_call <- mcpPostRequest headers call_req1
withValidJSONRPCResponse resp_call req_id1 $
validateToolCallResponse (toJSON (12 :: Int))
-- the client sends a tool/call request to the constant message tool
let req_id2 = 4
let call_req2 =
toJSON $
createCallToolRequest req_id2 "constant-msg-tool" []
resp_call2 <- mcpPostRequest headers call_req2
withValidJSONRPCResponse resp_call2 req_id2 $
validateToolCallResponse (toJSON ("Hello, World!" :: Text))
it "handles prompt/list requests successfully" $ do
withInitializedServer $ \headers -> do
let req_id = 5
let expected_prompts = ["code-review"]
let list_prompt_req = toJSON $ createPromptListRequest req_id
-- the client sends the list prompts request
resp_list_prompt <- mcpPostRequest headers list_prompt_req
-- verify response's id and listed prompts
withValidJSONRPCResponse resp_list_prompt req_id $
validatePromptListResponse expected_prompts
it "handles get/prompt requests successfully" $ do
withInitializedServer $ \headers -> do
-- the client sends a get/prompt request for the "code-review" prompt
let req_id = 6
let args = Map.fromList [("code", "def foo():\n return 42")]
let get_prompt_req = toJSON $ createGetPromptRequest req_id "code-review" (Just args)
resp_get_prompt <- mcpPostRequest headers get_prompt_req
-- verify response's id and retrieved prompt
withValidJSONRPCResponse resp_get_prompt req_id $
validateGetPromptResponse "A prompt to request a code review from the LLM" [User, Assistant]
it "handles list/resources requests successfully" $ do
withInitializedServer $ \headers -> do
let req_id = 7
let list_resources_req = toJSON $ createListResourcesRequest req_id
-- the client sends the list resources request
resp_list_resources <- mcpPostRequest headers list_resources_req
-- verify response's id and listed resources
withValidJSONRPCResponse resp_list_resources req_id $
validateListResourcesResponse
["resource://example/document", "resource://example/data", "resource://example/image"]
it "handles read/resource requests successfully" $ do
withInitializedServer $ \headers -> do
let requests =
[ (8, "resource://example/document", csvTestData)
, (9, "resource://example/data", jsonTestData)
, (10, "resource://example/image", imageTestData)
]
mapM_
( \(req_id, req_uri, expected_content) -> do
let read_resource = toJSON $ createReadResourceRequest req_id req_uri
-- the client sends the read resource request
resp_read_resource <- mcpPostRequest headers read_resource
-- verify response's id and read resource contents
withValidJSONRPCResponse resp_read_resource req_id $
validateReadResourceResponse req_uri expected_content
)
requests
-- | Test error handling scenarios
errorHandlingSpec :: SpecWith (AuthServer.JWTSettings, Application)
errorHandlingSpec = describe "Error Handling" $ do
it "handles malformed JSON requests" $ do
let headers = [("Content-Type", "application/json")]
mcpPostRequestExpects headers "invalid json" 400
it "handles requests with wrong content type" $
post "/mcp" "some text" `shouldRespondWith` 415
it "handles empty requests" $ do
let headers = [("Content-Type", "application/json")]
-- should auth fail first (401)?
mcpPostRequestExpects headers "" 400
it "handles large requests appropriately" $ do
withAuthenticatedRequest $ \headers -> do
let large_value = toJSON $ replicate 1000 ('x' :: Char)
let large_json =
toJSON $
createJSONRPCRequest Nothing (1 :: Int) "ping" large_value
mcpPostRequestOk headers large_json
-- | Test that methods are rejected before initialization
preInitializationSpec :: SpecWith (AuthServer.JWTSettings, Application)
preInitializationSpec = describe "Pre-Initialization Enforcement" $ do
it "rejects tools/list before initialization" $
withAuthenticatedRequest $ \headers -> do
let req = toJSON $ createListToolsRequest 1
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 1 $ \err_info ->
code err_info `shouldBe` sERVER_NOT_INITIALIZED
it "rejects tools/call before initialization" $
withAuthenticatedRequest $ \headers -> do
let req = toJSON $ createCallToolRequest 1 "addition-tool" [("arg1", toJSON (1 :: Int)), ("arg2", toJSON (2 :: Int))]
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 1 $ \err_info ->
code err_info `shouldBe` sERVER_NOT_INITIALIZED
it "rejects resources/list before initialization" $
withAuthenticatedRequest $ \headers -> do
let req = toJSON $ createListResourcesRequest 1
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 1 $ \err_info ->
code err_info `shouldBe` sERVER_NOT_INITIALIZED
it "rejects prompts/list before initialization" $
withAuthenticatedRequest $ \headers -> do
let req = toJSON $ createPromptListRequest 1
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 1 $ \err_info ->
code err_info `shouldBe` sERVER_NOT_INITIALIZED
it "rejects resources/templates/list before initialization" $
withAuthenticatedRequest $ \headers -> do
let req = toJSON $ createListResourceTemplatesRequest 1
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 1 $ \err_info ->
code err_info `shouldBe` sERVER_NOT_INITIALIZED
it "rejects completion/complete before initialization" $
withAuthenticatedRequest $ \headers -> do
let req = toJSON $ createCompleteRequest 1 "code-review" "code" ""
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 1 $ \err_info ->
code err_info `shouldBe` sERVER_NOT_INITIALIZED
it "allows ping before initialization" $
withAuthenticatedRequest $ \headers -> do
let req = toJSON createPingRequest
mcpPostRequestOk headers req
it "allows initialize before initialization" $
withAuthenticatedRequest $ \headers -> do
let req = toJSON createInitializeRequest
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 1 validateInitializationResponse
-- | Test various protocol-level methods
protocolMethodsSpec :: SpecWith (AuthServer.JWTSettings, Application)
protocolMethodsSpec = describe "Protocol Methods" $ do
it "handles ping after initialization" $
withInitializedServer $ \headers -> do
let req = toJSON createPingRequest
mcpPostRequestOk headers req
it "handles logging/setLevel" $
withInitializedServer $ \headers -> do
let req = toJSON $ createSetLevelRequest 2 Debug
mcpPostRequestOk headers req
it "returns method_not_found for unknown methods" $
withInitializedServer $ \headers -> do
let req =
toJSON $
createJSONRPCRequest Nothing (2 :: Int) "unknown/method" (object [])
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` mETHOD_NOT_FOUND
it "handles GET /mcp with valid authentication" $
withAuthenticatedRequest $ \headers -> do
let get_headers = filter (\(h, _) -> h /= "Content-Type") headers
let auth_header = filter (\(h, _) -> h == "Authorization") get_headers
resp <- getWithHeaders "/mcp" auth_header
liftIO $ HTTP.statusCode (simpleStatus resp) `shouldBe` 200
-- | Test tool call error scenarios
toolErrorSpec :: SpecWith (AuthServer.JWTSettings, Application)
toolErrorSpec = describe "Tool Call Errors" $ do
it "returns error for non-existent tool" $
withInitializedServer $ \headers -> do
let req = toJSON $ createCallToolRequest 2 "nonexistent-tool" []
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` 404
it "returns error for missing required arguments" $
withInitializedServer $ \headers -> do
-- addition-tool requires arg1 and arg2, call with no args
let req = toJSON $ createCallToolRequest 2 "addition-tool" []
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` 400
it "returns error for partially missing required arguments" $
withInitializedServer $ \headers -> do
-- addition-tool requires arg1 and arg2, call with only arg1
let req = toJSON $ createCallToolRequest 2 "addition-tool" [("arg1", toJSON (5 :: Int))]
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` 400
it "returns error for invalid argument types" $
withInitializedServer $ \headers -> do
-- addition-tool expects numbers, pass strings
let req = toJSON $ createCallToolRequest 2 "addition-tool" [("arg1", toJSON ("not-a-number" :: Text)), ("arg2", toJSON (5 :: Int))]
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` 400
-- | Test resource error scenarios
resourceErrorSpec :: SpecWith (AuthServer.JWTSettings, Application)
resourceErrorSpec = describe "Resource Errors" $ do
it "returns error for non-existent resource" $
withInitializedServer $ \headers -> do
let req = toJSON $ createReadResourceRequest 2 "resource://nonexistent/thing"
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` 404
-- | Test prompt error scenarios
promptErrorSpec :: SpecWith (AuthServer.JWTSettings, Application)
promptErrorSpec = describe "Prompt Errors" $ do
it "returns error for non-existent prompt" $
withInitializedServer $ \headers -> do
let req = toJSON $ createGetPromptRequest 2 "nonexistent-prompt" Nothing
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` 404
-- | Test resource template operations
resourceTemplateSpec :: SpecWith (AuthServer.JWTSettings, Application)
resourceTemplateSpec = describe "Resource Templates" $ do
it "handles resources/templates/list successfully" $
withInitializedServer $ \headers -> do
let req = toJSON $ createListResourceTemplatesRequest 2
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(ListResourceTemplatesResult{resourceTemplates = templates}) -> do
let template_names = fmap (\ResourceTemplate{name = n} -> n) templates
template_names `shouldMatchList` ["user-profile", "log-file"]
it "handles resources/templates/list with null params" $
withInitializedServer $ \headers -> do
let req =
toJSON $
createJSONRPCRequest Nothing (2 :: Int) "resources/templates/list" Aeson.Null
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(ListResourceTemplatesResult{resourceTemplates = templates}) ->
length templates `shouldBe` length availableResourceTemplates
-- | Test completion operations
completionSpec :: SpecWith (AuthServer.JWTSettings, Application)
completionSpec = describe "Completions" $ do
it "handles completion/complete for known prompt argument" $
withInitializedServer $ \headers -> do
let req = toJSON $ createCompleteRequest 2 "code-review" "code" ""
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(CompleteResult{completion = CompletionResult{values = vals}}) ->
vals `shouldBe` completionValues
it "handles completion/complete with prefix filter" $
withInitializedServer $ \headers -> do
let req = toJSON $ createCompleteRequest 2 "code-review" "code" "def"
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(CompleteResult{completion = CompletionResult{values = vals}}) ->
vals `shouldBe` ["def foo():"]
it "returns empty completions for unknown prompt" $
withInitializedServer $ \headers -> do
let req = toJSON $ createCompleteRequest 2 "nonexistent" "arg" ""
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(CompleteResult{completion = CompletionResult{values = vals}}) ->
vals `shouldBe` []
it "returns empty completions for unknown argument" $
withInitializedServer $ \headers -> do
let req = toJSON $ createCompleteRequest 2 "code-review" "nonexistent-arg" ""
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(CompleteResult{completion = CompletionResult{values = vals}}) ->
vals `shouldBe` []
-- | Test methods whose handlers are not configured
unimplementedHandlerSpec :: SpecWith (AuthServer.JWTSettings, Application)
unimplementedHandlerSpec = describe "Unimplemented Handler Methods" $ do
it "returns method_not_found for resources/subscribe" $
withInitializedServer $ \headers -> do
let req = toJSON $ createSubscribeRequest 2 "resource://example/document"
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` mETHOD_NOT_FOUND
-- | Test that null params work for list methods
nullParamsSpec :: SpecWith (AuthServer.JWTSettings, Application)
nullParamsSpec = describe "Null Params Handling" $ do
it "handles tools/list with null params" $
withInitializedServer $ \headers -> do
let req =
toJSON $
createJSONRPCRequest Nothing (2 :: Int) "tools/list" Aeson.Null
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(ListToolsResult{tools = ls_tools}) ->
length ls_tools `shouldBe` length availableTools
it "handles resources/list with null params" $
withInitializedServer $ \headers -> do
let req =
toJSON $
createJSONRPCRequest Nothing (2 :: Int) "resources/list" Aeson.Null
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(ListResourcesResult{resources = ls_resources}) ->
length ls_resources `shouldBe` length availableResources
it "handles prompts/list with null params" $
withInitializedServer $ \headers -> do
let req =
toJSON $
createJSONRPCRequest Nothing (2 :: Int) "prompts/list" Aeson.Null
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(ListPromptsResult{prompts = ls_prompts}) ->
length ls_prompts `shouldBe` length availablePrompts
it "handles resources/templates/list with null params" $
withInitializedServer $ \headers -> do
let req =
toJSON $
createJSONRPCRequest Nothing (2 :: Int) "resources/templates/list" Aeson.Null
resp <- mcpPostRequest headers req
withValidJSONRPCResponse resp 2 $ \(ListResourceTemplatesResult{resourceTemplates = templates}) ->
length templates `shouldBe` length availableResourceTemplates
it "rejects resources/read with null params" $
withInitializedServer $ \headers -> do
let req =
toJSON $
createJSONRPCRequest Nothing (2 :: Int) "resources/read" Aeson.Null
resp <- mcpPostRequest headers req
withValidJSONRPCErrorResponse resp 2 $ \err_info ->
code err_info `shouldBe` iNVALID_PARAMS
-- * Helper Functions
-- ** Validation
{- | Validates that the JSON-RPC response can be parsed and applies the given
properties to the parsed result
-}
withValidJSONRPCResponse ::
(FromJSON a) =>
SResponse ->
Int ->
(a -> Expectation) ->
WaiSession AuthServer.JWTSettings ()
withValidJSONRPCResponse resp expected_id props = do
liftIO $
case parseJSONRPCResponse resp of
Right (JSONRPCResponse rpc_vrs req_id json_result) -> do
case Aeson.fromJSON json_result of
Aeson.Error err -> expectationFailure $ "Failed to parse result: " <> err
Aeson.Success val -> do
rpc_vrs `shouldBe` rPC_VERSION
req_id `shouldBe` toRequestId expected_id
props val
Left err_msg -> expectationFailure err_msg
-- | Validates that the initialization response conforms to the protocol
validateInitializationResponse :: InitializeResult -> Expectation
validateInitializationResponse InitializeResult{protocolVersion = init_protocol_version} = do
init_protocol_version `shouldBe` pROTOCOL_VERSION
-- | Validates that the tools list response contains the expected tools
validateToolsListResponse :: [Text] -> ListToolsResult -> Expectation
validateToolsListResponse expected_tools = \case
(ListToolsResult{tools = ls_tools}) -> do
let tools_names = fmap (\(Tool{name = t_name}) -> t_name) ls_tools
tools_names `shouldMatchList` expected_tools
-- | Validates that the tool call response contains the expected result
validateToolCallResponse :: Aeson.Value -> CallToolResult -> Expectation
validateToolCallResponse expected_result = \case
(CallToolResult{structuredContent = structured}) -> do
case structured of
Just map_results ->
Map.lookup "result" map_results `shouldBe` Just expected_result
Nothing -> expectationFailure "Expected structured content in tool call result"
-- | Validates that the prompt list response contains the expected prompts
validatePromptListResponse :: [Text] -> ListPromptsResult -> Expectation
validatePromptListResponse expected_prompts = \case
(ListPromptsResult{prompts = ls_prompts}) -> do
let prompt_names = fmap (\(Prompt{name = p_name}) -> p_name) ls_prompts
prompt_names `shouldMatchList` expected_prompts
{- | Validates that the get prompt response contains the expected description
and messages with correct roles
-}
validateGetPromptResponse :: Text -> [Role] -> GetPromptResult -> Expectation
validateGetPromptResponse expected_description expected_roles = \case
(GetPromptResult{description = p_description, messages = p_messages}) -> do
p_description `shouldBe` Just expected_description
length p_messages `shouldBe` length expected_roles
fmap (\(PromptMessage{role = m_role}) -> m_role) p_messages
`shouldSatisfy` all (`elem` expected_roles)
-- | Validates that the list resources response is as expected
validateListResourcesResponse :: [Text] -> ListResourcesResult -> Expectation
validateListResourcesResponse expected_resources = \case
(ListResourcesResult{resources = ls_resources}) -> do
let resource_uris = fmap (\(Resource{uri = r_uri}) -> r_uri) ls_resources
resource_uris `shouldMatchList` expected_resources
-- | Validates that reading a resource response is as expected
validateReadResourceResponse :: Text -> Text -> ReadResourceResult -> Expectation
validateReadResourceResponse expected_uri expected_content = \case
(ReadResourceResult{contents = r_contents}) -> do
let uris_contents =
fmap
( \case
TextResource (TextResourceContents{uri = r_uri, text = r_content}) -> (r_uri, r_content)
BlobResource (BlobResourceContents{uri = r_uri, blob = r_content}) -> (r_uri, r_content)
)
r_contents
case uris_contents of
[(res_uri, res_content)] -> do
res_uri `shouldBe` expected_uri
res_content `shouldBe` expected_content
_ -> expectationFailure $ "Expected exactly 1 resource, got " <> show (length uris_contents)
{- | Validates that the JSON-RPC error response can be parsed and applies
the given properties to the parsed error info
-}
withValidJSONRPCErrorResponse ::
SResponse ->
Int ->
(JSONRPCErrorInfo -> Expectation) ->
WaiSession AuthServer.JWTSettings ()
withValidJSONRPCErrorResponse resp expected_id props = do
liftIO $
case parseJSONRPCErrorResponse resp of
Right (JSONRPCError rpc_vrs req_id err_info) -> do
rpc_vrs `shouldBe` rPC_VERSION
req_id `shouldBe` toRequestId expected_id
props err_info
Left err_msg -> expectationFailure err_msg
-- | Helper for making GET requests with specific headers
getWithHeaders ::
BS.ByteString ->
[(HTTP.HeaderName, BS.ByteString)] ->
WaiSession st SResponse
getWithHeaders path headers =
Test.Hspec.Wai.request HTTP.methodGet path headers ""
-- ** Parsing
-- | Parse JSON-RPC response from SSE response body
parseJSONRPCResponse :: SResponse -> Either String JSONRPCResponse
parseJSONRPCResponse resp =
case extractSSEData (simpleBody resp) of
Just bs_response ->
case Aeson.decodeStrict bs_response of
Just json_resp -> Right json_resp
Nothing -> Left $ "Failed to decode JSON-RPC response from: " <> show bs_response
Nothing -> Left $ "Failed to extract JSON data from SSE response: " <> show (simpleBody resp)
-- | Parse JSON-RPC error response from SSE response body
parseJSONRPCErrorResponse :: SResponse -> Either String JSONRPCError
parseJSONRPCErrorResponse resp =
case extractSSEData (simpleBody resp) of
Just bs_response ->
case Aeson.decodeStrict bs_response of
Just json_err -> Right json_err
Nothing -> Left $ "Failed to decode JSON-RPC error from: " <> show bs_response
Nothing -> Left $ "Failed to extract JSON data from SSE response: " <> show (simpleBody resp)
{- | Extract JSON data from Server-Sent Events (SSE) format
SSE format: "event: message\ndata:{json}\n\n"
-}
extractSSEData :: LBS.ByteString -> Maybe BS.ByteString
extractSSEData sseBody = snd <$> P.maybeResult (P.parse parseEvent sseBody)
where
parseEvent = do
_ <- P.string "event:"
msg_type <- P.takeWhileIncluding (not . C.isEndOfLine)
_ <- C.string "data:"
json_data <- P.takeWhile (not . C.isEndOfLine)
return (msg_type, json_data)