Skip to content

Commit 931b294

Browse files
committed
test(mock): judge plain data through the owner
The two mock files each kept a private isPlainObject that accepted any non-null, non-array object, beside the engine's owner in src/plain-data.ts, which also refuses a foreign prototype. Both copies go and both files import the owner; its predicate type is the type the mock's Json alias names, so no wrapper or cast is needed. A live_state seed can carry a foreign prototype after all: the yaml parser turns !!timestamp into a Date and !!set into a Set, and the schema passes them through as unknown. Under the owner such a seed replaces like a scalar, as the engine treats the same tag in a settings file, where the copies spread it into an empty mapping. deepMerge assigned out[key], so a seed key spelled __proto__ set the merged object's prototype, and under the stricter predicate reslugRepo then skipped that owner mapping. It now reads through own and writes through put from the same owner, and a test pins the seed as data with the owner still reslugged.
1 parent 858c7cd commit 931b294

3 files changed

Lines changed: 17 additions & 11 deletions

File tree

‎test/e2e/mock/request-body.ts‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
* handler that stored the body verbatim would hide both from every scenario.
66
*/
77

8+
import { isPlainObject } from "../../../src/plain-data.js";
89
import type { Route } from "../../../src/sections/contract/endpoints.js";
910
import { sharedValidator } from "../openapi/validate.js";
10-
import type { Json, MockResponse } from "./support.js";
11+
import type { MockResponse } from "./support.js";
1112

1213
/**
1314
* Fields GitHub accepts (verified live) that its descriptor omits, so the spec-derived allowlist would
@@ -19,10 +20,6 @@ export const UNDOCUMENTED_BODY_FIELDS: ReadonlyMap<Route, readonly string[]> = n
1920
["PATCH /repos/{owner}/{repo}", ["has_discussions"]],
2021
]);
2122

22-
function isPlainObject(value: unknown): value is Json {
23-
return value !== null && typeof value === "object" && !Array.isArray(value);
24-
}
25-
2623
/**
2724
* The body a handler receives for `route`: the documented top-level fields of an object body, every
2825
* other shape untouched (a raw string, an array, no body: the OpenAPI validator's concern). Nested

‎test/e2e/mock/state.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
environmentFromPut,
3434
invitationFromPut,
3535
LIST_MOCKS,
36+
type LiveState,
3637
type MockState,
3738
normalizePinnedSeed,
3839
protectionFromPut,
@@ -209,6 +210,17 @@ describe("buildState overlay semantics", () => {
209210
expect((third.repo.owner as Record<string, unknown>).login).toBe("e2e-owner");
210211
expect(third.repo.full_name).toBe("e2e-owner/e2e-repo");
211212
});
213+
214+
test("a seed key named __proto__ is data, as a YAML file can spell it: an own key after the merge, the owner still reslugged", () => {
215+
// Parsed, not a literal: `__proto__` in an object literal sets the prototype.
216+
const seed = JSON.parse('{"repo": {"owner": {"__proto__": {"example": true}}}}') as LiveState;
217+
const owner = buildState(seed, "org", "other-owner/svc").repo.owner as Record<string, unknown>;
218+
expect({
219+
proto: Object.getPrototypeOf(owner),
220+
ownKey: Object.getOwnPropertyDescriptor(owner, "__proto__")?.value,
221+
login: owner.login,
222+
}).toEqual({ proto: Object.prototype, ownKey: { example: true }, login: "other-owner" });
223+
});
212224
});
213225

214226
describe("protectionFromPut round trip", () => {

‎test/e2e/mock/state.ts‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* collaboratorFromPut -> role_name via roleForPermission, the same map the section runs on its declaration
1010
*/
1111

12+
import { isPlainObject, own, put } from "../../../src/plain-data.js";
1213
import {
1314
GRAPHQL_BOOLEAN_TWINS,
1415
GRAPHQL_REVIEW_TWINS,
@@ -380,10 +381,6 @@ export function normalizePinnedSeed(
380381
return pins.sort((a, b) => a.position - b.position);
381382
}
382383

383-
function isPlainObject(value: unknown): value is Json {
384-
return typeof value === "object" && value !== null && !Array.isArray(value);
385-
}
386-
387384
/**
388385
* Runs last in buildState, after the repo is re-slugged and `state.slug` is fixed: the slug is part of
389386
* every id, so an id minted earlier would name the fixture. Write handlers mint with the same codec
@@ -418,8 +415,8 @@ export function restRepoSurface(repo: Json): Json {
418415
function deepMerge(base: Json, overlay: Json): Json {
419416
const out: Json = { ...base };
420417
for (const [key, value] of Object.entries(overlay)) {
421-
const prev = out[key];
422-
out[key] = isPlainObject(prev) && isPlainObject(value) ? deepMerge(prev, value) : value;
418+
const prev = own(out, key);
419+
put(out, key, isPlainObject(prev) && isPlainObject(value) ? deepMerge(prev, value) : value);
423420
}
424421
return out;
425422
}

0 commit comments

Comments
 (0)