/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { parse } from "graphql"; import { describe, expect, it } from "vitest"; import { makeSession, TEST_SCHEMA } from "../../__tests__/helpers/schema.js"; import { selectLeafInSession } from "../../commands/query.js"; import { UserInputError } from "../errors.js"; import { renderQuery } from "../query-builder.js"; import { addVariable, createSiblingFieldInstance, deepSetArg, type FieldProjectionNode, type FragmentProjectionNode, selectLeaf, setAliasOnPath, setArg, } from "../session.js"; import { resolvePath } from "../walker.js"; describe("query-builder", () => { describe("integration with walker (fragment validation)", () => { it("fragment directories validate compatibility during navigation", () => { const session = makeSession(); expect(resolvePath(TEST_SCHEMA, "query", ["search", "[Account]"]).typeName).toBe("Account"); expect(() => resolvePath(TEST_SCHEMA, "query", ["search", "[Viewer]"])).toThrow( /not a possible type of union SearchHit/, ); session.navigationPath = ["query", "search", "[Account]"]; selectLeafInSession(session, "name"); expect(renderQuery(session)).toMatch(/\.\.\. on Account \{\n\s+name\n\s+\}/); }); }); describe("inline fragment dot-paths", () => { it("supports inline fragment syntax for union types", () => { const session = makeSession(); session.navigationPath = ["query", "search"]; selectLeafInSession(session, "[Account].name", "accountName"); const query = renderQuery(session); expect(query).toMatch(/\.\.\. on Account \{/); expect(query).toMatch(/accountName: name/); }); it("supports nested inline fragment dot-paths", () => { const session = makeSession(); session.navigationPath = ["query"]; selectLeafInSession(session, "search.[Contact].email"); const query = renderQuery(session); expect(query).toMatch(/\.\.\. on Contact \{/); expect(query).toMatch(/email/); }); }); describe("bare-$ variable render guard", () => { it("renders a valid placeholder as a bare variable reference", () => { const session = makeSession(); addVariable(session, "$foo", "Int"); session.navigationPath = ["query", "accounts"]; setArg(session, ["accounts"], "first", "$foo"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); // Bare reference: emitted as $foo, not quoted "$foo". expect(query).toMatch(/first: \$foo\b/); expect(query).not.toContain('"$foo"'); }); it("renders an invalid placeholder ($1var) as a quoted literal", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; setArg(session, ["accounts"], "first", "$1var"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); // $1var is not a valid GraphQL Name -> quoted literal, not a bare var. expect(query).toContain('first: "$1var"'); expect(query).not.toMatch(/first: \$1var\b/); }); it("renders a hyphenated placeholder ($foo-bar) as a quoted literal", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; setArg(session, ["accounts"], "first", "$foo-bar"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); // $foo-bar is not a valid GraphQL Name -> quoted literal, not a bare var. expect(query).toContain('first: "$foo-bar"'); expect(query).not.toMatch(/first: \$foo-bar\b/); }); // W-22697670 (PR #654 review): control chars in a string value are escaped via // JSON.stringify, so the rendered literal is well-formed and graphql.parse() // accepts it — a raw newline previously produced an "Unterminated string" error. it("escapes control chars in a string literal so the query stays parseable", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; setArg(session, ["accounts"], "first", "a\nb\tc"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); expect(query).toContain("first: " + JSON.stringify("a\nb\tc")); expect(query).not.toContain("a\nb"); // no raw newline inside the literal expect(() => parse(query)).not.toThrow(); }); }); describe("aliasing rendering", () => { it("renders multiple aliased instances with different variables", () => { const session = makeSession(); addVariable(session, "$limit", "Int"); addVariable(session, "$otherLimit", "Int"); addVariable(session, "$accountFilter", "AccountFilter"); addVariable(session, "$otherFilter", "AccountFilter"); session.navigationPath = ["query", "accounts"]; setAliasOnPath(session, ["accounts"], "highRevenueAccounts"); setArg(session, ["accounts"], "first", "$limit"); setArg(session, ["accounts"], "where", "$accountFilter"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); session.navigationPath = ["query", "accounts"]; createSiblingFieldInstance(session, ["accounts"], "regionalAccounts"); setArg(session, ["accounts"], "first", "$otherLimit"); setArg(session, ["accounts"], "where", "$otherFilter"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "id"]); const query = renderQuery(session); expect(query).toMatch( /highRevenueAccounts: accounts\(first: \$limit, where: \$accountFilter\)/, ); expect(query).toMatch( /regionalAccounts: accounts\(first: \$otherLimit, where: \$otherFilter\)/, ); }); }); // W-23204027: the render layer asserts every emitted GraphQL Name is valid, // making "the renderer never emits an injectable identifier" a system-wide // invariant layered under the per-builder guards. These tests drive raw, // unguarded names directly into the projection tree (bypassing every builder // and zod guard) to prove the renderer is the last line of defense, and // confirm legitimate output is never over-blocked. describe("render-layer GraphQL Name fail-safe (W-23204027)", () => { // The selection-set-breakout payload from the parent bug W-22735537: a raw // fieldName closes the enclosing block early and hoists a sibling selection. const INJECTION = "Id } injectedAlias: Name { value"; it("throws when a field node carries a raw, unguarded fieldName", () => { const session = makeSession(); const injected: FieldProjectionNode = { id: "fld_injected", kind: "field", parentId: null, // top-level → rendered by renderChildren(session, null, 1) schemaPath: [INJECTION], fieldName: INJECTION, args: {}, directives: [], }; session.nodes.push(injected); // Pinned to the emitter so the test can't pass for the wrong reason // (e.g. a future upstream guard throwing elsewhere). expect(() => renderQuery(session)).toThrow( /renderField: fieldName .* is not a valid GraphQL Name/, ); }); it("throws when selectLeaf stores a raw injection string as the leaf fieldName", () => { // Proves the actual residual sink: selectLeaf stores the final path // segment verbatim as node.fieldName with no assert of its own. const session = makeSession(); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", INJECTION]); expect(() => renderQuery(session)).toThrow( /renderField: fieldName .* is not a valid GraphQL Name/, ); }); it("throws when an alias is a raw, unguarded name", () => { const session = makeSession(); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); setAliasOnPath(session, ["accounts", "edges", "node", "name"], "evil { injected }"); expect(() => renderQuery(session)).toThrow( /renderField: alias .* is not a valid GraphQL Name/, ); }); it("throws when an inline-fragment type condition is a raw, unguarded name", () => { const session = makeSession(); const frag: FragmentProjectionNode = { id: "frag_injected", kind: "fragment", parentId: null, schemaPath: ["[Account] { id } ... on Contact"], onType: "Account { id } ... on Contact", directives: [], }; session.nodes.push(frag); expect(() => renderQuery(session)).toThrow( /renderInlineFragment: onType .* is not a valid GraphQL Name/, ); }); it("throws when a directive name is a raw, unguarded name", () => { const session = makeSession(); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const node = session.nodes.find( (n): n is FieldProjectionNode => n.kind === "field" && n.fieldName === "name", )!; node.directives.push({ name: "skip } injected @", args: {} }); expect(() => renderQuery(session)).toThrow( /renderDirective: directiveName .* is not a valid GraphQL Name/, ); }); it("throws when an operation name is a raw, unguarded name", () => { const session = makeSession(); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); session.operationName = "Evil { injected } query X"; expect(() => renderQuery(session)).toThrow( /renderQuery: operationName .* is not a valid GraphQL Name/, ); }); it("throws when a variable name is a raw, unguarded name", () => { const session = makeSession(); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); // Bypass addVariable's normalization to plant a raw name (the CLI `define` // path only strips a leading `$`, so a hyphenated name reaches the renderer). session.variables.push({ name: "foo-bar", type: "Int" }); expect(() => renderQuery(session)).toThrow( /renderQuery: variableName .* is not a valid GraphQL Name/, ); }); it("does not over-block legitimate output: framework fields, aliases, and type conditions parse to one operation", () => { const session = makeSession(); // Connection framework fields (edges/node) + a valid alias. session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); setAliasOnPath(session, ["accounts", "edges", "node", "name"], "accountName"); // Inline-fragment type condition (... on Account). selectLeafInSession // resolves relative to navigationPath, so reset to the query root first. session.navigationPath = ["query"]; selectLeafInSession(session, "search.[Account].name"); const query = renderQuery(session); expect(() => renderQuery(session)).not.toThrow(); const doc = parse(query); expect(doc.definitions).toHaveLength(1); expect(doc.definitions[0].kind).toBe("OperationDefinition"); expect(query).toMatch(/accountName: name/); expect(query).toMatch(/\.\.\. on Account \{/); }); }); // W-23204027 (PR #694 review): argument KEYS are emitted verbatim as // `: ` and are NOT validated at the builder or zod layer // (filter/orderBy are z.record(z.unknown()) with no key charset). A hostile // filter key can close the argument object early and inject a sibling // selection — reproduced as a fully parseable, schema-valid second connection // (a silent selection-set injection). The render-layer assert on every // emitted key is the universal choke point that closes it. describe("render-layer argument-key fail-safe (W-23204027 / PR#694)", () => { it("throws on a malicious filter object key emitted via the buildList path (the live sink)", () => { // Mirrors buildList: JSON.stringify(spec.filter) stored as the `where` // arg value, then rendered via jsonToGraphQL -> valueToGraphQL. const session = makeSession(); session.navigationPath = ["query", "accounts"]; // A clean-breakout key: closes the object + field selection and opens an // attacker-controlled second connection. Without the guard this renders // fully parseable, schema-valid GraphQL. const filter = { 'Name: {eq:"x"} }) { edges { node { Id } } } evilAlias: accounts(where: { Industry': { eq: "1", }, }; deepSetArg(session, ["accounts"], "where", [], JSON.stringify(filter)); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); expect(() => renderQuery(session)).toThrow( /valueToGraphQL: argumentKey .* is not a valid GraphQL Name/, ); }); it("throws on a filter key that injects only an extra where-condition", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; const filter = { 'Name: { eq: "hack" }, Industry': { eq: "1" } }; deepSetArg(session, ["accounts"], "where", [], JSON.stringify(filter)); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); expect(() => renderQuery(session)).toThrow( /valueToGraphQL: argumentKey .* is not a valid GraphQL Name/, ); }); it("throws on a raw, unguarded top-level argument key", () => { const session = makeSession(); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); // A top-level arg name is normally a hardcoded builder literal, but // sf_gql_raw's `set @args/` can plant an arbitrary one. const node = session.nodes.find( (n): n is FieldProjectionNode => n.kind === "field" && n.fieldName === "accounts", )!; node.args["first) { edges } evil"] = "1"; expect(() => renderQuery(session)).toThrow( /renderField: argumentKey .* is not a valid GraphQL Name/, ); }); it("throws on a raw, unguarded directive argument key", () => { const session = makeSession(); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const node = session.nodes.find( (n): n is FieldProjectionNode => n.kind === "field" && n.fieldName === "name", )!; node.directives.push({ name: "include", args: { "if) evil(x": "true" } }); expect(() => renderQuery(session)).toThrow( /renderDirective: argumentKey .* is not a valid GraphQL Name/, ); }); it("does not over-block a realistic complex filter (nested and/or/not, Custom__c fields, enums)", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; // Every key here is a legitimate GraphQL Name: logical operators, field // operators, an SF custom field API name, and connection args. const filter = { and: [ { Name: { like: "Acme%" } }, { Custom_Field__c: { eq: "widget" } }, { or: [{ AnnualRevenue: { gt: 1000 } }, { NumberOfEmployees: { lt: 50 } }] }, { not: { Status: { in: ["Open", "Closed"] } } }, ], }; deepSetArg(session, ["accounts"], "where", [], JSON.stringify(filter)); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); expect(() => renderQuery(session)).not.toThrow(); const query = renderQuery(session); expect(() => parse(query)).not.toThrow(); // Custom__c field key and logical operators all survive the guard. expect(query).toMatch(/Custom_Field__c: \{ eq: "widget" \}/); expect(query).toMatch(/and: \[/); expect(query).toMatch(/not: \{ Status:/); }); }); // W-23204027 (PR #694 review): a variable's default VALUE was emitted raw // (`$name: Type = `) — the last unformatted value position in the // renderer. Raw emission both breaks legitimate output (a multi-word string // default fails to parse) and is a live selection-set/operation injection sink // reachable via sf_gql_raw's `var $x ''`. The default now goes // through formatArgValue. NOTE: the reviewer's literal suggestion (route through // formatArgValue) was necessary but NOT sufficient on its own — formatArgValue's // quoted-string passthrough (`startsWith('"') && endsWith('"')`) let a payload // that merely starts and ends with a quote break out anyway, so that branch was // tightened to pass only a single well-formed literal. The passthrough test // below is the regression guard for that hole. describe("render-layer variable-default fail-safe (W-23204027 / PR#694)", () => { function withDefault(type: string, defaultValue: string): string { const session = makeSession(); addVariable(session, "w", type, defaultValue); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); return renderQuery(session); } // The security property is structural, not textual: an injection payload may // still appear verbatim *inside a quoted string literal* (that's the point — // it's been neutralized into a scalar value), so a naive substring check would // misfire. Instead, assert the parsed document is exactly one operation whose // only top-level field is the legitimate `accounts` — proving nothing broke // out into the selection set or a second operation. function topLevelFieldNames(query: string): string[] { const doc = parse(query); expect(doc.definitions).toHaveLength(1); const op = doc.definitions[0]; if (op.kind !== "OperationDefinition") throw new Error(`not an operation: ${op.kind}`); return op.selectionSet.selections.map((sel) => sel.kind === "Field" ? (sel.alias?.value ?? sel.name.value) : `<${sel.kind}>`, ); } it("quotes a legitimate multi-word string default so the query still parses", () => { // Was: `= Acme Corp` -> "Syntax Error: Expected \"$\", found Name \"Corp\"". const query = withDefault("String", "Acme Corp"); expect(query).toContain('$w: String = "Acme Corp"'); expect(topLevelFieldNames(query)).toEqual(["accounts"]); }); it("neutralizes an unquoted operation-injection default (no second operation)", () => { // Raw: `= 5) { stolen { Id } } query Decoy($z: Int` -> two operations. // After the fix the payload is confined to one quoted scalar default, so // the document stays a single operation with only the `accounts` field. const query = withDefault("Int", "5) { stolen { Id } } query Decoy($z: Int"); expect(topLevelFieldNames(query)).toEqual(["accounts"]); }); it("neutralizes a QUOTED-passthrough breakout default (the reviewer's fix alone would miss this)", () => { // This payload both starts and ends with `"`, so a bare quoted-string // passthrough would emit it verbatim and render two operations / a sibling // `stolen` field. The tightened passthrough re-encodes it into one safe // literal instead, so `accounts` remains the only top-level field. const query = withDefault("String", '"a") { stolen } query Y($q: String = "b"'); expect(topLevelFieldNames(query)).toEqual(["accounts"]); }); it("does not over-block legitimate scalar/enum defaults", () => { // Numbers, bools, and bare enum tokens must stay unquoted; a real quoted // string literal must pass through untouched. expect(withDefault("Int", "10")).toContain("$w: Int = 10"); expect(withDefault("Boolean", "true")).toContain("$w: Boolean = true"); expect(withDefault("SortOrder", "DESC")).toContain("$w: SortOrder = DESC"); expect(withDefault("String", '"hello world"')).toContain('$w: String = "hello world"'); const legit: Record = { Int: "10", Boolean: "true", SortOrder: "DESC", String: '"hello world"', }; for (const [type, value] of Object.entries(legit)) { expect(() => parse(withDefault(type, value))).not.toThrow(); } }); it("routes an input-object default through the arg-key guard (throws on a hostile key)", () => { // An object default is JSON.stringify'd, so its keys flow through the same // valueToGraphQL arg-key assert — a malicious input-object field name in a // default is closed by the render-layer fail-safe, same as in `where`. const evil: Record = {}; evil["x } ) { stolen } q("] = 1; expect(() => withDefault("AccountFilter", JSON.stringify(evil))).toThrow( /valueToGraphQL: argumentKey .* is not a valid GraphQL Name/, ); // ...while a well-formed object default renders fine. const ok = withDefault("AccountFilter", JSON.stringify({ minRevenue: 100 })); expect(ok).toContain("$w: AccountFilter = { minRevenue: 100 }"); expect(() => parse(ok)).not.toThrow(); }); // W-23204027 (PR #694 review, Round 3): the tests above only ever fed // *valid* JSON to a `{`/`[` default, so they exercised jsonToGraphQL's // success path. Its `catch` branch — hit when a `{`/`[`-prefixed default is // NOT well-formed JSON — used to `return jsonStr` verbatim, skipping // valueToGraphQL's arg-key guard entirely. That was a live injection sink: // a GraphQL input-object literal like `{ minRevenue: 0 }` has an UNQUOTED // key, so it fails JSON.parse and hit the catch. With a named operation and // a wired-in decoy variable it rendered a fully parse- AND validate-clean // second operation. The fix REJECTS (throws a typed UserInputError) instead // of emitting raw. No legitimate producer reaches this branch — the builders // JSON.stringify their objects (valid JSON), and the CLI set/assign path // JSON-validates `{`/`[` literals before storing. it("throws UserInputError on an invalid-JSON object default (the jsonToGraphQL catch sink)", () => { // Unquoted key ⇒ invalid JSON ⇒ jsonToGraphQL catch. The reviewer's // escalation payload: closes the arg + selection, opens a second op. const payload = "{ minRevenue: 0 }) { edges { node { id } } } } query Decoy($z: AccountFilter"; expect(() => withDefault("AccountFilter", payload)).toThrow(UserInputError); expect(() => withDefault("AccountFilter", payload)).toThrow( /is not valid JSON and cannot be rendered as a GraphQL literal/, ); }); it("throws UserInputError on an invalid-JSON array default too", () => { // The `[`-prefixed branch of formatArgValue routes here as well. expect(() => withDefault("AccountFilter", "[1, 2) { stolen } q(")).toThrow(UserInputError); }); }); // W-23204027 (PR #694 review, Round 3): a variable's TYPE is emitted verbatim // as `$name: `. It is NOT a bare Name (it carries `!`/`[]`), so it's // guarded structurally by assertGraphqlType — peel the wrappers, then assert // the innermost NamedType is a valid GraphQL Name. This is a defense-in-depth // backstop: no current builder feeds a raw agent-controlled type (schema // inference / createInputTypeName / hardcoded scalars only; CLI var/define set // the NAME, not the type), but a tampered/migrated on-disk session or a future // builder could. Without the guard, a type like `Int) { evil } query Decoy($z: Int` // breaks out into a second operation with no default value needed. describe("render-layer variable-type fail-safe (W-23204027 / PR#694)", () => { function withRawType(type: string): () => string { const session = makeSession(); // Bypass addVariable so the raw type reaches the renderer verbatim. session.variables.push({ name: "v", type }); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); return () => renderQuery(session); } it("throws when the innermost type is a breakout string", () => { expect(withRawType("Int) { evil { id } } query Decoy($z: Int")).toThrow( /renderQuery: variableType .* is not a valid GraphQL Name/, ); }); it("throws when a non-null/list wrapper hides a breakout inner type", () => { // The `!`/`[]` wrappers are peeled; the innermost NamedType is asserted. expect(withRawType("[Int) { evil } q(!]!")).toThrow( /renderQuery: variableType .* is not a valid GraphQL Name/, ); }); it("does not over-block legitimate type references (scalars, non-null, lists, list-of-non-null)", () => { for (const type of ["Int", "ID!", "[String]", "[ID!]!", "AccountFilter", "Custom__c"]) { expect(withRawType(type)).not.toThrow(); const query = withRawType(type)(); expect(query).toContain(`$v: ${type}`); expect(() => parse(query)).not.toThrow(); } }); }); // W-23336442: caller-supplied string VALUES (sf_gql_list scope/filter/orderBy) // reach the emitted GraphQL literal via formatArgValue / valueToGraphQL, which // use JSON.stringify. JSON.stringify escapes only the C0 range (U+0000-U+001F) // and leaves DEL (U+007F) plus the entire Cf class (bidi overrides, zero-width, // tag block) RAW inside the quotes. Since the query is a LIVE GraphQL document // reflected to the host, those survivors are post-escaped to GraphQL-valid // \uXXXX. CRITICAL invariants: (1) the specific poisoning code points are ABSENT // from the emitted source (present only as \uXXXX), and (2) graphql.parse() of // the emitted query still succeeds (\uXXXX is spec-valid; \xNN would not be). describe("control-char value escaping (W-23336442)", () => { // U+202E (bidi RLO), U+200B (ZWSP), U+007F (DEL): all survive JSON.stringify. const POISON = "a\u{202e}b\u{200b}c\u{7f}d"; const RAW_CODE_POINTS = /[\u{202e}\u{200b}\u{7f}]/u; it("escapes DEL/Cf in a top-level string arg value; the query still parses", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; // A scalar arg value (like a scope token) carrying poisoned bytes. setArg(session, ["accounts"], "first", POISON); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); // The dangerous code points appear ONLY as \uXXXX, never raw. expect(query).not.toMatch(RAW_CODE_POINTS); expect(query).toContain("\\u202e"); expect(query).toContain("\\u200b"); expect(query).toContain("\\u007f"); // Live-document round-trip: the escaped literal must still parse. expect(() => parse(query)).not.toThrow(); }); it("escapes DEL/Cf in a well-formed quoted string-literal arg value (the passthrough branch); the query still parses", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; // The value is ITSELF a well-formed quoted JSON string literal carrying the // poisoned bytes, so formatArgValue takes its quoted-passthrough branch // (query-builder.ts ~L291) — the third W-23336442 escape site, distinct from // the default JSON.stringify branch and the valueToGraphQL path. JSON.parse // accepts the raw DEL/Cf (legal unescaped inside a JSON string), so WITHOUT // the post-escape the raw bytes would reflect verbatim into the live query. setArg(session, ["accounts"], "first", JSON.stringify(POISON)); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); expect(query).not.toMatch(RAW_CODE_POINTS); expect(query).toContain("\\u202e"); expect(query).toContain("\\u200b"); expect(query).toContain("\\u007f"); expect(() => parse(query)).not.toThrow(); }); it("escapes DEL/Cf inside a nested filter-object string value (the valueToGraphQL path)", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; // Mirrors buildList: JSON.stringify(spec.filter) stored as `where`, then // rendered via jsonToGraphQL -> valueToGraphQL. The poisoned bytes live in // a nested input-object string value. const filter = { name: { like: POISON } }; deepSetArg(session, ["accounts"], "where", [], JSON.stringify(filter)); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); expect(query).not.toMatch(RAW_CODE_POINTS); expect(query).toContain("\\u202e"); expect(query).toContain("\\u200b"); expect(query).toContain("\\u007f"); expect(() => parse(query)).not.toThrow(); }); it("escapes an astral tag char (U+E0001) as a GraphQL-valid surrogate pair, never \\u{...}", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; setArg(session, ["accounts"], "first", "x\u{e0001}y"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); expect(query).not.toMatch(/\u{e0001}/u); // no raw astral tag char expect(query).not.toContain("\\u{"); // never the variable-width form expect(query).toContain("\\udb40\\udc01"); // the surrogate pair expect(() => parse(query)).not.toThrow(); }); it("does not disturb $var, numeric, enum, or boolean value paths", () => { const session = makeSession(); addVariable(session, "$lim", "Int"); session.navigationPath = ["query", "accounts"]; // $var placeholder — emitted bare, not quoted or escaped. setArg(session, ["accounts"], "first", "$lim"); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); expect(query).toMatch(/first: \$lim\b/); expect(query).not.toContain('"$lim"'); expect(() => parse(query)).not.toThrow(); // Numeric / boolean / enum defaults stay bare (unquoted, unescaped). function withDefault(type: string, value: string): string { const s = makeSession(); addVariable(s, "d", type, value); s.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(s, ["accounts", "edges", "node", "name"]); return renderQuery(s); } expect(withDefault("Int", "42")).toContain("$d: Int = 42"); expect(withDefault("Boolean", "true")).toContain("$d: Boolean = true"); expect(withDefault("SortOrder", "DESC")).toContain("$d: SortOrder = DESC"); }); it("does not over-block ordinary Unicode string values (accents, CJK, emoji)", () => { const session = makeSession(); session.navigationPath = ["query", "accounts"]; const ok = "café 日本語 \u{1f600}"; setArg(session, ["accounts"], "first", ok); session.navigationPath = ["query", "accounts", "edges", "node"]; selectLeaf(session, ["accounts", "edges", "node", "name"]); const query = renderQuery(session); // Ordinary Unicode passes through verbatim inside the literal. expect(query).toContain(`first: ${JSON.stringify(ok)}`); expect(() => parse(query)).not.toThrow(); }); }); });