Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions test/e2e/mock/chaos.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, test } from "bun:test";
import { takeCorruption } from "./chaos.js";

describe("missing_envelope corruption", () => {
test("a body key named __proto__ stays data in the stripped reply, as a seeded repo can spell it, and the lists still go", () => {
// Parsed, not a literal: `__proto__` in an object literal sets the prototype.
const body = JSON.parse('{"name": "svc", "topics": ["a"], "__proto__": {"example": true}}');
const result = takeCorruption(
"k",
{ corrupt: { key: "k", mode: "missing_envelope" }, corruptCounts: new Map() },
{ status: 200, body },
{ method: "GET", pathname: "/repos/octocat/svc", query: "", status: 0 },
);
const stripped = result?.response.body as Record<string, unknown>;
expect({
status: result?.response.status,
offSpec: result?.offSpecBody,
proto: Object.getPrototypeOf(stripped),
keys: Object.getOwnPropertyNames(stripped),
ownKey: Object.getOwnPropertyDescriptor(stripped, "__proto__")?.value,
name: stripped.name,
}).toEqual({
status: 200,
offSpec: true,
proto: Object.prototype,
keys: ["name", "__proto__"],
ownKey: { example: true },
name: "svc",
});
});
});
7 changes: 4 additions & 3 deletions test/e2e/mock/chaos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** Fault and chaos-corruption injection; a scenario addresses a section endpoint or an inline core route by the same key. */

import { isPlainObject, put } from "../../../src/plain-data.js";
import { ISSUE_REPORT_ENDPOINTS } from "../../../src/report/issue-report.js";
import { allEndpoints, allGraphqlOps } from "../../../src/sections/registry.js";
import type {
Expand Down Expand Up @@ -192,11 +193,11 @@ function applyCorruption(
return { response: { status: response.status, body: 42 }, log, offSpecBody: true };
}
const body = response.body;
if (body && typeof body === "object" && !Array.isArray(body)) {
if (isPlainObject(body)) {
const stripped: Json = {};
for (const [entryKey, value] of Object.entries(body as Json)) {
for (const [entryKey, value] of Object.entries(body)) {
if (!Array.isArray(value)) {
stripped[entryKey] = value;
put(stripped, entryKey, value);
}
}
return { response: { status: response.status, body: stripped }, log, offSpecBody: true };
Expand Down
7 changes: 2 additions & 5 deletions test/e2e/mock/request-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* handler that stored the body verbatim would hide both from every scenario.
*/

import { isPlainObject } from "../../../src/plain-data.js";
import type { Route } from "../../../src/sections/contract/endpoints.js";
import { sharedValidator } from "../openapi/validate.js";
import type { Json, MockResponse } from "./support.js";
import type { MockResponse } from "./support.js";

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

function isPlainObject(value: unknown): value is Json {
return value !== null && typeof value === "object" && !Array.isArray(value);
}

/**
* The body a handler receives for `route`: the documented top-level fields of an object body, every
* other shape untouched (a raw string, an array, no body: the OpenAPI validator's concern). Nested
Expand Down
7 changes: 3 additions & 4 deletions test/e2e/mock/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* -> denial barrier -> body allowlist -> handler -> response guard -> chaos hook
*/

import { isPlainObject } from "../../../src/plain-data.js";
import type { SectionKey } from "../../../src/schema.js";
import { endpointPath, toleratedStatuses } from "../../../src/sections/contract/endpoints.js";
import { toleratedGraphqlErrors } from "../../../src/sections/contract/graphql.js";
Expand Down Expand Up @@ -170,15 +171,13 @@ export function handleGraphqlRequest(
if (
typeof body.query !== "string" ||
typeof body.operationName !== "string" ||
typeof body.variables !== "object" ||
body.variables === null ||
Array.isArray(body.variables)
!isPlainObject(body.variables)
) {
return violation(
"GraphQL request body must carry query (string), operationName (string), and variables (object)",
);
}
const variables = body.variables as Json;
const variables = body.variables;

const dispatched = graphqlOpForBody(body, ops);
if (!dispatched) {
Expand Down
12 changes: 12 additions & 0 deletions test/e2e/mock/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
environmentFromPut,
invitationFromPut,
LIST_MOCKS,
type LiveState,
type MockState,
normalizePinnedSeed,
protectionFromPut,
Expand Down Expand Up @@ -209,6 +210,17 @@ describe("buildState overlay semantics", () => {
expect((third.repo.owner as Record<string, unknown>).login).toBe("e2e-owner");
expect(third.repo.full_name).toBe("e2e-owner/e2e-repo");
});

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", () => {
// Parsed, not a literal: `__proto__` in an object literal sets the prototype.
const seed = JSON.parse('{"repo": {"owner": {"__proto__": {"example": true}}}}') as LiveState;
const owner = buildState(seed, "org", "other-owner/svc").repo.owner as Record<string, unknown>;
expect({
proto: Object.getPrototypeOf(owner),
ownKey: Object.getOwnPropertyDescriptor(owner, "__proto__")?.value,
login: owner.login,
}).toEqual({ proto: Object.prototype, ownKey: { example: true }, login: "other-owner" });
});
});

describe("protectionFromPut round trip", () => {
Expand Down
11 changes: 4 additions & 7 deletions test/e2e/mock/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* collaboratorFromPut -> role_name via roleForPermission, the same map the section runs on its declaration
*/

import { isPlainObject, own, put } from "../../../src/plain-data.js";
import {
GRAPHQL_BOOLEAN_TWINS,
GRAPHQL_REVIEW_TWINS,
Expand Down Expand Up @@ -380,10 +381,6 @@ export function normalizePinnedSeed(
return pins.sort((a, b) => a.position - b.position);
}

function isPlainObject(value: unknown): value is Json {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

/**
* Runs last in buildState, after the repo is re-slugged and `state.slug` is fixed: the slug is part of
* every id, so an id minted earlier would name the fixture. Write handlers mint with the same codec
Expand Down Expand Up @@ -418,8 +415,8 @@ export function restRepoSurface(repo: Json): Json {
function deepMerge(base: Json, overlay: Json): Json {
const out: Json = { ...base };
for (const [key, value] of Object.entries(overlay)) {
const prev = out[key];
out[key] = isPlainObject(prev) && isPlainObject(value) ? deepMerge(prev, value) : value;
const prev = own(out, key);
put(out, key, isPlainObject(prev) && isPlainObject(value) ? deepMerge(prev, value) : value);
}
return out;
}
Expand Down Expand Up @@ -1029,7 +1026,7 @@ export function protectionFromPut(payload: Json): Json {
break;
}
default:
out[key] = value;
put(out, key, value);
}
}
return out;
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/mock/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* of them, so a fragment can depend on it without pulling the whole pipeline in.
*/

import { isPlainObject } from "../../../src/plain-data.js";
import type { SectionKey } from "../../../src/schema.js";
import {
type DefinitiveRejection,
Expand Down Expand Up @@ -137,7 +138,7 @@ function clampInt(raw: string | undefined, fallback: number): number {
// --- Handler helpers ------------------------------------------------------

export function asObject(body: unknown): Json {
return body && typeof body === "object" && !Array.isArray(body) ? (body as Json) : {};
return isPlainObject(body) ? body : {};
}

export function ok(body: unknown): MockResponse {
Expand Down
Loading