/** * 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 { buildUpdate } from "../build-update.js"; const SCHEMA_SDL = ` type Query { _placeholder: Boolean } type Mutation { uiapi(input: UIAPIMutationsInput): UIAPIMutations! } input UIAPIMutationsInput { allOrNone: Boolean } type UIAPIMutations { AccountUpdate(input: AccountUpdateInput!): AccountUpdatePayload Custom_Object__cUpdate(input: Custom_Object__cUpdateInput!): Custom_Object__cUpdatePayload } input AccountUpdateInput { Account: AccountUpdateRepresentation! } input AccountUpdateRepresentation { Name: String, Industry: String } type AccountUpdatePayload { Record: Account } input Custom_Object__cUpdateInput { Custom_Object__c: Custom_Object__cUpdateRepresentation! } input Custom_Object__cUpdateRepresentation { Name: String } type Custom_Object__cUpdatePayload { 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-update-validation"; const ORG_URL = "https://test-update-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-update — GraphQL name validation", () => { describe("object name validation", () => { it("throws for object with special characters: 'not valid!'", async () => { await expect( buildUpdate({ 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(buildUpdate({ 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(buildUpdate({ 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 buildUpdate( { org: ORG, object: "Custom_Object__c", returnFields: ["Id"] }, noopPrimeDeps(), ); expect(result.query).toContain("Custom_Object__cUpdate"); }); it("accepts standard object name: 'Account'", async () => { const result = await buildUpdate( { org: ORG, object: "Account", returnFields: ["Id"] }, noopPrimeDeps(), ); expect(result.query).toContain("mutation UpdateAccount"); }); it("accepts object starting with underscore: '_Foo'", async () => { // The regex allows underscore as first char per GraphQL spec. await expect( buildUpdate({ org: ORG, object: "_Foo" }, noopPrimeDeps()), ).resolves.toBeDefined(); }); }); describe("inputVariable name validation", () => { it("throws for inputVariable with spaces: 'has spaces'", async () => { await expect( buildUpdate({ 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( buildUpdate({ 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( buildUpdate({ 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 buildUpdate( { org: ORG, object: "Account", inputVariable: "$myInput" }, noopPrimeDeps(), ); expect(result.query).toContain("$myInput"); }); it("accepts valid inputVariable: 'accountInput'", async () => { const result = await buildUpdate( { org: ORG, object: "Account", inputVariable: "accountInput" }, noopPrimeDeps(), ); expect(result.query).toContain("$accountInput"); }); it("accepts default inputVariable 'input' when not specified", async () => { const result = await buildUpdate({ 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( buildUpdate({ 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", async () => { await expect( buildUpdate( { org: ORG, object: "Account", returnFields: ["Id } injectedAlias: Name { value"] }, noopPrimeDeps(), ), ).rejects.toThrow(/returnFields entry .* is not a valid field path/); }); it("accepts a legit dot-path (Owner.Name) — the guard allows dotted segments", async () => { await expect( buildUpdate( { org: ORG, object: "Account", returnFields: ["Id", "Owner.Name"] }, noopPrimeDeps(), ), ).resolves.toBeDefined(); }); }); });