-
-
Notifications
You must be signed in to change notification settings - Fork 186
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes #302
- Loading branch information
Showing
8 changed files
with
372 additions
and
2 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
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 |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { array } from './ArrayArbitrary'; | ||
import { | ||
buildAlphaNumericPercentArb, | ||
buildLowerAlphaArb, | ||
buildLowerAlphaNumericArb | ||
} from './helpers/SpecificCharacterRange'; | ||
import { option } from './OptionArbitrary'; | ||
import { stringOf } from './StringArbitrary'; | ||
import { tuple } from './TupleArbitrary'; | ||
|
||
/** @hidden */ | ||
function subdomain() { | ||
const alphaNumericArb = buildLowerAlphaNumericArb([]); | ||
const alphaNumericHyphenArb = buildLowerAlphaNumericArb(['-']); | ||
return tuple(alphaNumericArb, option(tuple(stringOf(alphaNumericHyphenArb), alphaNumericArb))) | ||
.map(([f, d]) => (d === null ? f : `${f}${d[0]}${d[1]}`)) | ||
.filter(d => d.length <= 63); | ||
} | ||
|
||
/** | ||
* For domains | ||
* having an extension with at least two lowercase characters | ||
* | ||
* According to RFC 1034, RFC 1123 and WHATWG URL Standard | ||
* - https://www.ietf.org/rfc/rfc1034.txt | ||
* - https://www.ietf.org/rfc/rfc1123.txt | ||
* - https://url.spec.whatwg.org/ | ||
*/ | ||
export function domain() { | ||
const alphaNumericArb = buildLowerAlphaArb([]); | ||
const extensionArb = stringOf(alphaNumericArb, 2, 10); | ||
return tuple(array(subdomain(), 1, 5), extensionArb) | ||
.map(([mid, ext]) => `${mid.join('.')}.${ext}`) | ||
.filter(d => d.length <= 255); | ||
} | ||
|
||
/** @hidden */ | ||
export function hostUserInfo() { | ||
const others = ['-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':']; | ||
return stringOf(buildAlphaNumericPercentArb(others)); | ||
} |
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 |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { constant } from '../../fast-check-default'; | ||
import { array } from './ArrayArbitrary'; | ||
import { constantFrom } from './ConstantArbitrary'; | ||
import { buildAlphaNumericPercentArb } from './helpers/SpecificCharacterRange'; | ||
import { domain, hostUserInfo } from './HostArbitrary'; | ||
import { nat } from './IntegerArbitrary'; | ||
import { ipV4, ipV6 } from './IpArbitrary'; | ||
import { oneof } from './OneOfArbitrary'; | ||
import { option } from './OptionArbitrary'; | ||
import { stringOf } from './StringArbitrary'; | ||
import { tuple } from './TupleArbitrary'; | ||
|
||
export interface WebAuthorityConstraints { | ||
/** Enable IPv4 in host */ | ||
withIPv4?: boolean; | ||
/** Enable IPv6 in host */ | ||
withIPv6?: boolean; | ||
/** Enable user information prefix */ | ||
withUserInfo?: boolean; | ||
/** Enable port suffix */ | ||
withPort?: boolean; | ||
} | ||
|
||
/** | ||
* For web authority | ||
* | ||
* According to RFC 3986 - https://www.ietf.org/rfc/rfc3986.txt - `authority = [ userinfo "@" ] host [ ":" port ]` | ||
* | ||
* @param constraints | ||
*/ | ||
export function webAuthority(constraints?: WebAuthorityConstraints) { | ||
const c = constraints || {}; | ||
const hostnameArbs = [domain()] | ||
.concat(c.withIPv4 === true ? [ipV4()] : []) | ||
.concat(c.withIPv6 === true ? [ipV6().map(ip => `[${ip}]`)] : []); | ||
return tuple( | ||
c.withUserInfo === true ? option(hostUserInfo()) : constant(null), | ||
oneof(...hostnameArbs), | ||
c.withPort === true ? option(nat(65536)) : constant(null) | ||
).map(([u, h, p]) => (u === null ? '' : `${u}@`) + h + (p === null ? '' : `:${p}`)); | ||
} | ||
|
||
/** | ||
* For internal segment of an URI (web included) | ||
* | ||
* According to RFC 3986 - https://www.ietf.org/rfc/rfc3986.txt | ||
* | ||
* eg.: In the url `https://github.com/dubzzz/fast-check/`, `dubzzz` and `fast-check` are segments | ||
*/ | ||
export function webSegment() { | ||
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | ||
// segment = *pchar | ||
const others = ['-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':', '@']; | ||
return stringOf(buildAlphaNumericPercentArb(others)); | ||
} | ||
|
||
/** @hidden */ | ||
function uriQueryOrFragment() { | ||
// query = *( pchar / "/" / "?" ) | ||
// fragment = *( pchar / "/" / "?" ) | ||
const others = ['-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':', '@', '/', '?']; | ||
return stringOf(buildAlphaNumericPercentArb(others)); | ||
} | ||
|
||
/** | ||
* For query parameters of an URI (web included) | ||
* | ||
* According to RFC 3986 - https://www.ietf.org/rfc/rfc3986.txt | ||
* | ||
* eg.: In the url `https://domain/plop/?hello=1&world=2`, `?hello=1&world=2` are query parameters | ||
*/ | ||
export function webQueryParameters() { | ||
return uriQueryOrFragment(); | ||
} | ||
|
||
/** | ||
* For fragments of an URI (web included) | ||
* | ||
* According to RFC 3986 - https://www.ietf.org/rfc/rfc3986.txt | ||
* | ||
* eg.: In the url `https://domain/plop?page=1#hello=1&world=2`, `?hello=1&world=2` are query parameters | ||
*/ | ||
export function webFragments() { | ||
return uriQueryOrFragment(); | ||
} | ||
|
||
export interface WebUrlConstraints { | ||
/** Enforce specific schemes, eg.: http, https */ | ||
validSchemes?: string[]; | ||
/** Settings for {@see webAuthority} */ | ||
authoritySettings?: WebAuthorityConstraints; | ||
/** Enable query parameters in the generated url */ | ||
withQueryParameters?: boolean; | ||
/** Enable fragments in the generated url */ | ||
withFragments?: boolean; | ||
} | ||
|
||
/** | ||
* For web url | ||
* | ||
* According to RFC 3986 and WHATWG URL Standard | ||
* - https://www.ietf.org/rfc/rfc3986.txt | ||
* - https://url.spec.whatwg.org/ | ||
* | ||
* @param constraints | ||
*/ | ||
export function webUrl(constraints?: { | ||
validSchemes?: string[]; | ||
authoritySettings?: WebAuthorityConstraints; | ||
withQueryParameters?: boolean; | ||
withFragments?: boolean; | ||
}) { | ||
const c = constraints || {}; | ||
const validSchemes = c.validSchemes || ['http', 'https']; | ||
const schemeArb = constantFrom(...validSchemes); | ||
const authorityArb = webAuthority(c.authoritySettings); | ||
const pathArb = array(webSegment()).map(p => p.map(v => `/${v}`).join('')); | ||
return tuple( | ||
schemeArb, | ||
authorityArb, | ||
pathArb, | ||
c.withQueryParameters === true ? option(uriQueryOrFragment()) : constant(null), | ||
c.withFragments === true ? option(uriQueryOrFragment()) : constant(null) | ||
).map(([s, a, p, q, f]) => `${s}://${a}${p}${q === null ? '' : `?${q}`}${f === null ? '' : `#${f}`}`); | ||
} |
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 |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { fullUnicode } from '../CharacterArbitrary'; | ||
import { frequency } from '../FrequencyArbitrary'; | ||
import { mapToConstant } from '../MapToConstantArbitrary'; | ||
|
||
/** @hidden */ | ||
const lowerCaseMapper = { num: 26, build: (v: number) => String.fromCharCode(v + 0x61) }; | ||
|
||
/** @hidden */ | ||
const upperCaseMapper = { num: 26, build: (v: number) => String.fromCharCode(v + 0x41) }; | ||
|
||
/** @hidden */ | ||
const numericMapper = { num: 10, build: (v: number) => String.fromCharCode(v + 0x30) }; | ||
|
||
/** @hidden */ | ||
const percentCharArb = fullUnicode().map(c => { | ||
const encoded = encodeURIComponent(c); | ||
return c !== encoded ? encoded : `%${c.charCodeAt(0).toString(16)}`; // always %xy / no %x or %xyz | ||
}); | ||
|
||
/** @hidden */ | ||
export const buildLowerAlphaArb = (others: string[]) => | ||
mapToConstant(lowerCaseMapper, { num: others.length, build: v => others[v] }); | ||
|
||
/** @hidden */ | ||
export const buildLowerAlphaNumericArb = (others: string[]) => | ||
mapToConstant(lowerCaseMapper, numericMapper, { num: others.length, build: v => others[v] }); | ||
|
||
/** @hidden */ | ||
export const buildAlphaNumericArb = (others: string[]) => | ||
mapToConstant(lowerCaseMapper, upperCaseMapper, numericMapper, { num: others.length, build: v => others[v] }); | ||
|
||
/** @hidden */ | ||
export const buildAlphaNumericPercentArb = (others: string[]) => | ||
frequency( | ||
{ | ||
weight: 10, | ||
arbitrary: buildAlphaNumericArb(others) | ||
}, | ||
{ | ||
weight: 1, | ||
arbitrary: percentCharArb | ||
} | ||
); |
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
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 |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import * as fc from '../../../src/fast-check'; | ||
import { URL } from 'url'; | ||
|
||
const seed = Date.now(); | ||
describe(`WebArbitrary (seed: ${seed})`, () => { | ||
it('Should produce valid domains', () => { | ||
fc.assert( | ||
fc.property(fc.domain(), domain => { | ||
const p = `http://user:pass@${domain}/path/?query#fragment`; | ||
const u = new URL(p); | ||
expect(u.hostname).toEqual(domain); | ||
}), | ||
{ seed: seed } | ||
); | ||
}); | ||
it('Should produce valid authorities', () => { | ||
fc.assert( | ||
fc.property( | ||
fc.webAuthority({ | ||
withIPv4: false, | ||
withIPv6: false, | ||
withUserInfo: true, | ||
withPort: true | ||
}), | ||
authority => { | ||
const p = `http://${authority}`; | ||
const u = new URL(p); | ||
expect(u.hostname).toEqual('github.com'); | ||
} | ||
), | ||
{ seed: seed } | ||
); | ||
}); | ||
it('Should produce valid URL parts', () => { | ||
fc.assert( | ||
fc.property( | ||
fc.webAuthority({ withIPv4: true, withIPv6: true, withUserInfo: true, withPort: true }), | ||
fc.array(fc.webSegment()).map(p => p.map(v => `/${v}`).join('')), | ||
fc.webQueryParameters(), | ||
fc.webFragments(), | ||
(authority, path, query, fragment) => { | ||
const p = `http://${authority}${path}?${query}#${fragment}`; | ||
const u = new URL(p); | ||
expect({ search: decodeURIComponent(u.search), hash: u.hash }).toEqual({ | ||
search: query === '' ? '' : decodeURIComponent(`?${query}`), | ||
hash: fragment === '' ? '' : `#${fragment}` | ||
}); | ||
|
||
const dotSanitizedPath = path | ||
.replace(/\/(%2e|%2E)($|\/)/g, '/.$2') | ||
.replace(/\/(%2e|%2E)(%2e|%2E)($|\/)/g, '/..$3'); | ||
if (!dotSanitizedPath.includes('/..')) { | ||
const sanitizedPath = dotSanitizedPath | ||
.replace(/\/\.\/(\.\/)*/g, '/') // replace /./, /././, etc.. by / | ||
.replace(/\/\.$/, '/'); // replace trailing /. by / if any | ||
expect(u.pathname).toEqual(sanitizedPath === '' ? '/' : sanitizedPath); | ||
} | ||
} | ||
), | ||
{ seed: seed } | ||
); | ||
}); | ||
}); |
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 |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { domain } from '../../../../src/check/arbitrary/HostArbitrary'; | ||
|
||
import * as genericHelper from './generic/GenericArbitraryHelper'; | ||
|
||
const isValidDomain = (t: string) => { | ||
// According to https://www.ietf.org/rfc/rfc1034.txt | ||
// <domain> ::= <subdomain> | " " | ||
// <subdomain> ::= <label> | <subdomain> "." <label> | ||
// <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ] | ||
// <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> | ||
// <let-dig-hyp> ::= <let-dig> | "-" | ||
// <let-dig> ::= <letter> | <digit> | ||
// Relaxed by https://www.ietf.org/rfc/rfc1123.txt | ||
// allowing first character of subdomain to be a digit | ||
const rfc1123SubDomain = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/; | ||
return t.split('.').every(sd => rfc1123SubDomain.test(sd) && sd.length <= 63) && t.length <= 255; | ||
}; | ||
|
||
const isValidDomainWithExtension = (t: string) => { | ||
const subdomains = t.split('.'); | ||
return isValidDomain(t) && subdomains.length >= 2 && /^[a-z]{2,}$/.test(subdomains[subdomains.length - 1]); | ||
}; | ||
|
||
describe('DomainArbitrary', () => { | ||
describe('domain', () => { | ||
genericHelper.isValidArbitrary(() => domain(), { | ||
isValidValue: (g: string) => isValidDomainWithExtension(g) | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.