From 8cb408a5b707f2fd5cc8fe781a9e3cc985f230fe Mon Sep 17 00:00:00 2001 From: James Cleveland Date: Sun, 26 Jul 2026 22:34:43 +0100 Subject: [PATCH] test(mcp): pin /graphql to honouring the local default token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behaviour is already correct: graphql_endpoint's resolve_token call already falls back to mcp.default_token when a request has no headers. But nothing exercised graphql_endpoint itself — the existing tests only covered the pure resolve_token() function. If a future edit made that call site headers-only (e.g. for a headers-only mcp subcommand), every existing test would still pass while GraphiQL silently broke: a browser cannot attach the x-fastmail-token header, so the IDE would have no way to authenticate. Adds a regression test that calls graphql_endpoint directly with a FastmailMcp built with a fake default token, empty headers, and a non-introspection query, pre-seeding the client cache so the resolver's network attempt hits a refusing loopback address instead of the real Fastmail API. Also adds a one-line comment at the call site pinning the invariant for future reviewers. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SWusZmJbmiuciJYJs78V4P --- src/mcp/mod.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index 1457111..11a0f83 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -327,6 +327,9 @@ async fn graphql_endpoint( let mut request = if is_introspection_only(&req.query) { async_graphql::Request::new(&req.query) } else { + // Must keep honouring `mcp.default_token` here: GraphiQL runs in a + // browser and cannot attach the token header, so making this + // headers-only breaks local development. let Some(token) = resolve_token(Some(&headers), mcp.default_token.as_deref()) else { return error(format!( "No Fastmail token available. Configure one via `fastmail auth` \ @@ -504,4 +507,43 @@ mod tests { assert_eq!(resolve_token(Some(&headers_with(None)), None), None); assert_eq!(resolve_token(None, None), None); } + + /// GraphiQL runs entirely in the browser, with no way to attach the + /// `x-fastmail-token` header, so `/graphql` has to keep honouring the + /// locally configured token when a request carries none — otherwise the + /// IDE that exists specifically to explore the API without ceremony + /// becomes unusable the moment credential resolution changes. + #[tokio::test] + async fn graphql_falls_back_to_local_config_when_no_headers() { + let mcp = FastmailMcp::build(Some("fake-token".to_string())); + + // Pre-seed the client cache with a client whose session already + // points at an address that refuses connections instantly, so the + // resolver's JMAP call fails fast instead of reaching the real + // Fastmail API with a fake token. + let client = JmapClient::with_test_session("http://127.0.0.1:1"); + mcp.clients + .lock() + .await + .insert("fake-token".to_string(), Arc::new(Mutex::new(client))); + + let req = HttpGraphqlRequest { + query: "{ session { status } }".to_string(), + variables: None, + operation_name: None, + }; + + let response = graphql_endpoint( + axum::extract::State(mcp), + http::HeaderMap::new(), + axum::Json(req), + ) + .await; + + let body = serde_json::to_string(&response.0).unwrap(); + assert!( + !body.contains("No Fastmail token available"), + "expected the default token to be used, got: {body}" + ); + } }