Skip to content

Commit 6425d00

Browse files
committed
Add test coverage for subscriptions, interfaces, operationFields, and diagnostics
- subscriptions.test.ts: 5 tests for subscription operations with arguments, arrays, and complex input types - interfaces-sdl.test.ts: 6 tests for interface type declarations, multiple interface implementations, interface inheritance, and optional fields - operation-fields-sdl.test.ts: 5 tests for @operationFields decorator SDL output including field arguments and interface operations - diagnostics.test.ts: 12 tests for emitter diagnostics including empty-schema, void-operation-return, duplicate union variants, interface validation errors, and operation-kind conflicts
1 parent ae89ef7 commit 6425d00

4 files changed

Lines changed: 1142 additions & 0 deletions

File tree

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
import { describe, expect, it } from "vitest";
2+
import { emitSingleSchemaWithDiagnostics } from "./test-host.js";
3+
4+
/**
5+
* Integration tests for GraphQL emitter diagnostics.
6+
* Tests that appropriate warnings and errors are reported.
7+
*/
8+
describe("diagnostics", () => {
9+
describe("empty-schema", () => {
10+
it("warns when schema has no operations", async () => {
11+
const code = `
12+
@schema
13+
namespace TestNamespace {
14+
model User {
15+
id: string;
16+
name: string;
17+
}
18+
}
19+
`;
20+
21+
const result = await emitSingleSchemaWithDiagnostics(code, {});
22+
const diagnostics = result.diagnostics.filter(
23+
(d) => d.code === "@typespec/graphql/empty-schema",
24+
);
25+
26+
expect(diagnostics).toHaveLength(1);
27+
expect(diagnostics[0].severity).toBe("warning");
28+
expect(diagnostics[0].message).toContain("at least one query operation");
29+
});
30+
31+
it("warns when schema has only mutations (no query)", async () => {
32+
const code = `
33+
@schema
34+
namespace TestNamespace {
35+
model User {
36+
id: string;
37+
}
38+
39+
@mutation
40+
op createUser(name: string): User;
41+
}
42+
`;
43+
44+
const result = await emitSingleSchemaWithDiagnostics(code, {});
45+
const diagnostics = result.diagnostics.filter(
46+
(d) => d.code === "@typespec/graphql/empty-schema",
47+
);
48+
49+
expect(diagnostics).toHaveLength(1);
50+
});
51+
52+
it("warns when schema has only subscriptions (no query)", async () => {
53+
const code = `
54+
@schema
55+
namespace TestNamespace {
56+
model Event {
57+
id: string;
58+
}
59+
60+
@subscription
61+
op onEvent(): Event;
62+
}
63+
`;
64+
65+
const result = await emitSingleSchemaWithDiagnostics(code, {});
66+
const diagnostics = result.diagnostics.filter(
67+
(d) => d.code === "@typespec/graphql/empty-schema",
68+
);
69+
70+
expect(diagnostics).toHaveLength(1);
71+
});
72+
});
73+
74+
describe("void-operation-return", () => {
75+
it("warns when operation returns void", async () => {
76+
const code = `
77+
@schema
78+
namespace TestNamespace {
79+
model User {
80+
id: string;
81+
}
82+
83+
@query
84+
op getUsers(): User[];
85+
86+
@mutation
87+
op doSomething(): void;
88+
}
89+
`;
90+
91+
const result = await emitSingleSchemaWithDiagnostics(code, {});
92+
const diagnostics = result.diagnostics.filter(
93+
(d) => d.code === "@typespec/graphql/void-operation-return",
94+
);
95+
96+
expect(diagnostics).toHaveLength(1);
97+
expect(diagnostics[0].severity).toBe("warning");
98+
expect(diagnostics[0].message).toContain("doSomething");
99+
});
100+
101+
it("warns for multiple void operations", async () => {
102+
const code = `
103+
@schema
104+
namespace TestNamespace {
105+
model User {
106+
id: string;
107+
}
108+
109+
@query
110+
op getUsers(): User[];
111+
112+
@mutation
113+
op doFirst(): void;
114+
115+
@mutation
116+
op doSecond(): void;
117+
}
118+
`;
119+
120+
const result = await emitSingleSchemaWithDiagnostics(code, {});
121+
const diagnostics = result.diagnostics.filter(
122+
(d) => d.code === "@typespec/graphql/void-operation-return",
123+
);
124+
125+
expect(diagnostics).toHaveLength(2);
126+
});
127+
});
128+
129+
describe("union diagnostics", () => {
130+
it("warns on duplicate union variants after flattening", async () => {
131+
const code = `
132+
@schema
133+
namespace TestNamespace {
134+
model A { a: string; }
135+
model B { b: string; }
136+
137+
union Inner {
138+
a: A,
139+
b: B,
140+
}
141+
142+
// Outer includes A directly and via Inner (which contains A)
143+
union Outer {
144+
a: A,
145+
inner: Inner,
146+
}
147+
148+
model Container {
149+
value: Outer;
150+
}
151+
152+
@query
153+
op get(): Container;
154+
}
155+
`;
156+
157+
const result = await emitSingleSchemaWithDiagnostics(code, {});
158+
const diagnostics = result.diagnostics.filter(
159+
(d) => d.code === "@typespec/graphql/duplicate-union-variant",
160+
);
161+
162+
// A appears twice after flattening Inner into Outer
163+
expect(diagnostics.length).toBeGreaterThanOrEqual(1);
164+
expect(diagnostics[0].severity).toBe("warning");
165+
});
166+
167+
});
168+
169+
describe("interface diagnostics", () => {
170+
it("errors when @compose used with non-interface", async () => {
171+
const code = `
172+
@schema
173+
namespace TestNamespace {
174+
model NotAnInterface {
175+
id: string;
176+
}
177+
178+
@compose(NotAnInterface)
179+
model User {
180+
id: string;
181+
name: string;
182+
}
183+
184+
@query
185+
op getUser(): User;
186+
}
187+
`;
188+
189+
const result = await emitSingleSchemaWithDiagnostics(code, {});
190+
const diagnostics = result.diagnostics.filter(
191+
(d) => d.code === "@typespec/graphql/invalid-interface",
192+
);
193+
194+
expect(diagnostics).toHaveLength(1);
195+
expect(diagnostics[0].severity).toBe("error");
196+
});
197+
198+
it("errors when interface property is missing", async () => {
199+
const code = `
200+
@schema
201+
namespace TestNamespace {
202+
@Interface
203+
model Node {
204+
id: string;
205+
}
206+
207+
@compose(Node)
208+
model User {
209+
// Missing 'id' property!
210+
name: string;
211+
}
212+
213+
@query
214+
op getUser(): User;
215+
}
216+
`;
217+
218+
const result = await emitSingleSchemaWithDiagnostics(code, {});
219+
const diagnostics = result.diagnostics.filter(
220+
(d) => d.code === "@typespec/graphql/missing-interface-property",
221+
);
222+
223+
expect(diagnostics).toHaveLength(1);
224+
expect(diagnostics[0].severity).toBe("error");
225+
});
226+
227+
it("errors when interface property type is incompatible", async () => {
228+
const code = `
229+
@schema
230+
namespace TestNamespace {
231+
@Interface
232+
model Node {
233+
id: string;
234+
}
235+
236+
@compose(Node)
237+
model User {
238+
id: int32; // Wrong type!
239+
name: string;
240+
}
241+
242+
@query
243+
op getUser(): User;
244+
}
245+
`;
246+
247+
const result = await emitSingleSchemaWithDiagnostics(code, {});
248+
const diagnostics = result.diagnostics.filter(
249+
(d) => d.code === "@typespec/graphql/incompatible-interface-property",
250+
);
251+
252+
expect(diagnostics).toHaveLength(1);
253+
expect(diagnostics[0].severity).toBe("error");
254+
});
255+
256+
it("errors on circular interface implementation", async () => {
257+
const code = `
258+
@schema
259+
namespace TestNamespace {
260+
@compose(SelfRef)
261+
@Interface
262+
model SelfRef {
263+
id: string;
264+
}
265+
266+
@compose(SelfRef)
267+
model User {
268+
id: string;
269+
}
270+
271+
@query
272+
op getUser(): User;
273+
}
274+
`;
275+
276+
const result = await emitSingleSchemaWithDiagnostics(code, {});
277+
const diagnostics = result.diagnostics.filter(
278+
(d) => d.code === "@typespec/graphql/circular-interface",
279+
);
280+
281+
expect(diagnostics).toHaveLength(1);
282+
expect(diagnostics[0].severity).toBe("error");
283+
});
284+
});
285+
286+
describe("operation-kind diagnostics", () => {
287+
it("errors when multiple operation kinds applied", async () => {
288+
const code = `
289+
@schema
290+
namespace TestNamespace {
291+
model User {
292+
id: string;
293+
}
294+
295+
@query
296+
op getUsers(): User[];
297+
298+
@query
299+
@mutation
300+
op conflicting(): User;
301+
}
302+
`;
303+
304+
const result = await emitSingleSchemaWithDiagnostics(code, {});
305+
const diagnostics = result.diagnostics.filter(
306+
(d) => d.code === "@typespec/graphql/graphql-operation-kind-duplicate",
307+
);
308+
309+
expect(diagnostics.length).toBeGreaterThanOrEqual(1);
310+
expect(diagnostics[0].severity).toBe("error");
311+
});
312+
});
313+
314+
describe("no errors for valid schemas", () => {
315+
it("produces no errors for well-formed schema", async () => {
316+
const code = `
317+
@schema
318+
namespace TestNamespace {
319+
@Interface
320+
model Node {
321+
id: string;
322+
}
323+
324+
@compose(Node)
325+
model User {
326+
id: string;
327+
name: string;
328+
}
329+
330+
@query
331+
op getUser(id: string): User;
332+
333+
@mutation
334+
op createUser(name: string): User;
335+
336+
@subscription
337+
op onUserCreated(): User;
338+
}
339+
`;
340+
341+
const result = await emitSingleSchemaWithDiagnostics(code, {});
342+
const errors = result.diagnostics.filter((d) => d.severity === "error");
343+
344+
expect(errors).toHaveLength(0);
345+
expect(result.graphQLOutput).toBeDefined();
346+
});
347+
});
348+
});

0 commit comments

Comments
 (0)