Skip to content

Commit 7ee878a

Browse files
committed
perf: add more test coverage
1 parent f4555aa commit 7ee878a

5 files changed

Lines changed: 842 additions & 7 deletions

File tree

src/validation/ValidationExecutor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export class ValidationExecutor {
185185
}
186186

187187
if (!hasConstraints(error.constraints)) {
188-
if (error.children.length === 0) {
188+
if (error.children?.length === 0) {
189189
return false;
190190
} else {
191191
delete error.constraints;

test/container.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { getFromContainer, useContainer } from '../src/container';
2+
3+
describe('container', () => {
4+
afterEach(() => {
5+
useContainer(
6+
{
7+
get() {
8+
return undefined;
9+
},
10+
},
11+
{ fallback: true }
12+
);
13+
});
14+
15+
it('should reuse the same default instance when fallback is enabled', () => {
16+
class Service {}
17+
18+
useContainer(
19+
{
20+
get() {
21+
return undefined;
22+
},
23+
},
24+
{ fallback: true }
25+
);
26+
27+
const first = getFromContainer(Service);
28+
const second = getFromContainer(Service);
29+
30+
expect(first).toBeInstanceOf(Service);
31+
expect(first).toBe(second);
32+
});
33+
34+
it('should fall back to the default container when fallbackOnErrors is enabled', () => {
35+
class ErrorService {}
36+
37+
useContainer(
38+
{
39+
get() {
40+
throw new Error('container failure');
41+
},
42+
},
43+
{ fallbackOnErrors: true }
44+
);
45+
46+
const first = getFromContainer(ErrorService);
47+
const second = getFromContainer(ErrorService);
48+
49+
expect(first).toBeInstanceOf(ErrorService);
50+
expect(first).toBe(second);
51+
});
52+
53+
it('should return the user container instance when one is provided', () => {
54+
class ExternalService {}
55+
const provided = { fromUserContainer: true };
56+
57+
useContainer({
58+
get() {
59+
return provided;
60+
},
61+
});
62+
63+
expect(getFromContainer(ExternalService)).toBe(provided);
64+
});
65+
});

test/functional/custom-decorators.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { registerDecorator } from '../../src/register-decorator';
44
import { ValidationOptions } from '../../src/decorator/ValidationOptions';
55
import { buildMessage, ValidatorConstraint } from '../../src/decorator/decorators';
66
import { ValidatorConstraintInterface } from '../../src/validation/ValidatorConstraintInterface';
7+
import { useContainer } from '../../src/container';
8+
import { getMetadataStorage, MetadataStorage } from '../../src/metadata/MetadataStorage';
9+
import { ConstraintMetadata } from '../../src/metadata/ConstraintMetadata';
710

811
const validator = new Validator();
912

@@ -276,6 +279,17 @@ describe('decorator with symbol constraint', () => {
276279
});
277280

278281
describe('inline custom decorator fast-path behavior', () => {
282+
afterEach(() => {
283+
useContainer(
284+
{
285+
get() {
286+
return undefined;
287+
},
288+
},
289+
{ fallback: true }
290+
);
291+
});
292+
279293
function InlineFailing(name: string, validationOptions?: ValidationOptions) {
280294
return function (object: object, propertyName: string): void {
281295
registerDecorator({
@@ -331,4 +345,58 @@ describe('inline custom decorator fast-path behavior', () => {
331345
expect(errors.length).toEqual(0);
332346
});
333347
});
348+
349+
it('should use an empty default message when inline validator does not provide one', () => {
350+
class NoDefaultMessageModel {}
351+
352+
registerDecorator({
353+
target: NoDefaultMessageModel,
354+
propertyName: 'value',
355+
name: 'noDefaultMessageValidator',
356+
validator: {
357+
validate(): boolean {
358+
return false;
359+
},
360+
},
361+
});
362+
363+
const metadata = getMetadataStorage()
364+
.getTargetValidationMetadatas(NoDefaultMessageModel, '', false, false)
365+
.find(entry => entry.propertyName === 'value');
366+
const constraint = getMetadataStorage().getTargetValidatorConstraints(metadata!.constraintCls)[0];
367+
368+
expect(constraint.instance.defaultMessage()).toEqual('');
369+
expect(metadata!.inlineDefaultMessage).toBeUndefined();
370+
});
371+
372+
it('should throw when multiple constraint implementations are returned for a validator class', () => {
373+
@ValidatorConstraint()
374+
class DuplicateConstraint implements ValidatorConstraintInterface {
375+
validate(): boolean {
376+
return true;
377+
}
378+
}
379+
380+
const metadataStorage = new MetadataStorage();
381+
metadataStorage.addConstraintMetadata(new ConstraintMetadata(DuplicateConstraint));
382+
metadataStorage.addConstraintMetadata(new ConstraintMetadata(DuplicateConstraint));
383+
384+
useContainer({
385+
get(target: Function) {
386+
if (target === MetadataStorage) {
387+
return metadataStorage;
388+
}
389+
390+
return new (target as any)();
391+
},
392+
});
393+
394+
expect(() =>
395+
registerDecorator({
396+
target: DuplicateConstraint,
397+
propertyName: 'value',
398+
validator: DuplicateConstraint,
399+
})
400+
).toThrow('More than one implementation of ValidatorConstraintInterface found');
401+
});
334402
});

test/functional/metadata-storage.spec.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { MetadataStorage } from '../../src/metadata/MetadataStorage';
2+
import { ConstraintMetadata } from '../../src/metadata/ConstraintMetadata';
23
import { ValidationMetadata } from '../../src/metadata/ValidationMetadata';
4+
import { ValidationSchema } from '../../src/validation-schema/ValidationSchema';
35
import { ValidationTypes } from '../../src/validation/ValidationTypes';
46

57
describe('MetadataStorage PR-2665 coverage', () => {
@@ -94,4 +96,175 @@ describe('MetadataStorage PR-2665 coverage', () => {
9496
expect(strictProperties).toContain('always');
9597
expect(strictProperties).not.toContain('grouped');
9698
});
99+
100+
it('should cache grouped metadata and reuse the same object for a cache key', () => {
101+
const storage = new MetadataStorage();
102+
const metadata = [createMetadata(ValidationTypes.CUSTOM_VALIDATION, 'prop')];
103+
104+
const first = storage.groupByPropertyName(metadata, 'group-cache-key');
105+
const second = storage.groupByPropertyName([], 'group-cache-key');
106+
107+
expect(second).toBe(first);
108+
expect(second.prop).toHaveLength(1);
109+
});
110+
111+
it('should append constraint metadata for the same target', () => {
112+
class TestConstraint {}
113+
114+
const storage = new MetadataStorage();
115+
storage.addConstraintMetadata(new ConstraintMetadata(TestConstraint, 'first'));
116+
storage.addConstraintMetadata(new ConstraintMetadata(TestConstraint, 'second'));
117+
118+
expect(storage.getTargetValidatorConstraints(TestConstraint)).toHaveLength(2);
119+
});
120+
121+
it('should cache target metadata results and include inherited grouped metadata', () => {
122+
class ParentTarget {}
123+
class ChildTarget extends ParentTarget {}
124+
125+
const storage = new MetadataStorage();
126+
const inheritedGrouped = new ValidationMetadata({
127+
type: ValidationTypes.CUSTOM_VALIDATION,
128+
target: ParentTarget,
129+
propertyName: 'inheritedGrouped',
130+
});
131+
inheritedGrouped.groups = ['g1'];
132+
133+
storage.addValidationMetadata(inheritedGrouped);
134+
135+
const cacheKey = storage.buildCacheKey(ChildTarget, '', false, false, ['g1']);
136+
const first = storage.getTargetValidationMetadatas(ChildTarget, '', false, false, ['g1'], cacheKey);
137+
const second = storage.getTargetValidationMetadatas(ChildTarget, '', false, false, ['g1'], cacheKey);
138+
139+
expect(second).toBe(first);
140+
expect(first.map(metadata => metadata.propertyName)).toContain('inheritedGrouped');
141+
});
142+
143+
it('should ignore inherited metadata whose target is not actually in the prototype chain', () => {
144+
class ParentTarget {}
145+
class ChildTarget extends ParentTarget {}
146+
class RogueTarget {}
147+
148+
const storage = new MetadataStorage();
149+
const rogueMetadata = new ValidationMetadata({
150+
type: ValidationTypes.CUSTOM_VALIDATION,
151+
target: RogueTarget,
152+
propertyName: 'rogue',
153+
});
154+
155+
(storage as any).validationMetadatas = new Map([[ParentTarget, [rogueMetadata]]]);
156+
157+
expect(storage.getTargetValidationMetadatas(ChildTarget, '', false, false)).toEqual([]);
158+
});
159+
160+
it('should transform validation schemas into metadata entries', () => {
161+
const storage = new MetadataStorage();
162+
const schema: ValidationSchema = {
163+
name: 'SchemaTarget',
164+
properties: {
165+
field: [
166+
{
167+
type: ValidationTypes.CUSTOM_VALIDATION,
168+
name: 'schemaConstraint',
169+
constraints: ['x'],
170+
message: 'schema message',
171+
},
172+
],
173+
},
174+
};
175+
176+
storage.addValidationSchema(schema);
177+
178+
const metadatas = (storage as any).validationMetadatas.get('SchemaTarget');
179+
expect(metadatas).toHaveLength(1);
180+
expect(metadatas[0].name).toEqual('schemaConstraint');
181+
expect(metadatas[0].propertyName).toEqual('field');
182+
});
183+
184+
it('should ignore original metadata entries whose target does not match the constructor or schema', () => {
185+
class ActualTarget {}
186+
class OtherTarget {}
187+
188+
const storage = new MetadataStorage();
189+
const mismatched = new ValidationMetadata({
190+
type: ValidationTypes.CUSTOM_VALIDATION,
191+
target: OtherTarget,
192+
propertyName: 'mismatch',
193+
});
194+
195+
(storage as any).validationMetadatas = new Map([[ActualTarget, [mismatched]]]);
196+
197+
expect(storage.getTargetValidationMetadatas(ActualTarget, '', false, false)).toEqual([]);
198+
});
199+
200+
it('should exclude string and self-targeted inherited metadata', () => {
201+
class ParentTarget {}
202+
class ChildTarget extends ParentTarget {}
203+
204+
const storage = new MetadataStorage();
205+
const schemaInherited = new ValidationMetadata({
206+
type: ValidationTypes.CUSTOM_VALIDATION,
207+
target: 'SchemaTarget',
208+
propertyName: 'schemaInherited',
209+
});
210+
const selfInherited = new ValidationMetadata({
211+
type: ValidationTypes.CUSTOM_VALIDATION,
212+
target: ChildTarget,
213+
propertyName: 'selfInherited',
214+
});
215+
const validInherited = new ValidationMetadata({
216+
type: ValidationTypes.CUSTOM_VALIDATION,
217+
target: ParentTarget,
218+
propertyName: 'validInherited',
219+
});
220+
validInherited.always = true;
221+
222+
(storage as any).validationMetadatas = new Map([[ParentTarget, [schemaInherited, selfInherited, validInherited]]]);
223+
224+
const metadatas = storage.getTargetValidationMetadatas(ChildTarget, '', false, true);
225+
226+
expect(metadatas.map(metadata => metadata.propertyName)).toEqual(['validInherited']);
227+
});
228+
229+
it('should prefer original metadata over inherited metadata with the same property and type', () => {
230+
class ParentTarget {}
231+
class ChildTarget extends ParentTarget {}
232+
233+
const storage = new MetadataStorage();
234+
const original = new ValidationMetadata({
235+
type: ValidationTypes.CUSTOM_VALIDATION,
236+
target: ChildTarget,
237+
propertyName: 'shared',
238+
});
239+
const inheritedDuplicate = new ValidationMetadata({
240+
type: ValidationTypes.CUSTOM_VALIDATION,
241+
target: ParentTarget,
242+
propertyName: 'shared',
243+
});
244+
245+
storage.addValidationMetadata(original);
246+
storage.addValidationMetadata(inheritedDuplicate);
247+
248+
const metadatas = storage.getTargetValidationMetadatas(ChildTarget, '', false, false);
249+
250+
expect(metadatas).toHaveLength(1);
251+
expect(metadatas[0]).toBe(original);
252+
});
253+
254+
it('should exclude inherited grouped metadata when strictGroups is enabled without groups', () => {
255+
class ParentTarget {}
256+
class ChildTarget extends ParentTarget {}
257+
258+
const storage = new MetadataStorage();
259+
const inheritedGrouped = new ValidationMetadata({
260+
type: ValidationTypes.CUSTOM_VALIDATION,
261+
target: ParentTarget,
262+
propertyName: 'inheritedGrouped',
263+
});
264+
inheritedGrouped.groups = ['g1'];
265+
266+
storage.addValidationMetadata(inheritedGrouped);
267+
268+
expect(storage.getTargetValidationMetadatas(ChildTarget, '', false, true)).toEqual([]);
269+
});
97270
});

0 commit comments

Comments
 (0)