Tests Phase 2: httptest-mocked HTTP integration + H1/H2 regression guards#3
Merged
Conversation
…ards
Second of three planned phases. Phase 2 covers the L2 layer per the test
plan: in-process httptest.Server fakes for the upstream services
FusionDataCLI talks to (APS GraphQL, APS OAuth, Fusion MCP JSON-RPC),
exercising every network-bound code path without ever leaving the
process.
Coverage jumps:
auth 27.6% -> 73.9% (+46pp; OAuth exchange/refresh, callback)
api 13.2% -> 70.9% (+58pp; gqlQuery, pagination, details,
STEP derivative, file download)
fusion 27.9% -> 83.8% (+56pp; MCP init+tools/call, session retry,
SSE response parsing, stateless mode)
config 90.6% -> 90.6% (unchanged; already covered in Phase 1)
ui 24.5% -> 24.5% (unchanged; UI flow tests are Phase 3)
total 23.9% -> 38.5%
New shared test scaffolding:
internal/testutil/graphql.go GraphQLServer helper
internal/testutil/mcp.go NewMCPServer helper with InitCount,
CallCount, SessionIDsSeen probes
Production refactors (minimal, all const->var so tests can swap):
api/client.go graphqlEndpoint
auth/oauth.go authEndpoint, tokenEndpoint, authScope
auth/callback.go callbackPort, CallbackURL
api/download.go userHomeDir, nowFunc (for StepDownloadPath)
Security regression coverage added:
H1 TestWaitForCallback_OAuthError asserts the response body contains
`<script>` and not the literal `<script>...` payload, so a
future change that drops html.EscapeString in the callback handler
will fail loudly.
H2 TestDownloadFile_Streams asserts the request to the signed-URL
server has an empty Authorization header, guarding against
anyone re-introducing the bearer leak.
Notable test design choices:
- The OAuth callback tests bind the listener on an ephemeral port (`:0`)
via the new var-overridable callbackPort/CallbackURL, so they can run
in parallel with anything else on the dev machine and never collide
with the production 7879.
- The fusion session-retry test (TestInvoke_SessionRetryOn404) drives
the production session-cache + 404-on-tools/call -> drop-cache ->
re-handshake retry path through the MCPServer helper, which exposes
InitCount and SessionIDsSeen so the assertions can prove the retry
actually happened (not just that the second call succeeded).
- Empty-data GraphQL responses are tested via RawBody: `{}` (no `data`
key) rather than `{"data":""}`, because the JSON string `""` decodes
to a 2-byte json.RawMessage that does not trigger the production
len(gr.Data) == 0 branch.
Found-but-not-fixed (will flag separately):
- fusion/mcp.go ActiveHubProjects's payload.Success==false guard
(lines 168-170) is dead code: parseToolErrorText in callTool catches
the same condition first and returns its error, so the
ActiveHubProjects-level check never fires. Safe to delete or leave
as defense-in-depth; not in this PR's scope.
All tests run in under 5s locally with -race -count=1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second of three planned phases bringing this repo's test suite up. Phase 2 covers the L2 layer per the test plan: in-process
httptest.Serverfakes for the upstream services FusionDataCLI talks to (APS GraphQL, APS OAuth, Fusion MCP JSON-RPC), exercising every network-bound code path without leaving the process.Coverage jumps
authapifusionconfiguiuiis unchanged because TUI flow tests are Phase 3 territory. The four core packages now average 79.8%.What's new
Shared scaffolding:
internal/testutil/graphql.go—GraphQLServer(t, handler)decodes APS GraphQL POSTs and lets handlers return canned{data, errors}envelopes.internal/testutil/mcp.go—NewMCPServer(t, scenario)emulates the Fusion MCP JSON-RPC server (initialize handshake,notifications/initialized,tools/call), with probes forInitCount(),CallCount(tool), andSessionIDsSeen()so retry / session-cache behaviour is testable.Production refactors (minimal, all
const→varso tests can swap):api/client.go—graphqlEndpointauth/oauth.go—authEndpoint,tokenEndpoint,authScopeauth/callback.go—callbackPort,CallbackURLapi/download.go—userHomeDir,nowFunc(for deterministicStepDownloadPath)Security regression coverage
TestWaitForCallback_OAuthErrorasserts the callback response body contains<script>(HTML-escaped) and not the literal<script>...payload — guarding against anyone removinghtml.EscapeStringfrom the callback handler.TestDownloadFile_Streamsasserts the request to the signed-URL server has an emptyAuthorizationheader — guarding against anyone re-introducing the bearer leak.Notable test-design choices
:0) via the new var-overridablecallbackPort/CallbackURL, so they don't collide with anything else on the dev machine.TestInvoke_SessionRetryOn404drives the production session-cache + 404-on-tools/call→ drop-cache → re-handshake retry path throughNewMCPServer's probes, asserting the retry actually happened (not just that the second call succeeded).RawBody: \{}`(nodatakey) rather than{"data":""}— the latter decodes to a 2-bytejson.RawMessagethat doesn't trigger the productionlen(gr.Data) == 0` branch.Found-but-not-fixed
fusion/mcp.goActiveHubProjects'spayload.Success == falseguard (lines 168-170) is dead code:parseToolErrorTextincallToolcatches the same condition first. Safe to delete or leave as defense-in-depth — not in this PR's scope.Test plan
make check(vet + race + tests) passes locally in <5stest.ymlworkflow runs green on this PR🤖 Generated with Claude Code