@tdb/util
Version:
Shared helpers and utilities.
410 lines (362 loc) • 12.4 kB
text/typescript
import { Route, RouteOptions } from '.';
import { expect } from '../../test';
describe('url.Route', () => {
describe('constructor', () => {
it('default values', () => {
const route = Route.create(' /foo/:id ');
expect(route.method).to.eql('GET');
expect(route.path).to.eql('/foo/:id');
expect(route.toString()).to.eql('/foo/:id');
expect(route.title).to.eql(undefined);
expect(route.docs).to.eql(undefined);
expect(route.description).to.eql(undefined);
expect(route.meta).to.eql({});
});
it('given values', () => {
const route = Route.create('/foo/:id', {
title: 'My Title',
docs: '/docs',
description: 'Does...',
meta: { foo: 123 },
});
expect(route.title).to.eql('My Title');
expect(route.docs).to.eql('/docs');
expect(route.description).to.eql('Does...');
expect(route.meta).to.eql({ foo: 123 });
});
it('sets REST method', () => {
const route = Route.create('/foo/:id', { method: 'POST' });
expect(route.method).to.eql('POST');
});
it('symbols', () => {
const route = Route.create('/foo/:id', {
schema: { params: 'IFoo', query: 'IBam', data: 'IBoo' },
});
expect(route.schema.params).to.eql('IFoo');
expect(route.schema.query).to.eql('IBam');
expect(route.schema.data).to.eql('IBoo');
});
it('title/docs', () => {
const route = Route.create('/foo/:id', {
title: 'MyRoute',
docs: '/docs/my-route',
});
expect(route.title).to.eql('MyRoute');
expect(route.docs).to.eql('/docs/my-route');
});
});
describe('clone', () => {
it('clones with same values', () => {
const route = Route.create('/foo/:id');
const clone = route.clone();
expect(route).to.not.equal(clone);
});
it('clones with new value', () => {
const route = Route.create('/foo/:id', {});
const clone = route.clone({ path: '/bar' });
expect(route).to.not.equal(clone);
expect(clone.path).to.eql('/bar');
});
});
describe('toObject', () => {
it('with default values', () => {
const route = Route.create('/');
const obj = route.toObject();
expect(obj.path).to.eql('/');
expect(obj.method).to.eql('GET');
expect(obj.tokens).to.eql(['/']);
expect(obj.schema).to.eql({});
});
it('with specific values', () => {
const route = Route.create('/foo/:id', {
method: 'POST',
schema: { params: 'IFoo' },
});
const obj = route.toObject();
expect(obj.path).to.eql('/foo/:id');
expect(obj.method).to.eql('POST');
expect(obj.tokens.length).to.eql(2);
expect(obj.tokens[0]).to.eql('/foo');
expect(obj.tokens[1]).to.eql({
name: 'id',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
partial: false,
});
expect(obj.schema).to.eql({ params: 'IFoo' });
});
});
describe('isMatch', () => {
const test = (
url: string | undefined,
result: boolean,
options?: Partial<RouteOptions>,
) => {
const route = Route.create('/foo/:id', { ...options });
expect(route.isMatch(url)).to.eql(result);
};
it('is a match', () => {
test('/foo/123', true);
test('/foo/abc', true);
test('/foo/abc?q=123', true);
test('/foo/abc/', true, { strict: false });
test('/Foo/bar', true, { caseSensitive: false }); // Case sentitive (no match)
});
it('is not a match', () => {
test(undefined, false);
test('', false);
test('/', false);
test('/foobar', false);
test('/foo/abc/', false); // Strict (by default)
test('/Foo/bar', false); // Case sentitive (no match)
});
it('multi-part', () => {
const route = Route.create('/o/:name/:path*');
const path = '/o/acme/foo/bar';
expect(route.isMatch(path)).to.eql(true);
expect(route.params(path)).to.eql({ name: 'acme', path: 'foo/bar' });
});
});
describe('params (strongly typed)', () => {
type IParams = {
org: string;
id: number | string;
};
const route = Route.create<IParams>('/foo/:org/:id');
it('no url (undefined}', () => {
const res = route.params();
expect(res).to.eql({});
});
it(':id as string', () => {
const res = route.params('/foo/ibm/hello');
expect(res.org).to.eql('ibm');
expect(res.id).to.eql('hello');
});
it(':id as number', () => {
const res = route.params('/foo/ibm/123');
expect(res.org).to.eql('ibm');
expect(res.id).to.eql(123);
});
it(':id as boolean', () => {
const res = route.params('/foo/ibm/TruE');
expect(res.org).to.eql('ibm');
expect(res.id).to.eql(true);
});
it('has escaped colon (:) in route', () => {
const route = Route.create<{ id: number }>('/foo::id');
const res = route.params('/foo:123');
expect(res).to.eql({ id: 123 });
});
});
describe('params', () => {
it('wildcard (*)', () => {
const route = Route.create<{ bar: string }>('/foo/:bar*');
const res = route.params('/foo/bar/bax');
expect(res.bar).to.eql('bar/bax');
});
it('removes query-string', () => {
const route = Route.create<{ id: number }>('/foo/:id?q=:srch');
const res = route.params('/foo/123?q=search');
expect(res.id).to.eql(123);
});
});
describe('query', () => {
type IParams = { id: number | string };
type IQuery = { q: string; force?: boolean };
const route = Route.create<IParams, IQuery>('/foo/:id');
it('as string', () => {
const query = route.query('/foo/123?q=abc');
expect(query.q).to.eql('abc');
});
it('as number', () => {
const query = route.query('/foo/123?q=456');
expect(query.q).to.eql(456);
});
it('as boolean', () => {
const query = route.query('/foo/123?q=truE');
expect(query.q).to.eql(true);
});
it('flag', () => {
const test = (url: string, result?: boolean) => {
const query = route.query(url);
expect(query.force).to.eql(result);
};
test('/foo?force', true);
test('/foo?force=true', true);
test('/foo?force=false', false);
test('/foo', undefined);
});
});
describe('url', () => {
type IParams = { id: number | string };
type IQuery = { q: string; force?: boolean; f?: boolean };
const URL = '/foo/123?q=hello&force';
const route = Route.create<IParams, IQuery>('/foo/:id', {});
const url = route.url(URL);
it('creates URL', () => {
expect(url.route).to.eql(route);
expect(url.path).to.eql(URL);
expect(url.toString()).to.eql(URL);
expect(url.params).to.eql({ id: 123 });
expect(url.query).to.eql({ q: 'hello', force: true });
});
it('url with origin (domain)', () => {
const complete = `http://localhost:3000/foo/123?q=hello&force`;
const res1 = url.toString({ origin: 'http://localhost:3000' });
const res2 = url.toString({ origin: 'http://localhost:3000/' });
const res3 = url.toString({ origin: 'http://localhost:3000///' });
expect(res1).to.eql(complete);
expect(res2).to.eql(complete);
expect(res3).to.eql(complete);
});
it('hasFlag', () => {
expect(url.hasFlag('force')).to.eql(true);
expect(url.hasFlag('f')).to.eql(false);
expect(url.hasFlag(['f', 'force'])).to.eql(true);
expect(url.hasFlag()).to.eql(false);
expect(url.hasFlag('bob')).to.eql(false);
});
});
describe('toUrl', () => {
type IQuery = { q: string; force?: boolean; f?: boolean };
type IParams = {
org: string;
id: number | string;
};
const route = Route.create<IParams, IQuery>('/foo/:org/:id');
it('no params (error)', () => {
const fn = () => {
route.toUrl({ params: {} as any });
};
expect(fn).to.throw(/Expected "org"/);
});
it('params only', () => {
const url = route.toUrl({
params: { org: 'acme', id: 123 },
}).s;
expect(url).to.eql('/foo/acme/123');
});
it('query as [:param]', () => {
const route = Route.create('/manifest?path=:path&markdown');
const url = route.toUrl({ params: { path: '/my/path' } });
expect(url.toString()).to.eql('/manifest?path=%2Fmy%2Fpath&markdown');
expect(url.query).to.eql({ path: '/my/path', markdown: true });
expect(url.params).to.eql({ path: '%2Fmy%2Fpath' });
});
it('query as [:param] as well as {object}', () => {
const route = Route.create('/manifest?path=:path&markdown');
const url = route.toUrl({
params: { path: '**' },
query: { s: 'find' },
});
expect(url.toString()).to.eql('/manifest?path=**&markdown&s=find');
expect(url.query).to.eql({ path: '**', markdown: true, s: 'find' });
});
it('encoded', () => {
const org = 'café verona';
const url = route.toUrl({
params: { org, id: 123 },
}).s;
expect(url).to.eql('/foo/caf%C3%A9%20verona/123');
expect(decodeURI(url)).to.eql('/foo/café verona/123');
});
it('params and query-string', () => {
const url = route
.toUrl({
params: { org: 'acme', id: 123 },
query: { q: 'search' },
})
.toString();
expect(url).to.eql('/foo/acme/123?q=search');
});
it('multi part query-string', () => {
const url = route
.toUrl({
params: { org: 'acme', id: 123 },
query: { q: 'hello', f: true },
})
.toString();
expect(url).to.eql('/foo/acme/123?q=hello&f=true');
});
it('query-string with no value (undeined)', () => {
const url = route
.toUrl({
params: { org: 'acme', id: 123 },
query: { q: 'hello', f: undefined },
})
.toString();
expect(url).to.eql('/foo/acme/123?q=hello');
});
it('encoded query-string', () => {
const url = route
.toUrl({
params: { org: 'acme', id: 123 },
query: { q: 'foo bar' },
})
.toString();
expect(url).to.eql('/foo/acme/123?q=foo%20bar');
});
it('has no origin by default', () => {
const url = route.toUrl({ params: { org: 'acme', id: 123 } });
expect(url.origin).to.eql(undefined);
expect(url.toString()).to.eql('/foo/acme/123');
});
it('sets a default origin', () => {
const url = route.toUrl({
params: { org: 'acme', id: 123 },
origin: 'http://localhost:8080//',
});
expect(url.origin).to.eql('http://localhost:8080');
expect(url.toString()).to.eql('http://localhost:8080/foo/acme/123');
// NB: Override with parameter.
expect(url.toString({ origin: 'localhost:3000' })).to.eql(
'localhost:3000/foo/acme/123',
);
});
});
describe('walk/map/find', () => {
const TREE = {
id: 123,
foo: Route.create('/foo'),
bar: {
baz: Route.create('/foo/bar'),
label: 'Hello',
zoo: {
child: Route.create('/deep'),
},
},
};
it('walks a tree', () => {
let routes: Route[] = [];
Route.walk(TREE, route => (routes = [...routes, route]));
expect(routes.length).to.eql(3);
expect(routes[0].path).to.eql('/foo');
expect(routes[1].path).to.eql('/foo/bar');
expect(routes[2].path).to.eql('/deep');
});
it('walks a tree and stops before end', () => {
let count = 0;
Route.walk(TREE, (route, e) => {
if (count >= 1) {
e.stop();
}
count++;
});
expect(count).to.eql(2);
});
it('maps a tree', () => {
const res = Route.map(TREE, route => route.path);
expect(res).to.eql(['/foo', '/foo/bar', '/deep']);
});
it('finds a route', () => {
const res1 = Route.find(TREE, r => r.path.startsWith('/foo'));
const res2 = Route.find(TREE, r => r.path.startsWith('/deep'));
const res3 = Route.find(TREE, r => r.path.startsWith('/__NOT_EXIST__'));
expect(res1 && res1.path).to.eql('/foo');
expect(res2 && res2.path).to.eql('/deep');
expect(res3).to.eql(undefined);
});
});
});