Skip to content

Commit da2af61

Browse files
committed
fix(sections): live creation-cap and webhook shapes reject bodies the spec forbids
The creation-cap GET response requires max_open_pull_requests, but the live shape had it optional, so a body of just { enabled: true } read as a cap with no limit and the snapshot dropped the live limit silently. The field is required now, so such a body fails the read as a shape violation like the other off-shape bodies. A live webhook config was an untyped record, so a non-string config.secret (123) reached the snapshot, which minted a $WEBHOOK_SECRET_<id> reference for it. The live config types secret as the string the spec documents, so a non-string one fails the read instead. Both are pinned in the round-trip suite: { enabled: true } and secret: 123 each fail naming the field.
1 parent a83773d commit da2af61

3 files changed

Lines changed: 36 additions & 7 deletions

File tree

‎src/sections/interaction_limits/index.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function noLiveLimit(live: unknown): boolean {
117117

118118
const LiveCreationCap = z.looseObject({
119119
enabled: z.boolean(),
120-
max_open_pull_requests: z.number().optional(),
120+
max_open_pull_requests: z.number(),
121121
});
122122

123123
const LiveInteractionLimit = z.looseObject({
@@ -362,10 +362,12 @@ export const interactionLimitsSection = {
362362
return plan;
363363
},
364364
/**
365-
* The base limit only when the repository owns it (an inherited one is the organization's
366-
* setting); the cap only where the feature exists (its 405 also means the bypass endpoints
367-
* would deny) and is enabled, since a disabled cap is GitHub's default; the bypass list only
368-
* when someone is on it.
365+
* Only what the repository itself owns reads back; an inherited limit is the org's or user's
366+
* setting, and a disabled cap is GitHub's default.
367+
*
368+
* limit inherited (org or user origin) -> noted, not declared
369+
* cap answers 405 -> cap and bypass both omitted, noted
370+
* cap disabled, or nobody on the bypass -> omitted
369371
*/
370372
async snapshot(ctx) {
371373
const notes: string[] = [];

‎src/sections/webhooks/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const LiveHook = z.looseObject({
4242
name: z.string().optional(),
4343
active: z.boolean().optional(),
4444
events: z.array(z.string()).optional(),
45-
config: z.record(z.string(), z.unknown()).optional(),
45+
config: z.looseObject({ secret: z.string().optional() }).optional(),
4646
});
4747
type LiveHook = z.infer<typeof LiveHook>;
4848

‎test/sections/snapshot-roundtrip.test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,14 @@ describe("snapshot round trip", () => {
159159
});
160160

161161
test.each([
162-
[{ enabled: "yes" }, "enabled: Invalid input: expected boolean, received string"],
162+
[
163+
{ enabled: "yes", max_open_pull_requests: 1 },
164+
"enabled: Invalid input: expected boolean, received string",
165+
],
166+
[
167+
{ enabled: true },
168+
"max_open_pull_requests: Invalid input: expected number, received undefined",
169+
],
163170
[null, "(body): Invalid input: expected object, received null"],
164171
])(
165172
"a creation-cap body off the shape (%j) fails the snapshot instead of reading as no cap",
@@ -182,6 +189,26 @@ describe("snapshot round trip", () => {
182189
},
183190
);
184191

192+
test("a hook whose config.secret is not a string fails the snapshot instead of minting a reference", async () => {
193+
const fake = registryFake({});
194+
const api: GithubClient = {
195+
tryRequest: (method, path, payload, options) =>
196+
method === "GET" && path.startsWith("/repos/o/r/hooks?")
197+
? Promise.resolve({
198+
data: [
199+
{ id: 7, name: "web", config: { url: "https://ci.example.com/hook", secret: 123 } },
200+
],
201+
})
202+
: fake.tryRequest(method, path, payload, options),
203+
tryGraphql: (op, variables, slug) => fake.tryGraphql(op, variables, slug),
204+
};
205+
await expect(webhooksSection.snapshot(planContext(webhooksSection, api, REPO))).rejects.toThrow(
206+
"webhooks: GET /repos/{owner}/{repo}/hooks returned a body outside the documented shape - " +
207+
"[0].config.secret: Invalid input: expected string, received number. Check the " +
208+
'"api-version" input against the GitHub REST docs for this endpoint',
209+
);
210+
});
211+
185212
test("a hook without a config.url is noted and left out; alone, it leaves nothing to declare", async () => {
186213
const api = registryFake({ hooks: [{ id: 7, config: {} }] });
187214
const snapshot = await webhooksSection.snapshot(planContext(webhooksSection, api, REPO));

0 commit comments

Comments
 (0)