Repository navigation
test(e2e): check GraphQL variables against GitHub's schema in the mock - #611
Conversation
The e2e mock answered a GraphQL request by operation name and handed its variables to the handler unread, so a mutation carrying an input field GitHub's schema lacks, or a null for a `Boolean!` variable, passed every scenario. The mock now checks each request the way GitHub does before it runs any of it: graphql-js validates the document against the schema and coerces the variables to the types the operation declares. A request that fails either is answered with HTTP 200, `errors`, and no `data`. No handler runs, and the pipeline records a mock violation, so the scenario fails whatever outcome it expected. The check sits ahead of the check-mode, fault, and permission barriers. A request GitHub accepts reaches its handler with the variables exactly as sent. The schema is the SDL of @octokit/graphql-schema plus the upstream-gap SDL, built once per process. Its builder moves from test/sections/graphql-queries.test.ts to test/e2e/mock/graphql-schema.ts, so the declared-query test and the mock read one schema and one gap tripwire. The section unit fake runs the same check. The fixture operations of graphql-pipeline.test.ts were off GitHub's schema (`hasWiki` is not an `UpdateRepositoryInput` field, and two reads carried another operation's document), so they are now valid documents.
File size check0 over a hard cap (fails), 6 warning(s).
Split the file, wrap the line, shorten or exempt the comment, or list the path in 5 managed file(s) skipped; repo-platform owns them. |
Member shape check22 finding(s).
Add the blank lines, make the comment one line directly above the item, comment every field or none, or list the path in 4 generated file(s) skipped. |
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are reported for the changes adding schema validation to GraphQL test mocks.
No open issues 🎉
What changed in this PR
This PR strengthens the GraphQL test mocks by rejecting documents and variables that GitHub's schema would refuse, preventing tests from passing on invalid requests.
Changes:
- Adds schema validation before GraphQL pipeline barriers, returning errors and recording a mock violation on refusal.
- Shares the schema builder across mock validation and declared-query tests, preserving upstream-gap checks.
- Adds rejection and valid-request tests, updates fixtures, and applies validation to the section-test fake.
| File | Description |
|---|---|
test/sections/graphql-queries.test.ts |
Uses the shared schema builder instead of a local implementation. |
test/sections/fragment-fake.ts |
Validates GraphQL requests before invoking section-test handlers. |
test/e2e/mock/routes.ts |
Rejects invalid requests before barriers and records violations. |
test/e2e/mock/graphql-schema.ts |
Centralizes schema construction, gap extensions, and process-level caching. |
test/e2e/mock/graphql-request.ts |
Checks document syntax, schema validity, and variable coercion. |
test/e2e/mock/graphql-request.test.ts |
Tests wire-level refusals, unchanged state, and valid-request handling. |
test/e2e/mock/graphql-pipeline.test.ts |
Updates fixtures and tests validation ordering and unchanged variable forwarding. |
Review effort: Balanced
… consumer The unit fake's refusal of a request off GitHub's schema had no test, because the section suites send only requests GitHub accepts. test/sections/fragment-fake.test.ts sends `pinned: null` through the fake and expects GitHub's refusal with nothing stored, then the same request with a boolean, which unpins. The generated gap index said UNSHIPPED_GRAPHQL_SDL is consumed by test/sections/graphql-queries.test.ts. The schema builder in test/e2e/mock/graphql-schema.ts consumes it now, so the generator's sentence names the builder and src/generated/upstream-gaps.ts is regenerated: one comment line.
Before
After
Both runs are a product mutant whose unpin sends
pinned: null. A second mutant adds an unknown input field:Refused now: a variable off its declared type, an input field GitHub's schema lacks, a document that does not validate.
For the owner: this would not have caught the wildcard
enforce_admins: nullbug. That null was legal input. Catching it needs a check of what the mock answers, recorded below.How
graphql, withvalidatefor the document andvalidateExecutionArgsfor the variables.Proof
Technical details
nullfor a nullable input field, such asisAdminEnforced: nullon a rule mutation, which GitHub's SDL typesBoolean.git archive origin/main | tar -x -C <dir>, linknode_modules, runbun run build:schema, apply one mutant, run the Before command in<dir>.test/e2e/mock/over<dir>/test/e2e/mock/and run the same command for the After block.src/sections/environments/pins.ts, the unpin's variables{ environmentId, pinned: false }become{ environmentId, pinned: null }.src/sections/branches/graphql-writes.ts, the rule delete's{ input: { branchProtectionRuleId: node.id } }gainspattern.bun test/e2e/run.ts --scenario branches-wildcard-lifecycle | sed -n '3,/passed$/p'printsPASSand1/1 passed.bun test test/e2e/mock/graphql-request.test.ts test/e2e/mock/graphql-pipeline.test.ts test/sections/fragment-fake.test.ts test/sections/graphql-queries.test.ts.control:tests pass on main on purpose: a valid request still answersdata, and the handler still receives the variables as sent.control:test is the unit fake's, intest/sections/fragment-fake.test.ts, and fails on main by design.bun test/e2e/run.ts --sections all, then 388 scenarios plus the corpus witness line.FUZZ_SEED=20261111 bun test/e2e/fuzz.ts --iterations 25.bun test/e2e/run.ts --sections branches,environments,repository(126 scenarios).runScenarioover that slice and checked every logged/graphqlrequest with graphql-js.RepositoryFeaturesandUpdateRepositoryFeaturesvalidate only withsrc/upstream-gaps/issue-creation-policy.ts, which the shared builder already adds.a gap whose SDL the package already ships fails the extension naming the gap file (negative control)intest/sections/graphql-queries.test.tspasses unchanged against the moved builder.data) does not report it a second time.a read without $owner/$repo variables is a violationis nowa read addressing no repository is a violation.$repoleft out is GitHub's own refusal now, so the target-resolution rule is reached by a read that declares no address.UpdateTogglesselectedhasWiki, whichUpdateRepositoryInputlacks, and two fixture reads carried another operation's document.test/sections/fragment-fake.tsruns the check ingraphqlRouteOf, sincetryGraphqlsits at the lint's complexity ceiling.test/sections/fragment-fake.test.tspins the refusal path, which the section suites never reach.src/generated/:src/generated/upstream-gaps.tschanged by one comment line, regenerated from.github/scripts/gen-gaps-index.tsto name the schema builder as the gap SDL's consumer, so nothing a run executes undersrc/changes.executeSyncreported 0 errors on the 71 data responses of the same 73 requests.isAdminEnforced: nullreportsCannot return null for non-nullable field BranchProtectionRule.isAdminEnforced.required_linear_history: nullsends{"requiresLinearHistory": null}and the mock stores it.test/e2e/scenarios/on main, the scenario below printsPASSforbun test/e2e/run.ts --scenario branches-wildcard-null-linear-history.fixpoint: apply_idempotentadded underexpect, the same command printsapply-idempotence: second apply exited 1, expected 0.Boolean, so the null is legal input, and what the real API does with it is unverified.