mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(browser): reject inexact storage values
This commit is contained in:
1 parent
f3a301eca5
commit
15a8f6847c
3 files changed
+69
-18
No files matched your search
@@ -30,6 +30,14 @@ const pointerTokens = (pointer: string): readonly string[] =>
|
||||
.split("/")
|
||||
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
|
||||
|
||||
/** Keep unrelated fields usable while marking numbers that cannot be selected exactly. */
|
||||
export const parseScenarioValueJson = (text: string) =>
|
||||
safeParseJson(text, (_key, value, context) =>
|
||||
typeof value === "number" && !isExactJsonNumber(value, context?.source)
|
||||
? Number.NaN
|
||||
: value,
|
||||
);
|
||||
|
||||
/** Exact own-property lookup: an absent pointer remains distinct from a non-scalar value. */
|
||||
export const responsePointerValue = (
|
||||
value: unknown,
|
||||
@@ -82,11 +90,7 @@ export const readResponseValues = (
|
||||
}
|
||||
if (text.length > MAX_SAVED_RESPONSE_CHARACTERS)
|
||||
return err(issue("the response is too large to save values from."));
|
||||
const parsed = safeParseJson(text, (_key, value, context) =>
|
||||
typeof value === "number" && !isExactJsonNumber(value, context?.source)
|
||||
? Number.NaN
|
||||
: value,
|
||||
);
|
||||
const parsed = parseScenarioValueJson(text);
|
||||
if (!parsed.ok)
|
||||
return err(
|
||||
issue("the response is not JSON, so no value can be saved from it."),
|
||||
|
||||
@@ -584,6 +584,54 @@ describe("request step confinement", () => {
|
||||
expect(calls.at(-1)!.url, path).toBe(`${ORIGIN}${sent}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("request page value admission", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("preserves exact local storage pointer values and refuses lossy numbers before dispatch", async () => {
|
||||
const calls = stubFetch(200);
|
||||
const parsed = scenario([
|
||||
requestStep({
|
||||
headers: [
|
||||
{
|
||||
name: "X-Account",
|
||||
value: {
|
||||
source: "local_storage",
|
||||
key: "auth",
|
||||
json_pointer: "/id",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
for (const literal of ["9007199254740993", "1e400", "-0"]) {
|
||||
await expect(
|
||||
perform(
|
||||
fakePage({ storage: { auth: `{"id":${literal}}` } }),
|
||||
parsed.actions[0]!,
|
||||
resolved(parsed),
|
||||
),
|
||||
).rejects.toThrow("cannot be read exactly");
|
||||
}
|
||||
expect(calls).toEqual([]);
|
||||
for (const [literal, expected] of [
|
||||
["9007199254740991", "9007199254740991"],
|
||||
['"9007199254740993"', "9007199254740993"],
|
||||
["42", "42"],
|
||||
]) {
|
||||
await perform(
|
||||
fakePage({
|
||||
storage: { auth: `{"id":${literal},"unselected":9007199254740993}` },
|
||||
}),
|
||||
parsed.actions[0]!,
|
||||
resolved(parsed),
|
||||
);
|
||||
expect(calls.at(-1)!.init.headers).toEqual([["X-Account", expected]]);
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses page values a header cannot carry, and stored values it cannot read", async () => {
|
||||
const calls = stubFetch(200);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MAX_SAVED_RESPONSE_CHARACTERS,
|
||||
MAX_SAVED_RESPONSE_BYTES,
|
||||
responsePointerValue,
|
||||
parseScenarioValueJson,
|
||||
readResponseValues,
|
||||
readBoundedResponseBody,
|
||||
} from "./BrowserScenarioResponseValues.js";
|
||||
@@ -46,14 +47,6 @@ const originOf = (url: string): string | undefined => {
|
||||
}
|
||||
};
|
||||
|
||||
/** The string or number at a JSON pointer, as text. */
|
||||
const pointerText = (value: unknown, pointer: string): string | undefined => {
|
||||
const found = responsePointerValue(value, pointer);
|
||||
return typeof found === "string" || typeof found === "number"
|
||||
? String(found)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
interface PageValueStep {
|
||||
readonly page: Page;
|
||||
readonly url: URL;
|
||||
@@ -105,15 +98,21 @@ const readPageValue = async (
|
||||
);
|
||||
if (value.json_pointer === undefined) read = stored;
|
||||
else {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(stored);
|
||||
} catch {
|
||||
const parsed = parseScenarioValueJson(stored);
|
||||
if (!parsed.ok) {
|
||||
throw new ScenarioRequestError(
|
||||
`Request step ${stepId}: local storage ${value.key} is not JSON.`,
|
||||
);
|
||||
}
|
||||
read = pointerText(parsed, value.json_pointer);
|
||||
const selected = responsePointerValue(parsed.value, value.json_pointer);
|
||||
if (typeof selected === "number" && !Number.isFinite(selected))
|
||||
throw new ScenarioRequestError(
|
||||
`Request step ${stepId}: local storage ${value.key} has a number at ${value.json_pointer} that cannot be read exactly.`,
|
||||
);
|
||||
read =
|
||||
typeof selected === "string" || typeof selected === "number"
|
||||
? String(selected)
|
||||
: undefined;
|
||||
if (read === undefined)
|
||||
throw new ScenarioRequestError(
|
||||
`Request step ${stepId}: local storage ${value.key} has no string or number at ${value.json_pointer}.`,
|
||||
|
||||
Reference in new issue
Block a user