-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/master' into fix_#250/support-po…
…stman-2.0.0-auth
- Loading branch information
Showing
2 changed files
with
131 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,10 @@ import { Sampler, OpenAPISchema } from './Sampler'; | |
import RandExp from 'randexp'; | ||
|
||
export class StringSampler implements Sampler { | ||
// ADHOC: 500 seems enough to protect sampler in case of patterns with infinite | ||
// quantifiers as the complexity may reach O(n^x) for the x quantifier nesting | ||
private readonly MAX_PATTERN_SAMPLE_LENGTH = 500; | ||
|
||
private readonly stringFormats = { | ||
'email': () => '[email protected]', | ||
'idn-email': () => 'джон.сноу@таргариен.укр', | ||
|
@@ -44,41 +48,73 @@ export class StringSampler implements Sampler { | |
|
||
const { minLength: min, maxLength: max } = schema; | ||
|
||
return this.checkLength(sampler(min || 0, max, schema), format, min, max); | ||
return this.checkLength(sampler(min, max, schema), format, min, max); | ||
} | ||
|
||
private patternSample( | ||
pattern: string | RegExp, | ||
min?: number, | ||
max?: number | ||
): string { | ||
this.assertLength(min, max); | ||
|
||
const randExp = new RandExp(pattern); | ||
randExp.randInt = (a, b) => Math.floor((a + b) / 2); | ||
|
||
if (min) { | ||
// ADHOC: make a probe for regex using min quantifier value | ||
// e.g. ^[a]+[b]+$ expect 'ab', ^[a-z]*$ expect '' | ||
if (min !== undefined) { | ||
return this.sampleWithMinLength(randExp, min, max); | ||
} | ||
|
||
randExp.max = 0; | ||
randExp.randInt = (a, _) => a; | ||
randExp.max = max ?? randExp.max; | ||
const result = randExp.gen(); | ||
|
||
const result = randExp.gen(); | ||
return max !== undefined && | ||
result.length > max && | ||
this.hasInfiniteQuantifier(pattern) | ||
? this.sampleWithMaxLength(randExp, max) | ||
: result; | ||
} | ||
|
||
if (result.length >= min) { | ||
return result; | ||
} | ||
private hasInfiniteQuantifier(pattern: string | RegExp) { | ||
const pat = typeof pattern === 'string' ? pattern : pattern.source; | ||
|
||
// ADHOC: fallback for failed min quantifier probe with doubled min length | ||
return /(\+|\*|\{\d*,\})/.test(pat); | ||
} | ||
|
||
randExp.max = 2 * min; | ||
randExp.randInt = (a, b) => Math.floor((a + b) / 2); | ||
private sampleWithMaxLength(randExp: RandExp, max: number): string { | ||
let result = ''; | ||
|
||
return this.adjustMaxLength(randExp.gen(), max); | ||
for (let i = 1; i <= Math.min(max, 20); i++) { | ||
randExp.max = Math.floor(max / i); | ||
result = randExp.gen(); | ||
|
||
if (result.length <= max) { | ||
break; | ||
} | ||
} | ||
|
||
randExp.max = max ?? randExp.max; | ||
randExp.randInt = (a, b) => Math.floor((a + b) / 2); | ||
return result; | ||
} | ||
|
||
private sampleWithMinLength(randExp: RandExp, min: number, max?: number) { | ||
// ADHOC: make a probe for regex using min quantifier value | ||
// e.g. ^[a]+[b]+$ expect 'ab', ^[a-z]*$ expect '' | ||
|
||
return randExp.gen(); | ||
const randInt = randExp.randInt; | ||
randExp.max = min; | ||
randExp.randInt = (a, _) => a; | ||
|
||
let result = randExp.gen(); | ||
|
||
randExp.randInt = randInt; | ||
|
||
if (result.length < min) { | ||
// ADHOC: fallback for failed min quantifier probe with doubled min length | ||
randExp.max = 2 * min; | ||
result = this.adjustMaxLength(randExp.gen(), max); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
private checkLength( | ||
|
@@ -107,6 +143,28 @@ export class StringSampler implements Sampler { | |
return value; | ||
} | ||
|
||
private assertLength(min?: number, max?: number) { | ||
const pairs = [ | ||
{ key: 'minLength', value: min }, | ||
{ key: 'maxLength', value: max } | ||
]; | ||
|
||
const boundariesStr = pairs | ||
.filter((p) => !this.checkBoundary(p.value)) | ||
.map((p) => `${p.key}=${p.value}`) | ||
.join(', '); | ||
|
||
if (boundariesStr) { | ||
throw new Error( | ||
`Sample string cannot be generated by boundaries: ${boundariesStr}. Both minLength and maxLength must not exceed ${this.MAX_PATTERN_SAMPLE_LENGTH}` | ||
); | ||
} | ||
} | ||
|
||
private checkBoundary(boundary?: number) { | ||
return boundary === undefined || boundary <= this.MAX_PATTERN_SAMPLE_LENGTH; | ||
} | ||
|
||
private adjustLength(sample: string, min: number, max: number): string { | ||
const minLength = min ? min : 0; | ||
const maxLength = max ? max : sample.length; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,16 @@ import { sample } from '../src'; | |
|
||
describe('StringSampler', () => { | ||
[ | ||
{ | ||
input: { | ||
maxLength: 55, | ||
minLength: 0, | ||
format: 'pattern', | ||
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$', | ||
type: 'string' | ||
}, | ||
expected: '[email protected]' | ||
}, | ||
{ | ||
input: { | ||
type: 'string', | ||
|
@@ -410,6 +420,36 @@ describe('StringSampler', () => { | |
}, | ||
expected: | ||
'Sample string cannot be generated by boundaries: maxLength=35, format=uuid' | ||
}, | ||
{ | ||
input: { | ||
maxLength: Number.MAX_SAFE_INTEGER, | ||
format: 'pattern', | ||
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$', | ||
type: 'string' | ||
}, | ||
expected: | ||
'Sample string cannot be generated by boundaries: maxLength=9007199254740991. Both minLength and maxLength must not exceed 500' | ||
}, | ||
{ | ||
input: { | ||
minLength: Number.MAX_SAFE_INTEGER, | ||
format: 'pattern', | ||
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$', | ||
type: 'string' | ||
}, | ||
expected: | ||
'Sample string cannot be generated by boundaries: minLength=9007199254740991. Both minLength and maxLength must not exceed 500' | ||
}, | ||
{ | ||
input: { | ||
maxLength: 501, | ||
format: 'pattern', | ||
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$', | ||
type: 'string' | ||
}, | ||
expected: | ||
'Sample string cannot be generated by boundaries: maxLength=501. Both minLength and maxLength must not exceed 500' | ||
} | ||
].forEach(({ input, expected }) => { | ||
const { type, ...restrictions } = input; | ||
|
@@ -421,4 +461,20 @@ describe('StringSampler', () => { | |
expect(result).toThrowError(expected); | ||
}); | ||
}); | ||
|
||
it.each([10, 100, 500])(`should handle maxLength=%d gracefully`, (input) => { | ||
// arrange | ||
const pattern = /^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{1,4}$/; | ||
|
||
// act | ||
const result = sample({ | ||
maxLength: input, | ||
format: 'pattern', | ||
pattern: pattern.source, | ||
type: 'string' | ||
}); | ||
|
||
// assert | ||
expect(result).toMatch(pattern); | ||
}); | ||
}); |