Skip to content

Commit 0f6422e

Browse files
michaeloboyleclaude
andcommitted
test: Improve test coverage for validation and NodeQuery
- Add comprehensive validation.test.ts (27 tests, 100% coverage) - Cover all edge cases for node/edge type validation - Test schema enforcement for properties and relationships - Test node ID validation edge cases - Enhance NodeQuery.test.ts with COUNT(DISTINCT) test - Add test for count() with 'both' direction (line 376 coverage) - Ensures proper duplicate handling in bidirectional queries - Brings NodeQuery.ts to 100% coverage Test results: - validation.ts: 100% coverage (was 38-67%) - NodeQuery.ts: 100% coverage (was 98.93%) - All 294 tests passing 🤖 Generated with Claude Code Co-Authored-By: Claude <[email protected]>
1 parent dd0c9ca commit 0f6422e

File tree

2 files changed

+354
-0
lines changed

2 files changed

+354
-0
lines changed

tests/unit/NodeQuery.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,24 @@ describe('NodeQuery', () => {
640640

641641
expect(countAll).toBe(countLimited);
642642
});
643+
644+
it('should use COUNT(DISTINCT) for both direction to avoid duplicates', () => {
645+
// Create bidirectional relationship
646+
const person1 = db.createNode('Person', { name: 'Alice' });
647+
const person2 = db.createNode('Person', { name: 'Bob' });
648+
649+
// Create edges in both directions (simulating bidirectional KNOWS relationship)
650+
db.createEdge(person1.id, 'KNOWS', person2.id);
651+
db.createEdge(person2.id, 'KNOWS', person1.id);
652+
653+
// Count with 'both' direction should use DISTINCT to avoid counting duplicates
654+
const count = db.nodes('Person')
655+
.connectedTo('Person', 'KNOWS', 'both')
656+
.count();
657+
658+
// Both persons should be counted once, not twice
659+
expect(count).toBe(2);
660+
});
643661
});
644662

645663
describe('exists() predicate', () => {

tests/unit/validation.test.ts

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
/**
2+
* Comprehensive tests for validation utilities
3+
*
4+
* Target coverage for src/utils/validation.ts:
5+
* - Line 25: Edge schema validation
6+
* - Line 38: Properties type validation
7+
* - Line 48: Undefined properties warning
8+
* - Lines 62-75: validateEdgeRelationship function
9+
*/
10+
11+
import {
12+
validateNodeType,
13+
validateEdgeType,
14+
validateNodeProperties,
15+
validateEdgeRelationship,
16+
validateNodeId
17+
} from '../../src/utils/validation';
18+
import { GraphSchema } from '../../src/types';
19+
20+
describe('Validation Utilities', () => {
21+
describe('validateNodeType', () => {
22+
it('should accept valid node type without schema', () => {
23+
expect(() => validateNodeType('Person')).not.toThrow();
24+
expect(() => validateNodeType('Job')).not.toThrow();
25+
});
26+
27+
it('should throw on empty node type', () => {
28+
expect(() => validateNodeType('')).toThrow('Node type must be a non-empty string');
29+
});
30+
31+
it('should throw on non-string node type', () => {
32+
expect(() => validateNodeType(123 as any)).toThrow('Node type must be a non-empty string');
33+
expect(() => validateNodeType(null as any)).toThrow('Node type must be a non-empty string');
34+
expect(() => validateNodeType(undefined as any)).toThrow('Node type must be a non-empty string');
35+
});
36+
37+
it('should validate against schema when provided', () => {
38+
const schema: GraphSchema = {
39+
nodes: {
40+
Person: { properties: ['name', 'email'] },
41+
Company: { properties: ['name'] }
42+
},
43+
edges: {}
44+
};
45+
46+
expect(() => validateNodeType('Person', schema)).not.toThrow();
47+
expect(() => validateNodeType('Company', schema)).not.toThrow();
48+
});
49+
50+
it('should throw when node type not in schema', () => {
51+
const schema: GraphSchema = {
52+
nodes: {
53+
Person: { properties: ['name'] }
54+
},
55+
edges: {}
56+
};
57+
58+
expect(() => validateNodeType('InvalidType', schema))
59+
.toThrow("Node type 'InvalidType' is not defined in schema");
60+
});
61+
});
62+
63+
describe('validateEdgeType', () => {
64+
it('should accept valid edge type without schema', () => {
65+
expect(() => validateEdgeType('KNOWS')).not.toThrow();
66+
expect(() => validateEdgeType('WORKS_AT')).not.toThrow();
67+
});
68+
69+
it('should throw on empty edge type', () => {
70+
expect(() => validateEdgeType('')).toThrow('Edge type must be a non-empty string');
71+
});
72+
73+
it('should throw on non-string edge type', () => {
74+
expect(() => validateEdgeType(123 as any)).toThrow('Edge type must be a non-empty string');
75+
expect(() => validateEdgeType(null as any)).toThrow('Edge type must be a non-empty string');
76+
expect(() => validateEdgeType(undefined as any)).toThrow('Edge type must be a non-empty string');
77+
});
78+
79+
it('should validate against schema when provided', () => {
80+
const schema: GraphSchema = {
81+
nodes: {},
82+
edges: {
83+
KNOWS: { from: 'Person', to: 'Person' },
84+
WORKS_AT: { from: 'Person', to: 'Company' }
85+
}
86+
};
87+
88+
expect(() => validateEdgeType('KNOWS', schema)).not.toThrow();
89+
expect(() => validateEdgeType('WORKS_AT', schema)).not.toThrow();
90+
});
91+
92+
it('should throw when edge type not in schema (line 25 coverage)', () => {
93+
const schema: GraphSchema = {
94+
nodes: {},
95+
edges: {
96+
KNOWS: { from: 'Person', to: 'Person' }
97+
}
98+
};
99+
100+
expect(() => validateEdgeType('INVALID_EDGE', schema))
101+
.toThrow("Edge type 'INVALID_EDGE' is not defined in schema");
102+
});
103+
});
104+
105+
describe('validateNodeProperties', () => {
106+
it('should accept valid properties object', () => {
107+
expect(() => validateNodeProperties('Person', { name: 'Alice' })).not.toThrow();
108+
expect(() => validateNodeProperties('Job', { title: 'Engineer', salary: 100000 })).not.toThrow();
109+
});
110+
111+
it('should throw on non-object properties (line 38 coverage)', () => {
112+
expect(() => validateNodeProperties('Person', null as any))
113+
.toThrow('Properties must be an object');
114+
expect(() => validateNodeProperties('Person', undefined as any))
115+
.toThrow('Properties must be an object');
116+
expect(() => validateNodeProperties('Person', 'invalid' as any))
117+
.toThrow('Properties must be an object');
118+
expect(() => validateNodeProperties('Person', 123 as any))
119+
.toThrow('Properties must be an object');
120+
});
121+
122+
it('should validate properties against schema', () => {
123+
const schema: GraphSchema = {
124+
nodes: {
125+
Person: { properties: ['name', 'email', 'age'] }
126+
},
127+
edges: {}
128+
};
129+
130+
expect(() =>
131+
validateNodeProperties('Person', { name: 'Alice', email: '[email protected]' }, schema)
132+
).not.toThrow();
133+
});
134+
135+
it('should warn about undefined properties in schema (line 48 coverage)', () => {
136+
const schema: GraphSchema = {
137+
nodes: {
138+
Person: { properties: ['name', 'email'] }
139+
},
140+
edges: {}
141+
};
142+
143+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
144+
145+
validateNodeProperties('Person', {
146+
name: 'Alice',
147+
148+
undefinedProp1: 'value1',
149+
undefinedProp2: 'value2'
150+
}, schema);
151+
152+
expect(consoleWarnSpy).toHaveBeenCalledWith(
153+
expect.stringContaining("Node type 'Person' has undefined properties: undefinedProp1, undefinedProp2")
154+
);
155+
156+
consoleWarnSpy.mockRestore();
157+
});
158+
159+
it('should not warn when all properties are defined', () => {
160+
const schema: GraphSchema = {
161+
nodes: {
162+
Person: { properties: ['name', 'email'] }
163+
},
164+
edges: {}
165+
};
166+
167+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
168+
169+
validateNodeProperties('Person', {
170+
name: 'Alice',
171+
172+
}, schema);
173+
174+
expect(consoleWarnSpy).not.toHaveBeenCalled();
175+
176+
consoleWarnSpy.mockRestore();
177+
});
178+
});
179+
180+
describe('validateEdgeRelationship', () => {
181+
it('should skip validation when no schema provided (lines 62-64 coverage)', () => {
182+
expect(() =>
183+
validateEdgeRelationship('KNOWS', 'Person', 'Person')
184+
).not.toThrow();
185+
186+
expect(() =>
187+
validateEdgeRelationship('WORKS_AT', 'Person', 'Company')
188+
).not.toThrow();
189+
});
190+
191+
it('should skip validation when schema has no edges (lines 62-64 coverage)', () => {
192+
const schema: GraphSchema = {
193+
nodes: {},
194+
edges: {}
195+
};
196+
197+
expect(() =>
198+
validateEdgeRelationship('KNOWS', 'Person', 'Person', schema)
199+
).not.toThrow();
200+
});
201+
202+
it('should skip validation when edge type not in schema (lines 62-64 coverage)', () => {
203+
const schema: GraphSchema = {
204+
nodes: {},
205+
edges: {
206+
KNOWS: { from: 'Person', to: 'Person' }
207+
}
208+
};
209+
210+
expect(() =>
211+
validateEdgeRelationship('WORKS_AT', 'Person', 'Company', schema)
212+
).not.toThrow();
213+
});
214+
215+
it('should validate "from" node type constraint (lines 68-72 coverage)', () => {
216+
const schema: GraphSchema = {
217+
nodes: {},
218+
edges: {
219+
WORKS_AT: { from: 'Person', to: 'Company' }
220+
}
221+
};
222+
223+
expect(() =>
224+
validateEdgeRelationship('WORKS_AT', 'Person', 'Company', schema)
225+
).not.toThrow();
226+
227+
expect(() =>
228+
validateEdgeRelationship('WORKS_AT', 'Company', 'Company', schema)
229+
).toThrow("Edge type 'WORKS_AT' requires 'from' node type 'Person', got 'Company'");
230+
});
231+
232+
it('should validate "to" node type constraint (lines 74-78 coverage)', () => {
233+
const schema: GraphSchema = {
234+
nodes: {},
235+
edges: {
236+
WORKS_AT: { from: 'Person', to: 'Company' }
237+
}
238+
};
239+
240+
expect(() =>
241+
validateEdgeRelationship('WORKS_AT', 'Person', 'Company', schema)
242+
).not.toThrow();
243+
244+
expect(() =>
245+
validateEdgeRelationship('WORKS_AT', 'Person', 'Person', schema)
246+
).toThrow("Edge type 'WORKS_AT' requires 'to' node type 'Company', got 'Person'");
247+
});
248+
249+
it('should validate both "from" and "to" constraints', () => {
250+
const schema: GraphSchema = {
251+
nodes: {},
252+
edges: {
253+
POSTED_BY: { from: 'Job', to: 'Company' }
254+
}
255+
};
256+
257+
expect(() =>
258+
validateEdgeRelationship('POSTED_BY', 'Job', 'Company', schema)
259+
).not.toThrow();
260+
261+
expect(() =>
262+
validateEdgeRelationship('POSTED_BY', 'Person', 'Company', schema)
263+
).toThrow("Edge type 'POSTED_BY' requires 'from' node type 'Job', got 'Person'");
264+
265+
expect(() =>
266+
validateEdgeRelationship('POSTED_BY', 'Job', 'Person', schema)
267+
).toThrow("Edge type 'POSTED_BY' requires 'to' node type 'Company', got 'Person'");
268+
});
269+
270+
it('should accept edges with matching from and to types', () => {
271+
const schema: GraphSchema = {
272+
nodes: {},
273+
edges: {
274+
FOLLOWS: { from: 'Person', to: 'Person' }
275+
}
276+
};
277+
278+
expect(() =>
279+
validateEdgeRelationship('FOLLOWS', 'Person', 'Person', schema)
280+
).not.toThrow();
281+
});
282+
283+
it('should handle multiple edge types in schema', () => {
284+
const schema: GraphSchema = {
285+
nodes: {},
286+
edges: {
287+
WORKS_AT: { from: 'Person', to: 'Company' },
288+
POSTED_BY: { from: 'Job', to: 'Company' },
289+
APPLIED_TO: { from: 'Person', to: 'Job' }
290+
}
291+
};
292+
293+
expect(() =>
294+
validateEdgeRelationship('WORKS_AT', 'Person', 'Company', schema)
295+
).not.toThrow();
296+
297+
expect(() =>
298+
validateEdgeRelationship('POSTED_BY', 'Job', 'Company', schema)
299+
).not.toThrow();
300+
301+
expect(() =>
302+
validateEdgeRelationship('APPLIED_TO', 'Person', 'Job', schema)
303+
).not.toThrow();
304+
305+
// Verify constraints still enforced
306+
expect(() =>
307+
validateEdgeRelationship('WORKS_AT', 'Company', 'Person', schema)
308+
).toThrow("Edge type 'WORKS_AT' requires 'from' node type 'Person', got 'Company'");
309+
});
310+
});
311+
312+
describe('validateNodeId', () => {
313+
it('should accept valid positive integer IDs', () => {
314+
expect(() => validateNodeId(1)).not.toThrow();
315+
expect(() => validateNodeId(100)).not.toThrow();
316+
expect(() => validateNodeId(999999)).not.toThrow();
317+
});
318+
319+
it('should throw on non-integer IDs', () => {
320+
expect(() => validateNodeId(1.5)).toThrow('Node ID must be a positive integer');
321+
expect(() => validateNodeId(3.14)).toThrow('Node ID must be a positive integer');
322+
});
323+
324+
it('should throw on zero or negative IDs', () => {
325+
expect(() => validateNodeId(0)).toThrow('Node ID must be a positive integer');
326+
expect(() => validateNodeId(-1)).toThrow('Node ID must be a positive integer');
327+
expect(() => validateNodeId(-100)).toThrow('Node ID must be a positive integer');
328+
});
329+
330+
it('should throw on non-number IDs', () => {
331+
expect(() => validateNodeId('1' as any)).toThrow('Node ID must be a positive integer');
332+
expect(() => validateNodeId(null as any)).toThrow('Node ID must be a positive integer');
333+
expect(() => validateNodeId(undefined as any)).toThrow('Node ID must be a positive integer');
334+
});
335+
});
336+
});

0 commit comments

Comments
 (0)