Skip to content

Commit ec77240

Browse files
committed
add expectedFieldsStack and validate extra fields after traversal
1 parent bce0e70 commit ec77240

File tree

3 files changed

+105
-11
lines changed

3 files changed

+105
-11
lines changed

src/methods/validate-fixture-input.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,34 @@ import {
1313
} from "graphql";
1414
import { inlineNamedFragmentSpreads } from "../utils/inline-named-fragment-spreads.js";
1515

16+
/**
17+
* Checks fixture objects for fields that are not present in the GraphQL query.
18+
*
19+
* @param fixtureObjects - Array of fixture objects to validate
20+
* @param expectedFields - Set of field names that are expected based on the query
21+
* @returns Array of error messages for any extra fields found (empty if valid)
22+
*
23+
* @remarks
24+
* Only validates object types - skips null values and arrays.
25+
*/
26+
function checkForExtraFields(
27+
fixtureObjects: any[],
28+
expectedFields: Set<string>
29+
): string[] {
30+
const errors: string[] = [];
31+
for (const fixtureObject of fixtureObjects) {
32+
if (typeof fixtureObject === "object" && fixtureObject !== null && !Array.isArray(fixtureObject)) {
33+
const fixtureFields = Object.keys(fixtureObject);
34+
for (const fixtureField of fixtureFields) {
35+
if (!expectedFields.has(fixtureField)) {
36+
errors.push(`Extra field "${fixtureField}" found in fixture data not in query`);
37+
}
38+
}
39+
}
40+
}
41+
return errors;
42+
}
43+
1644
export interface ValidateFixtureInputResult {
1745
errors: string[];
1846
}
@@ -37,6 +65,7 @@ export function validateFixtureInput(
3765
const inlineFragmentSpreadsAst = inlineNamedFragmentSpreads(queryAST);
3866
const typeInfo = new TypeInfo(schema);
3967
const valueStack: any[][] = [[value]];
68+
const expectedFieldsStack: Set<string>[] = [new Set()]; // Initial set tracks root level fields
4069
const errors: string[] = [];
4170
visit(
4271
inlineFragmentSpreadsAst,
@@ -48,6 +77,9 @@ export function validateFixtureInput(
4877

4978
const responseKey = node.alias?.value || node.name.value;
5079

80+
// Track this field as expected in the parent's set
81+
expectedFieldsStack[expectedFieldsStack.length - 1].add(responseKey);
82+
5183
const fieldDefinition = typeInfo.getFieldDef();
5284
const fieldType = fieldDefinition?.type;
5385

@@ -124,10 +156,21 @@ export function validateFixtureInput(
124156
}
125157
}
126158

159+
// If this field has nested selections, prepare to track expected child fields
160+
if (node.selectionSet) {
161+
expectedFieldsStack.push(new Set<string>());
162+
}
163+
127164
valueStack.push(nestedValues);
128165
},
129-
leave() {
130-
valueStack.pop();
166+
leave(node) {
167+
const nestedValues = valueStack.pop()!;
168+
169+
// If this field had nested selections, check for extra fields
170+
if (node.selectionSet) {
171+
const expectedFields = expectedFieldsStack.pop()!;
172+
errors.push(...checkForExtraFields(nestedValues, expectedFields));
173+
}
131174
},
132175
},
133176
SelectionSet: {
@@ -155,6 +198,11 @@ export function validateFixtureInput(
155198
},
156199
})
157200
);
201+
202+
// The query's root SelectionSet has no parent Field node, so there's no Field.leave event to check it.
203+
// We manually perform the same check here that would happen in Field.leave for nested objects.
204+
errors.push(...checkForExtraFields(valueStack[0], expectedFieldsStack[0]));
205+
158206
return { errors };
159207
}
160208

test/methods/validate-fixture-input.test.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,12 @@ describe("validateFixtureInput", () => {
7474
firstItems: [
7575
{
7676
id: "gid://test/Item/1",
77-
count: 5,
78-
details: {
79-
name: "First Item"
80-
}
77+
count: 5
8178
}
8279
],
8380
secondItems: [
8481
{
8582
id: "gid://test/Item/1",
86-
count: 5,
8783
details: {
8884
name: "First Item"
8985
}
@@ -729,7 +725,7 @@ describe("validateFixtureInput", () => {
729725
// in fixture data that aren't present in the query. Currently, it only validates
730726
// that all required fields from the query are present in the fixture, but doesn't
731727
// flag additional fields that shouldn't be there.
732-
it.skip("detects extra fields not in query", () => {
728+
it("detects extra fields not in query", () => {
733729
const queryAST = parse(`
734730
query Query {
735731
data {
@@ -938,5 +934,54 @@ describe("validateFixtureInput", () => {
938934
expect(result.errors).toHaveLength(1);
939935
expect(result.errors[0]).toBe('Cannot validate nonExistentField: missing type information');
940936
});
937+
938+
it("detects extra fields with multiple aliases for the same field", () => {
939+
const queryAST = parse(`
940+
query Query {
941+
data {
942+
firstItems: items {
943+
id
944+
count
945+
}
946+
secondItems: items {
947+
id
948+
details {
949+
name
950+
}
951+
}
952+
}
953+
}
954+
`);
955+
956+
const fixtureInput = {
957+
data: {
958+
firstItems: [
959+
{
960+
id: "gid://test/Item/1",
961+
count: 5,
962+
details: {
963+
name: "First Item"
964+
}
965+
}
966+
],
967+
secondItems: [
968+
{
969+
id: "gid://test/Item/1",
970+
count: 5,
971+
details: {
972+
name: "First Item"
973+
}
974+
}
975+
]
976+
}
977+
};
978+
979+
const result = validateFixtureInput(queryAST, schema, fixtureInput);
980+
981+
// Each alias is validated independently, so extra fields in each should be detected
982+
expect(result.errors).toHaveLength(2);
983+
expect(result.errors[0]).toBe('Extra field "details" found in fixture data not in query');
984+
expect(result.errors[1]).toBe('Extra field "count" found in fixture data not in query');
985+
});
941986
});
942987
});

test/methods/validate-test-assets.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,11 @@ describe('validateTestAssets', () => {
140140

141141
expect(result.inputQuery.errors).toHaveLength(0);
142142

143-
// Input fixture should be invalid due to missing fields
144-
expect(result.inputFixture.errors.length).toBe(2);
143+
// Input fixture should be invalid due to missing fields and extra field
144+
expect(result.inputFixture.errors.length).toBe(3);
145145
expect(result.inputFixture.errors[0]).toBe('Missing expected fixture data for details');
146-
expect(result.inputFixture.errors[1]).toBe('Missing expected fixture data for metadata');
146+
expect(result.inputFixture.errors[1]).toBe('Extra field "invalidField" found in fixture data not in query');
147+
expect(result.inputFixture.errors[2]).toBe('Missing expected fixture data for metadata');
147148

148149
expect(result.outputFixture.errors).toHaveLength(0);
149150
});

0 commit comments

Comments
 (0)