Skip to content

Commit 8c4d407

Browse files
test(shared): add comprehensive tests for validation enrichment functions
1 parent a82ea63 commit 8c4d407

File tree

1 file changed

+289
-0
lines changed

1 file changed

+289
-0
lines changed
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
parseDocumentWithPositions,
4+
enrichWithDocumentPositions
5+
} from './validation-enrichment.js';
6+
import { ValidationOutcome, ValidationOutput } from './validation.output.js';
7+
8+
describe('parseDocumentWithPositions', () => {
9+
it('should parse valid JSON and return context with data and parseResult', () => {
10+
const content = '{"nodes": [{"unique-id": "node-1"}]}';
11+
const result = parseDocumentWithPositions(content, 'architecture');
12+
13+
expect(result).toBeDefined();
14+
expect(result!.id).toBe('architecture');
15+
expect(result!.data).toEqual({ nodes: [{ 'unique-id': 'node-1' }] });
16+
expect(result!.parseResult).toBeDefined();
17+
expect(result!.parseResult.data).toEqual({ nodes: [{ 'unique-id': 'node-1' }] });
18+
});
19+
20+
it('should return context with diagnostics for malformed JSON', () => {
21+
const content = '{"nodes": [invalid json}';
22+
const result = parseDocumentWithPositions(content, 'test');
23+
24+
// @stoplight/json doesn't throw, it returns diagnostics
25+
expect(result).toBeDefined();
26+
expect(result!.parseResult.diagnostics.length).toBeGreaterThan(0);
27+
});
28+
29+
it('should return undefined for empty string', () => {
30+
const content = '';
31+
const result = parseDocumentWithPositions(content, 'test');
32+
33+
// Empty string parses to undefined data
34+
expect(result).toBeDefined();
35+
expect(result!.data).toBeUndefined();
36+
});
37+
38+
it('should handle JSON with different data types', () => {
39+
const content = '{"string": "value", "number": 42, "boolean": true, "null": null, "array": [1,2,3]}';
40+
const result = parseDocumentWithPositions(content, 'mixed');
41+
42+
expect(result).toBeDefined();
43+
expect(result!.data).toEqual({
44+
string: 'value',
45+
number: 42,
46+
boolean: true,
47+
null: null,
48+
array: [1, 2, 3]
49+
});
50+
});
51+
52+
it('should preserve the id in the returned context', () => {
53+
const content = '{}';
54+
55+
const result1 = parseDocumentWithPositions(content, 'architecture');
56+
const result2 = parseDocumentWithPositions(content, 'pattern');
57+
58+
expect(result1!.id).toBe('architecture');
59+
expect(result2!.id).toBe('pattern');
60+
});
61+
});
62+
63+
describe('enrichWithDocumentPositions', () => {
64+
const createValidationOutput = (path: string, source?: string): ValidationOutput => {
65+
return new ValidationOutput('test-code', 'error', 'Test message', path, undefined, undefined, undefined, undefined, undefined, source);
66+
};
67+
68+
const createOutcome = (outputs: ValidationOutput[]): ValidationOutcome => {
69+
return new ValidationOutcome(outputs, [], true, false);
70+
};
71+
72+
it('should enrich validation output with line numbers from parsed document', () => {
73+
const content = `{
74+
"nodes": [
75+
{
76+
"unique-id": "node-1",
77+
"name": "Test Node"
78+
}
79+
]
80+
}`;
81+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
82+
const output = createValidationOutput('/nodes/0');
83+
const outcome = createOutcome([output]);
84+
85+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
86+
87+
expect(output.line_start).toBeDefined();
88+
expect(output.line_end).toBeDefined();
89+
expect(output.line_start).toBeGreaterThan(0);
90+
});
91+
92+
it('should handle outputs with unique-id based paths', () => {
93+
const content = `{
94+
"nodes": [
95+
{"unique-id": "api-gateway", "name": "API Gateway"},
96+
{"unique-id": "database", "name": "Database"}
97+
]
98+
}`;
99+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
100+
const output = createValidationOutput('/nodes/database');
101+
const outcome = createOutcome([output]);
102+
103+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
104+
105+
// Should find the node by unique-id and provide line numbers
106+
expect(output.line_start).toBeDefined();
107+
expect(output.path).toBe('/nodes/database'); // Path preserved with id
108+
});
109+
110+
it('should handle missing context gracefully', () => {
111+
const output = createValidationOutput('/nodes/0');
112+
const outcome = createOutcome([output]);
113+
114+
// No contexts provided
115+
enrichWithDocumentPositions(outcome, {});
116+
117+
// Should not throw, output unchanged
118+
expect(output.line_start).toBeUndefined();
119+
expect(output.line_end).toBeUndefined();
120+
});
121+
122+
it('should handle null outcome gracefully', () => {
123+
const parseContext = parseDocumentWithPositions('{}', 'architecture')!;
124+
125+
// Should not throw
126+
expect(() => {
127+
enrichWithDocumentPositions(null as unknown as ValidationOutcome, { architecture: parseContext });
128+
}).not.toThrow();
129+
});
130+
131+
it('should handle outcome with no allValidationOutputs method', () => {
132+
const parseContext = parseDocumentWithPositions('{}', 'architecture')!;
133+
const badOutcome = {} as ValidationOutcome;
134+
135+
// Should not throw
136+
expect(() => {
137+
enrichWithDocumentPositions(badOutcome, { architecture: parseContext });
138+
}).not.toThrow();
139+
});
140+
141+
it('should infer architecture source when not specified', () => {
142+
const content = '{"nodes": []}';
143+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
144+
const output = createValidationOutput('/nodes', undefined); // no source
145+
const outcome = createOutcome([output]);
146+
147+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
148+
149+
expect(output.source).toBe('architecture');
150+
});
151+
152+
it('should infer pattern source when only pattern context available', () => {
153+
const content = '{"nodes": []}';
154+
const parseContext = parseDocumentWithPositions(content, 'pattern')!;
155+
const output = createValidationOutput('/nodes', undefined);
156+
const outcome = createOutcome([output]);
157+
158+
enrichWithDocumentPositions(outcome, { pattern: parseContext });
159+
160+
expect(output.source).toBe('pattern');
161+
});
162+
163+
it('should use explicit source from output when available', () => {
164+
const archContent = '{"nodes": [{"unique-id": "a"}]}';
165+
const patternContent = '{"nodes": [{"unique-id": "b"}]}';
166+
const archContext = parseDocumentWithPositions(archContent, 'architecture')!;
167+
const patternContext = parseDocumentWithPositions(patternContent, 'pattern')!;
168+
169+
const output = createValidationOutput('/nodes/0', 'pattern');
170+
const outcome = createOutcome([output]);
171+
172+
enrichWithDocumentPositions(outcome, {
173+
architecture: archContext,
174+
pattern: patternContext
175+
});
176+
177+
expect(output.source).toBe('pattern');
178+
});
179+
180+
it('should handle deeply nested paths', () => {
181+
const content = `{
182+
"nodes": [
183+
{
184+
"unique-id": "service",
185+
"interfaces": [
186+
{
187+
"unique-id": "api",
188+
"port": 8080
189+
}
190+
]
191+
}
192+
]
193+
}`;
194+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
195+
const output = createValidationOutput('/nodes/0/interfaces/0/port');
196+
const outcome = createOutcome([output]);
197+
198+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
199+
200+
expect(output.line_start).toBeDefined();
201+
});
202+
203+
it('should handle invalid path gracefully', () => {
204+
const content = '{"nodes": []}';
205+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
206+
const output = createValidationOutput('/nonexistent/path/here');
207+
const outcome = createOutcome([output]);
208+
209+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
210+
211+
// Should not throw, line numbers remain undefined
212+
expect(output.line_start).toBeUndefined();
213+
});
214+
215+
it('should handle path without leading slash', () => {
216+
const content = '{"nodes": []}';
217+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
218+
const output = createValidationOutput('nodes'); // no leading slash
219+
const outcome = createOutcome([output]);
220+
221+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
222+
223+
// Invalid JSON pointer format, should not crash
224+
expect(output.line_start).toBeUndefined();
225+
});
226+
227+
it('should handle empty path', () => {
228+
const content = '{"nodes": []}';
229+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
230+
const output = createValidationOutput('');
231+
const outcome = createOutcome([output]);
232+
233+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
234+
235+
// Empty path, should not crash
236+
expect(output.line_start).toBeUndefined();
237+
});
238+
239+
it('should rewrite array index paths to use unique-ids', () => {
240+
const content = `{
241+
"relationships": [
242+
{"unique-id": "rel-1", "source": "a", "target": "b"},
243+
{"unique-id": "rel-2", "source": "c", "target": "d"}
244+
]
245+
}`;
246+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
247+
const output = createValidationOutput('/relationships/1');
248+
const outcome = createOutcome([output]);
249+
250+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
251+
252+
// Path should be rewritten to use unique-id
253+
expect(output.path).toBe('/relationships/rel-2');
254+
});
255+
256+
it('should handle multiple outputs in single outcome', () => {
257+
const content = `{
258+
"nodes": [
259+
{"unique-id": "node-1"},
260+
{"unique-id": "node-2"}
261+
]
262+
}`;
263+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
264+
const output1 = createValidationOutput('/nodes/0');
265+
const output2 = createValidationOutput('/nodes/1');
266+
const outcome = createOutcome([output1, output2]);
267+
268+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
269+
270+
expect(output1.line_start).toBeDefined();
271+
expect(output2.line_start).toBeDefined();
272+
expect(output1.line_start).not.toBe(output2.line_start);
273+
});
274+
275+
it('should handle spectral outputs as well as json schema outputs', () => {
276+
const content = '{"nodes": [{"unique-id": "n1"}]}';
277+
const parseContext = parseDocumentWithPositions(content, 'architecture')!;
278+
279+
const jsonOutput = createValidationOutput('/nodes/0');
280+
const spectralOutput = createValidationOutput('/nodes/0');
281+
282+
const outcome = new ValidationOutcome([jsonOutput], [spectralOutput], true, false);
283+
284+
enrichWithDocumentPositions(outcome, { architecture: parseContext });
285+
286+
expect(jsonOutput.line_start).toBeDefined();
287+
expect(spectralOutput.line_start).toBeDefined();
288+
});
289+
});

0 commit comments

Comments
 (0)