Skip to content

Commit

Permalink
Merge d608888 into a342596
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Apr 11, 2019
2 parents a342596 + d608888 commit 4596ce0
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 2 deletions.
7 changes: 7 additions & 0 deletions documentation/Arbitraries.md
Expand Up @@ -70,6 +70,13 @@ More specific strings:
- `fc.lorem()`, `fc.lorem(maxWordsCount: number)` or `fc.lorem(maxWordsCount: number, sentencesMode: boolean)` lorem ipsum strings. Generator can be configured by giving it a maximum number of characters by using `maxWordsCount` or switching the mode to sentences by setting `sentencesMode` to `true` in which case `maxWordsCount` is used to cap the number of sentences allowed
- `fc.ipV4()` IP v4 strings
- `fc.ipV6()` IP v6 strings
- `fc.domain()` Domain name with extension following RFC 1034, RFC 1123 and WHATWG URL Standard
- `fc.webAuthority()` Web authority following RFC 3986
- `fc.webFragments()` Fragments to build an URI. Fragment is the optional part right after the # in an URI
- `fc.webQueryParameters()` Query parameters to build an URI. Fragment is the optional part right after the ? in an URI
- `fc.webSegment()` Web URL path segment
- `fc.webUrl()` Web URL following the specs specified by RFC 3986 and WHATWG URL Standard
- `fc.emailAddress()` Email address following RFC 1123 and RFC 5322

## Combinors of arbitraries (:T)

Expand Down
17 changes: 17 additions & 0 deletions src/check/arbitrary/EmailArbitrary.ts
@@ -0,0 +1,17 @@
import { array } from './ArrayArbitrary';
import { buildLowerAlphaNumericArb } from './helpers/SpecificCharacterRange';
import { domain } from './HostArbitrary';
import { stringOf } from './StringArbitrary';
import { tuple } from './TupleArbitrary';

/**
* For email address
*
* According to RFC 5322 - https://www.ietf.org/rfc/rfc5322.txt
*/
export function emailAddress() {
const others = ['!', '#', '$', '%', '&', "'", '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~'];
const atextArb = buildLowerAlphaNumericArb(others);
const dotAtomArb = array(stringOf(atextArb, 1, 10), 1, 5).map(a => a.join('.'));
return tuple(dotAtomArb, domain()).map(([lp, d]) => `${lp}@${d}`);
}
41 changes: 41 additions & 0 deletions src/check/arbitrary/HostArbitrary.ts
@@ -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));
}
125 changes: 125 additions & 0 deletions src/check/arbitrary/WebArbitrary.ts
@@ -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}`}`);
}
43 changes: 43 additions & 0 deletions src/check/arbitrary/helpers/SpecificCharacterRange.ts
@@ -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
}
);
25 changes: 23 additions & 2 deletions src/fast-check-default.ts
Expand Up @@ -16,9 +16,11 @@ import { dedup } from './check/arbitrary/DedupArbitrary';
import { Arbitrary } from './check/arbitrary/definition/Arbitrary';
import { Shrinkable } from './check/arbitrary/definition/Shrinkable';
import { dictionary } from './check/arbitrary/DictionaryArbitrary';
import { emailAddress } from './check/arbitrary/EmailArbitrary';
import { double, float } from './check/arbitrary/FloatingPointArbitrary';
import { frequency } from './check/arbitrary/FrequencyArbitrary';
import { compareBooleanFunc, compareFunc, func } from './check/arbitrary/FunctionArbitrary';
import { domain } from './check/arbitrary/HostArbitrary';
import { integer, maxSafeInteger, maxSafeNat, nat } from './check/arbitrary/IntegerArbitrary';
import { ipV4, ipV6 } from './check/arbitrary/IpArbitrary';
import { lorem } from './check/arbitrary/LoremArbitrary';
Expand Down Expand Up @@ -49,6 +51,15 @@ import {
} from './check/arbitrary/StringArbitrary';
import { shuffledSubarray, subarray } from './check/arbitrary/SubarrayArbitrary';
import { genericTuple, tuple } from './check/arbitrary/TupleArbitrary';
import {
webAuthority,
WebAuthorityConstraints,
webFragments,
webQueryParameters,
webSegment,
webUrl,
WebUrlConstraints
} from './check/arbitrary/WebArbitrary';

import { AsyncCommand } from './check/model/command/AsyncCommand';
import { Command } from './check/model/command/Command';
Expand Down Expand Up @@ -111,8 +122,6 @@ export {
hexaString,
base64String,
lorem,
ipV4,
ipV6,
constant,
constantFrom,
clonedConstant,
Expand Down Expand Up @@ -140,6 +149,16 @@ export {
compareFunc,
func,
context,
// web
ipV4,
ipV6,
domain,
webAuthority,
webSegment,
webFragments,
webQueryParameters,
webUrl,
emailAddress,
// model-based
AsyncCommand,
Command,
Expand All @@ -158,6 +177,8 @@ export {
ObjectConstraints,
Parameters,
RecordConstraints,
WebAuthorityConstraints,
WebUrlConstraints,
RunDetails,
Random,
Stream,
Expand Down
64 changes: 64 additions & 0 deletions test/e2e/arbitraries/WebArbitrary.spec.ts
@@ -0,0 +1,64 @@
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 domain = /(^|@)([-a-z0-9\.]+)(:\d+$|$)/.exec(authority)![2];
const p = `http://${authority}`;
const u = new URL(p);
expect(u.hostname).toEqual(domain);
}
),
{ 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 }
);
});
});

0 comments on commit 4596ce0

Please sign in to comment.