/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { buildSchema } from "graphql"; import { describe, expect, it } from "vitest"; import { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js"; import { primeSchemaCache } from "../../lib/walker.js"; import { buildCreate } from "../build-create.js"; const SCHEMA_SDL = ` type Query { _placeholder: Boolean } type Mutation { uiapi(input: UIAPIMutationsInput): UIAPIMutations! } input UIAPIMutationsInput { allOrNone: Boolean } type UIAPIMutations { AccountCreate(input: AccountCreateInput!): AccountCreatePayload Custom_Object__cCreate(input: Custom_Object__cCreateInput!): Custom_Object__cCreatePayload } input AccountCreateInput { Account: AccountCreateRepresentation! } input AccountCreateRepresentation { Name: String, Industry: String } type AccountCreatePayload { Record: Account } input Custom_Object__cCreateInput { Custom_Object__c: Custom_Object__cCreateRepresentation! } input Custom_Object__cCreateRepresentation { Name: String } type Custom_Object__cCreatePayload { Record: Custom_Object__c } type Account { Id: ID!, Name: StringValue, Industry: StringValue } type Custom_Object__c { Id: ID!, Name: StringValue } type StringValue { value: String } `; const ORG = "test-create-validation"; const ORG_URL = "https://test-create-validation.my.salesforce.com"; const SCHEMA = buildSchema(SCHEMA_SDL); primeSchemaCache(ORG, SCHEMA); primeSchemaCache(ORG_URL, SCHEMA); const noopPrimeDeps = () => makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA); describe("intent/build-create — GraphQL name validation", () => { describe("object name validation", () => { it("throws for object with special characters: 'not valid!'", async () => { await expect( buildCreate({ org: ORG, object: "not valid!" }, noopPrimeDeps()), ).rejects.toThrow(/buildMutation: object 'not valid!' is not a valid GraphQL Name/); }); it("throws for object starting with a digit: '123Object'", async () => { await expect(buildCreate({ org: ORG, object: "123Object" }, noopPrimeDeps())).rejects.toThrow( /buildMutation: object '123Object' is not a valid GraphQL Name/, ); }); it("throws for object with hyphens: 'my-object'", async () => { await expect(buildCreate({ org: ORG, object: "my-object" }, noopPrimeDeps())).rejects.toThrow( /buildMutation: object 'my-object' is not a valid GraphQL Name/, ); }); it("accepts valid object name with underscores: 'Custom_Object__c'", async () => { const result = await buildCreate( { org: ORG, object: "Custom_Object__c", returnFields: ["Id"] }, noopPrimeDeps(), ); expect(result.query).toContain("Custom_Object__cCreate"); }); it("accepts standard object name: 'Account'", async () => { const result = await buildCreate( { org: ORG, object: "Account", returnFields: ["Id"] }, noopPrimeDeps(), ); expect(result.query).toContain("mutation CreateAccount"); }); it("accepts object starting with underscore: '_Foo'", async () => { // The regex allows underscore as first char per GraphQL spec await expect( buildCreate({ org: ORG, object: "_Foo" }, noopPrimeDeps()), ).resolves.toBeDefined(); }); }); describe("inputVariable name validation", () => { it("throws for inputVariable with spaces: 'has spaces'", async () => { await expect( buildCreate({ org: ORG, object: "Account", inputVariable: "has spaces" }, noopPrimeDeps()), ).rejects.toThrow(/buildMutation: inputVariable 'has spaces' is not a valid GraphQL Name/); }); it("throws for inputVariable with dollar sign in body: 'my$var'", async () => { await expect( buildCreate({ org: ORG, object: "Account", inputVariable: "my$var" }, noopPrimeDeps()), ).rejects.toThrow(/buildMutation: inputVariable 'my\$var' is not a valid GraphQL Name/); }); it("throws for inputVariable starting with digit: '1input'", async () => { await expect( buildCreate({ org: ORG, object: "Account", inputVariable: "1input" }, noopPrimeDeps()), ).rejects.toThrow(/buildMutation: inputVariable '1input' is not a valid GraphQL Name/); }); it("accepts inputVariable with leading $ (prefix stripped): '$myInput'", async () => { const result = await buildCreate( { org: ORG, object: "Account", inputVariable: "$myInput" }, noopPrimeDeps(), ); // The leading $ is stripped by existing code before validation expect(result.query).toContain("$myInput"); }); it("accepts valid inputVariable: 'accountInput'", async () => { const result = await buildCreate( { org: ORG, object: "Account", inputVariable: "accountInput" }, noopPrimeDeps(), ); expect(result.query).toContain("$accountInput"); }); it("accepts default inputVariable 'input' when not specified", async () => { const result = await buildCreate({ org: ORG, object: "Account" }, noopPrimeDeps()); expect(result.query).toContain("$input"); }); }); describe("operationName name validation", () => { // Exhaustive name matrix lives in lib/__tests__/graphql-name.spec.ts; this // just asserts buildMutation wires operationName through the guard. it("throws for an operationName that is not a valid GraphQL Name", async () => { await expect( buildCreate({ org: ORG, object: "Account", operationName: "has spaces" }, noopPrimeDeps()), ).rejects.toThrow(/buildMutation: operationName 'has spaces' is not a valid GraphQL Name/); }); }); describe("returnFields validation (W-22735537)", () => { it("rejects a selection-set breakout payload instead of rendering it", async () => { await expect( buildCreate( { org: ORG, object: "Account", returnFields: ["Id } injectedAlias: Name { value"] }, noopPrimeDeps(), ), ).rejects.toThrow(/returnFields entry .* is not a valid field path/); }); it("hard-throws a breakout (not demoted to a warning) — gating order", async () => { // Core-defect regression guard: the assert must run BEFORE the // warn-swallowing try in buildMutation. A breakout must REJECT (throw), // never resolve with the payload tucked into warnings/query. const outcome = await buildCreate( { org: ORG, object: "Account", returnFields: ["Id } injectedAlias: Name { value"] }, noopPrimeDeps(), ).then( (r) => ({ threw: false as const, r }), (e: Error) => ({ threw: true as const, e }), ); expect(outcome.threw).toBe(true); if (outcome.threw) expect(outcome.e.message).toMatch(/is not a valid field path/); }); it("accepts a legit dot-path (Owner.Name) — the guard allows dotted segments", async () => { // Owner.Name is a valid dotted path. It is unsupported in mutation results // (warns-and-skips downstream) but must NOT be rejected by the charset guard. await expect( buildCreate( { org: ORG, object: "Account", returnFields: ["Id", "Owner.Name"] }, noopPrimeDeps(), ), ).resolves.toBeDefined(); }); }); });