diff --git a/src/packages/application/errors/controller-missing.js b/src/errors/controller-missing-error.js similarity index 76% rename from src/packages/application/errors/controller-missing.js rename to src/errors/controller-missing-error.js index 5f8ef34c..b00867a5 100644 --- a/src/packages/application/errors/controller-missing.js +++ b/src/errors/controller-missing-error.js @@ -3,7 +3,7 @@ /** * @private */ -class ControllerMissingError extends Error { +class ControllerMissingError extends ReferenceError { constructor(resource: string) { super(`Could not resolve controller by name '${resource}'`); } diff --git a/src/errors/index.js b/src/errors/index.js deleted file mode 100644 index 03cfc77b..00000000 --- a/src/errors/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as ModuleMissingError } from './module-missing'; diff --git a/src/errors/module-missing.js b/src/errors/module-missing-error.js similarity index 88% rename from src/errors/module-missing.js rename to src/errors/module-missing-error.js index 13d34fd6..8d1be899 100644 --- a/src/errors/module-missing.js +++ b/src/errors/module-missing-error.js @@ -6,7 +6,7 @@ import { line } from '../packages/logger'; /** * @private */ -class ModuleMissingError extends Error { +class ModuleMissingError extends ReferenceError { constructor(name: string) { super(line` ${red(`Could not find required module '${name}'.`)} diff --git a/src/packages/application/errors/serializer-missing.js b/src/errors/serializer-missing-error.js similarity index 76% rename from src/packages/application/errors/serializer-missing.js rename to src/errors/serializer-missing-error.js index 3b58dbd6..947f4d9c 100644 --- a/src/packages/application/errors/serializer-missing.js +++ b/src/errors/serializer-missing-error.js @@ -3,7 +3,7 @@ /** * @private */ -class SerializerMissingError extends Error { +class SerializerMissingError extends ReferenceError { constructor(resource: string) { super(`Could not resolve serializer by name '${resource}'`); } diff --git a/src/packages/application/errors/index.js b/src/packages/application/errors/index.js deleted file mode 100644 index 9e16fefa..00000000 --- a/src/packages/application/errors/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -export { default as ControllerMissingError } from './controller-missing'; -export { default as SerializerMissingError } from './serializer-missing'; diff --git a/src/packages/application/index.js b/src/packages/application/index.js index 7f549d90..3681e874 100644 --- a/src/packages/application/index.js +++ b/src/packages/application/index.js @@ -124,5 +124,4 @@ class Application { } export default Application; - export type { Application$opts, Application$factoryOpts } from './interfaces'; diff --git a/src/packages/application/initialize.js b/src/packages/application/initialize.js index f6fb772a..2321102a 100644 --- a/src/packages/application/initialize.js +++ b/src/packages/application/initialize.js @@ -6,8 +6,8 @@ import Router from '../router'; import Server from '../server'; import { build, createLoader } from '../loader'; import { freezeProps, deepFreezeProps } from '../freezeable'; +import ControllerMissingError from '../../errors/controller-missing-error'; -import { ControllerMissingError } from './errors'; import normalizePort from './utils/normalize-port'; import createController from './utils/create-controller'; import createSerializer from './utils/create-serializer'; diff --git a/src/packages/application/utils/create-controller.js b/src/packages/application/utils/create-controller.js index de2caf6f..02485256 100644 --- a/src/packages/application/utils/create-controller.js +++ b/src/packages/application/utils/create-controller.js @@ -41,17 +41,16 @@ export default function createController( const instance: T = Reflect.construct(constructor, [{ model, namespace, - serializer, - serializers + serializer }]); if (serializer) { if (!instance.filter.length) { - instance.filter = [].concat(serializer.attributes); + instance.filter = [...serializer.attributes]; } if (!instance.sort.length) { - instance.sort = [].concat(serializer.attributes); + instance.sort = [...serializer.attributes]; } } diff --git a/src/packages/database/model/index.js b/src/packages/database/model/index.js index de474b1b..74d35ee2 100644 --- a/src/packages/database/model/index.js +++ b/src/packages/database/model/index.js @@ -57,6 +57,11 @@ class Model { */ dirtyAttributes: Set; + /** + * @private + */ + isModelInstance: boolean; + /** * @private */ @@ -145,21 +150,24 @@ class Model { enumerable: false, configurable: false }, - initialValues: { value: new Map(), writable: false, enumerable: false, configurable: false }, - dirtyAttributes: { value: new Set(), writable: false, enumerable: false, configurable: false }, - + isModelInstance: { + value: true, + writable: false, + enumerable: false, + configurable: false + }, prevAssociations: { value: new Set(), writable: false, diff --git a/src/packages/database/relationship/utils/validate-type.js b/src/packages/database/relationship/utils/validate-type.js index b1d25c14..d44c723b 100644 --- a/src/packages/database/relationship/utils/validate-type.js +++ b/src/packages/database/relationship/utils/validate-type.js @@ -3,7 +3,7 @@ import isNull from '../../../../utils/is-null'; import type { Model } from '../../index'; function validateOne(model: Class, value: void | ?mixed) { - return isNull(value) || value instanceof model; + return isNull(value) || model.isInstance(value); } export default function validateType(model: Class, value: mixed) { diff --git a/src/packages/database/utils/connect.js b/src/packages/database/utils/connect.js index f727a8d5..62023371 100644 --- a/src/packages/database/utils/connect.js +++ b/src/packages/database/utils/connect.js @@ -3,8 +3,8 @@ import { join as joinPath } from 'path'; import { NODE_ENV, DATABASE_URL } from '../../../constants'; import { VALID_DRIVERS } from '../constants'; import { tryCatchSync } from '../../../utils/try-catch'; -import { ModuleMissingError } from '../../../errors'; import { InvalidDriverError } from '../errors'; +import ModuleMissingError from '../../../errors/module-missing-error'; /** * @private diff --git a/src/packages/loader/builder/utils/create-children-builder.js b/src/packages/loader/builder/utils/create-children-builder.js index 719117ab..1669df17 100644 --- a/src/packages/loader/builder/utils/create-children-builder.js +++ b/src/packages/loader/builder/utils/create-children-builder.js @@ -1,5 +1,4 @@ // @flow -import setType from '../../../../utils/set-type'; import type { Builder$Construct, Builder$ChildrenBuilder } from '../interfaces'; export default function createChildrenBuilder( @@ -15,13 +14,13 @@ export default function createChildrenBuilder( if (parent && normalized.endsWith('application')) { return [ normalized, - setType(() => constructor) + parent ]; } return [ normalized, - construct(name, constructor, parent) + construct(normalized, constructor, parent) ]; })); } diff --git a/src/packages/router/definitions/context/index.js b/src/packages/router/definitions/context/index.js index b842ea5c..459dad11 100644 --- a/src/packages/router/definitions/context/index.js +++ b/src/packages/router/definitions/context/index.js @@ -4,6 +4,9 @@ import Namespace from '../../namespace'; import K from '../../../../utils/k'; import type { Router$Namespace } from '../../index'; // eslint-disable-line max-len, no-unused-vars import type { Router$DefinitionBuilder } from '../interfaces'; +import { + default as ControllerMissingError +} from '../../../../errors/controller-missing-error'; import createDefinitionGroup from './utils/create-definition-group'; import normalizeResourceArgs from './utils/normalize-resource-args'; @@ -48,10 +51,11 @@ export function contextFor(build: Router$DefinitionBuilder<*>) { path = isRoot ? `/${name}` : `${path}/${name}`; - let controller = controllers.get(`${path.substr(1)}/application`); + const controllerKey = `${path.substr(1)}/application`; + const controller = controllers.get(controllerKey); if (!controller) { - controller = namespace.controller; + throw new ControllerMissingError(controllerKey); } const child = new Namespace({ @@ -77,10 +81,11 @@ export function contextFor(build: Router$DefinitionBuilder<*>) { path = namespace.path + opts.path; } - let controller = controllers.get(path.substr(1)); + const controllerKey = path.substr(1); + const controller = controllers.get(controllerKey); if (!controller) { - controller = namespace.controller; + throw new ControllerMissingError(controllerKey); } const child = new Resource({ diff --git a/src/packages/router/definitions/context/utils/normalize-resource-args.js b/src/packages/router/definitions/context/utils/normalize-resource-args.js index 03b85d0d..9228bc29 100644 --- a/src/packages/router/definitions/context/utils/normalize-resource-args.js +++ b/src/packages/router/definitions/context/utils/normalize-resource-args.js @@ -7,7 +7,7 @@ import type { Controller$builtIn } from '../../../../controller'; // eslint-disa */ export default function normalizeResourceArgs(args: [ string, - ?{ path: string, only: Array }, + { path: string, only: Array }, Function ]): [{ name: string, diff --git a/src/packages/router/definitions/test/normalize-resource-args.test.js b/src/packages/router/definitions/test/normalize-resource-args.test.js new file mode 100644 index 00000000..72056ecf --- /dev/null +++ b/src/packages/router/definitions/test/normalize-resource-args.test.js @@ -0,0 +1,107 @@ +// @flow +import { expect } from 'chai'; +import { it, describe, before } from 'mocha'; + +import { BUILT_IN_ACTIONS } from '../../../controller'; + +import normalizeResourceArgs from '../context/utils/normalize-resource-args'; + +describe('module "router/definitions/context"', () => { + describe('util normalizeResourceArgs()', () => { + it('normalizes arguments with a name only', () => { + // $FlowIgnore + const result = normalizeResourceArgs(['posts']); + + expect(result).to.be.an('array'); + + expect(result) + .to.have.property('0') + .and.deep.equal({ + name: 'posts', + path: '/posts', + only: BUILT_IN_ACTIONS + }); + + expect(result) + .to.have.property('1') + .and.be.a('function'); + }); + + it('normalizes arguments with a name and options', () => { + // $FlowIgnore + const result = normalizeResourceArgs(['posts', { + only: [ + 'show', + 'index' + ] + }]); + + expect(result).to.be.an('array'); + + expect(result) + .to.have.property('0') + .and.deep.equal({ + name: 'posts', + path: '/posts', + only: [ + 'show', + 'index' + ] + }); + + expect(result) + .to.have.property('1') + .and.be.a('function'); + }); + + it('normalizes arguments with a name and builder', () => { + // $FlowIgnore + const result = normalizeResourceArgs(['posts', function () { + return undefined; + }]); + + expect(result).to.be.an('array'); + + expect(result) + .to.have.property('0') + .and.deep.equal({ + name: 'posts', + path: '/posts', + only: BUILT_IN_ACTIONS + }); + + expect(result) + .to.have.property('1') + .and.be.a('function'); + }); + + it('normalizes arguments with a name, options, and builder', () => { + // $FlowIgnore + const result = normalizeResourceArgs(['posts', { + only: [ + 'show', + 'index' + ] + }, function () { + return undefined; + }]); + + expect(result).to.be.an('array'); + + expect(result) + .to.have.property('0') + .and.deep.equal({ + name: 'posts', + path: '/posts', + only: [ + 'show', + 'index' + ] + }); + + expect(result) + .to.have.property('1') + .and.be.a('function'); + }); + }); +}); diff --git a/src/packages/router/route/action/constants.js b/src/packages/router/route/action/constants.js index 9a18f78b..77d0abb2 100644 --- a/src/packages/router/route/action/constants.js +++ b/src/packages/router/route/action/constants.js @@ -1,2 +1,2 @@ // @flow -export const FINAL_HANDLER = '__FINAL__HANDLER__'; +export const FINAL_HANDLER = '__FINAL_HANDLER__'; diff --git a/src/packages/router/route/action/enhancers/resource.js b/src/packages/router/route/action/enhancers/resource.js index a293a716..32d12e2a 100644 --- a/src/packages/router/route/action/enhancers/resource.js +++ b/src/packages/router/route/action/enhancers/resource.js @@ -1,5 +1,5 @@ // @flow -import { Model, Query } from '../../../../database'; +import { Query } from '../../../../database'; import { getDomain } from '../../../../server'; import createPageLinks from '../utils/create-page-links'; import type { Action } from '../interfaces'; @@ -24,17 +24,15 @@ export default function resource(action: Action): Action { data = await result; } - if (Array.isArray(data) || data instanceof Model) { + if (Array.isArray(data) || (data && data.isModelInstance)) { const domain = getDomain(req); const { params, - url: { path, pathname }, - route: { controller: { namespace, diff --git a/src/packages/router/route/action/index.js b/src/packages/router/route/action/index.js index 3974dd10..2afd9039 100644 --- a/src/packages/router/route/action/index.js +++ b/src/packages/router/route/action/index.js @@ -19,12 +19,13 @@ export function createAction( fn = resource(fn); } - // eslint-disable-next-line no-underscore-dangle - function __FINAL__HANDLER__(req, res) { - return fn(req, res); - } - - return [...controller.beforeAction, __FINAL__HANDLER__].map(trackPerf); + return [ + ...controller.beforeAction, + // eslint-disable-next-line no-underscore-dangle + function __FINAL_HANDLER__(req, res) { + return fn(req, res); + } + ].map(trackPerf); } export { default as createPageLinks } from './utils/create-page-links'; diff --git a/src/packages/router/route/action/test/create-page-links.test.js b/src/packages/router/route/action/test/action.test.js similarity index 78% rename from src/packages/router/route/action/test/create-page-links.test.js rename to src/packages/router/route/action/test/action.test.js index 36f508ec..9401f0c9 100644 --- a/src/packages/router/route/action/test/create-page-links.test.js +++ b/src/packages/router/route/action/test/action.test.js @@ -1,14 +1,58 @@ // @flow import { expect } from 'chai'; -import { it, describe } from 'mocha'; +import { it, describe, before } from 'mocha'; -import createPageLinks from '../utils/create-page-links'; +import type Controller from '../../../../controller'; +import type { Request, Response } from '../../../../server'; +import { getTestApp } from '../../../../../../test/utils/get-test-app'; + +import { createAction, createPageLinks } from '../index'; +import type { Action } from '../index'; const DOMAIN = 'http://localhost:4000'; const RESOURCE = 'posts'; describe('module "router/route/action"', () => { - describe('util createPageLinks()', () => { + describe('#createAction()', () => { + let result; + let createRequest; + let createResponse; + + before(async () => { + const { router, controllers } = await getTestApp(); + + // $FlowIgnore + const controller: Controller = controllers.get('health'); + const action: Action = controller.index; + + // $FlowIgnore + createRequest = (): Request => ({ + route: router.get('GET:/health'), + method: 'GET', + params: {} + }); + + // $FlowIgnore + createResponse = (): Response => ({ + stats: [] + }); + + result = createAction('custom', action, controller); + }); + + it('returns an array of functions', () => { + expect(result).to.be.an('array').with.lengthOf(1); + }); + + it('resolves with the expected value', async () => { + const fn = result.slice().pop(); + const data = await fn(createRequest(), createResponse()); + + expect(data).to.equal(204); + }); + }); + + describe('#createPageLinks()', () => { const getOptions = ({ total = 100, params = {} diff --git a/src/packages/router/route/action/test/get-action-name.test.js b/src/packages/router/route/action/test/get-action-name.test.js new file mode 100644 index 00000000..cf591ada --- /dev/null +++ b/src/packages/router/route/action/test/get-action-name.test.js @@ -0,0 +1,28 @@ +// @flow +import { expect } from 'chai'; +import { it, describe, before } from 'mocha'; + +import type { Request } from '../../../../server'; +import getActionName from '../utils/get-action-name'; +import { getTestApp } from '../../../../../../test/utils/get-test-app'; + +describe('module "router/route/action"', () => { + describe('util getActionName()', () => { + let subject: Request; + + before(async () => { + const { router } = await getTestApp(); + + // $FlowIgnore + subject = { + route: router.get('GET:/posts') + }; + }); + + it('returns the correct action name', () => { + const result = getActionName(subject); + + expect(result).to.equal('index'); + }); + }); +}); diff --git a/src/packages/router/route/action/test/get-controller-name.test.js b/src/packages/router/route/action/test/get-controller-name.test.js new file mode 100644 index 00000000..6310ff7e --- /dev/null +++ b/src/packages/router/route/action/test/get-controller-name.test.js @@ -0,0 +1,28 @@ +// @flow +import { expect } from 'chai'; +import { it, describe, before } from 'mocha'; + +import type { Request } from '../../../../server'; +import getControllerName from '../utils/get-controller-name'; +import { getTestApp } from '../../../../../../test/utils/get-test-app'; + +describe('module "router/route/action"', () => { + describe('util getControllerName()', () => { + let subject: Request; + + before(async () => { + const { router } = await getTestApp(); + + // $FlowIgnore + subject = { + route: router.get('GET:/posts') + }; + }); + + it('returns the correct controller name', () => { + const result = getControllerName(subject); + + expect(result).to.equal('PostsController'); + }); + }); +}); diff --git a/src/packages/router/route/action/test/resource.test.js b/src/packages/router/route/action/test/resource.test.js new file mode 100644 index 00000000..67e2b3ba --- /dev/null +++ b/src/packages/router/route/action/test/resource.test.js @@ -0,0 +1,206 @@ +// @flow +import { expect } from 'chai'; +import { it, describe, before } from 'mocha'; + +import type Controller from '../../../../controller'; +import type { Request, Response } from '../../../../server'; +import merge from '../../../../../utils/merge'; +import { getTestApp } from '../../../../../../test/utils/get-test-app'; + +import resource from '../enhancers/resource'; + +import type { Action } from '../../../index'; + +const DOMAIN = 'localhost:4000'; + +describe('module "router/route/action"', () => { + describe('enhancer resource()', () => { + // $FlowIgnore + const createResponse = (): Response => ({ + stats: [] + }); + + //$FlowIgnore + const createRequestBuilder = ({ path, route, params }) => (): Request => ({ + route, + method: 'GET', + url: { + protocol: null, + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: '', + query: {}, + pathname: path, + path: path, + href: path + }, + params: merge({ + fields: { + posts: [ + 'body', + 'title', + 'createdAt', + 'updatedAt' + ] + } + }, params), + headers: new Map([ + ['host', DOMAIN] + ]), + connection: { + encrypted: false + } + }); + + describe('- type "collection"', () => { + let subject: Action; + let createRequest; + + before(async () => { + const { router, controllers } = await getTestApp(); + + // $FlowIgnore + const controller: Controller = controllers.get('posts'); + + subject = resource(controller.index.bind(controller)); + createRequest = createRequestBuilder({ + path: '/posts', + route: router.get('GET:/posts'), + params: { + sort: 'createdAt', + filter: {}, + page: { + size: 25, + number: 1 + } + } + }); + }); + + it('returns an enhanced action', () => { + expect(subject) + .to.be.a('function') + .with.a.lengthOf(2); + }); + + it('resolves with a serialized payload', async () => { + const result = await subject(createRequest(), createResponse()); + + expect(result).to.be.an('object'); + }); + }); + + describe('- type "member"', () => { + describe('- with "root" namespace', () => { + const path = '/posts/1'; + let subject: Action; + let createRequest; + + before(async () => { + const { router, controllers } = await getTestApp(); + + // $FlowIgnore + const controller: Controller = controllers.get('posts'); + + subject = resource(controller.show.bind(controller)); + createRequest = createRequestBuilder({ + path, + route: router.get('GET:/posts/:dynamic'), + params: { + id: 1 + } + }); + }); + + it('returns an enhanced action', () => { + expect(subject) + .to.be.a('function') + .with.a.lengthOf(2); + }); + + it('resolves with a serialized payload', async () => { + const result = await subject(createRequest(), createResponse()); + + expect(result) + .to.be.an('object') + .and.to.have.property('links') + .and.be.an('object') + .with.property('self', `http://${DOMAIN}${path}`); + }); + }); + + describe('- with "admin" namespace', () => { + const path = '/admin/posts/1'; + let subject: Action; + let createRequest; + + before(async () => { + const { router, controllers } = await getTestApp(); + + // $FlowIgnore + const controller: Controller = controllers.get('admin/posts'); + + subject = resource(controller.show.bind(controller)); + createRequest = createRequestBuilder({ + path, + route: router.get('GET:/admin/posts/:dynamic'), + params: { + id: 1 + } + }); + }); + + it('returns an enhanced action', () => { + expect(subject) + .to.be.a('function') + .with.a.lengthOf(2); + }); + + it('resolves with a serialized payload', async () => { + const result = await subject(createRequest(), createResponse()); + + expect(result) + .to.be.an('object') + .and.to.have.property('links') + .and.be.an('object') + .with.property('self', `http://${DOMAIN}${path}`); + }); + }); + + describe('- with non-model data', () => { + const path = '/posts/10000'; + let subject: Action; + let createRequest; + + before(async () => { + const { router } = await getTestApp(); + + subject = resource(() => Promise.resolve(null)); + createRequest = createRequestBuilder({ + path, + route: router.get('GET:/posts/:dynamic'), + params: { + id: 1 + } + }); + }); + + it('returns an enhanced action', () => { + expect(subject) + .to.be.a('function') + .with.a.lengthOf(2); + }); + + it('resolves with the result of the action', async () => { + const result = await subject(createRequest(), createResponse()); + + expect(result).to.be.null; + }); + }); + }); + }); +}); diff --git a/src/packages/router/route/action/test/track-perf.test.js b/src/packages/router/route/action/test/track-perf.test.js new file mode 100644 index 00000000..36300b7c --- /dev/null +++ b/src/packages/router/route/action/test/track-perf.test.js @@ -0,0 +1,121 @@ +// @flow +import { expect } from 'chai'; +import { it, describe, before } from 'mocha'; + +import type { Action } from '../../../index'; +import type { Request, Response } from '../../../../server'; +import trackPerf from '../enhancers/track-perf'; +import { getTestApp } from '../../../../../../test/utils/get-test-app'; + +function sleep(amount: number) { + return new Promise(resolve => setTimeout(resolve, amount)); +} + +describe('module "router/route/action"', () => { + describe('enhancer trackPerf()', () => { + let createRequest; + let createResponse; + + const DATA = Object.freeze({ + data: [ + { + id: 1, + type: 'posts', + attributes: {} + }, + { + id: 2, + type: 'posts', + attributes: {} + }, + { + id: 3, + type: 'posts', + attributes: {} + } + ] + }); + + async function __FINAL_HANDLER__(req, res) { + await sleep(50); + return DATA; + } + + async function middleware(req, res) { + await sleep(5); + } + + before(async () => { + const { router } = await getTestApp(); + + // $FlowIgnore + createRequest = (): Request => ({ + route: router.get('GET:/posts'), + method: 'GET', + params: {} + }); + + // $FlowIgnore + createResponse = (): Response => ({ + stats: [] + }); + }); + + it('works with actions', async () => { + const req = createRequest(); + const res = createResponse(); + const result = await trackPerf(__FINAL_HANDLER__)(req, res); + + expect(result).to.deep.equal(DATA); + expect(res.stats).to.have.lengthOf(1); + + const { stats: [stat] } = res; + + expect(stat).to.have.property('type', 'action'); + expect(stat).to.have.property('name', 'index'); + expect(stat).to.have.property('controller', 'PostsController'); + + expect(stat) + .to.have.property('duration') + .and.be.at.least(49); + }); + + it('works with middleware', async () => { + const req = createRequest(); + const res = createResponse(); + const result = await trackPerf(middleware)(req, res); + + expect(result).to.be.undefined; + expect(res.stats).to.have.lengthOf(1); + + const { stats: [stat] } = res; + + expect(stat).to.have.property('type', 'middleware'); + expect(stat).to.have.property('name', 'middleware'); + expect(stat).to.have.property('controller', 'PostsController'); + + expect(stat) + .to.have.property('duration') + .and.be.at.least(4); + }); + + it('works with anonymous functions', async () => { + const req = createRequest(); + const res = createResponse(); + const result = await trackPerf(() => sleep(20))(req, res); + + expect(result).to.be.undefined; + expect(res.stats).to.have.lengthOf(1); + + const { stats: [stat] } = res; + + expect(stat).to.have.property('type', 'middleware'); + expect(stat).to.have.property('name', 'anonymous'); + expect(stat).to.have.property('controller', 'PostsController'); + + expect(stat) + .to.have.property('duration') + .and.be.at.least(19); + }); + }); +}); diff --git a/src/packages/router/test/fixtures/data.js b/src/packages/router/test/fixtures/data.js deleted file mode 100644 index 24fb86e2..00000000 --- a/src/packages/router/test/fixtures/data.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow -export const ROUTE_KEY = 'GET:/users'; - -export const RESOURCE_KEYS = [ - 'GET:/posts', - 'GET:/posts/:dynamic', - 'POST:/posts', - 'PATCH:/posts/:dynamic', - 'DELETE:/posts/:dynamic', - 'HEAD:/posts', - 'HEAD:/posts/:dynamic', - 'OPTIONS:/posts', - 'OPTIONS:/posts/:dynamic' -]; diff --git a/src/packages/router/test/router.test.js b/src/packages/router/test/router.test.js index b766afae..53ba59ec 100644 --- a/src/packages/router/test/router.test.js +++ b/src/packages/router/test/router.test.js @@ -2,67 +2,157 @@ import { expect } from 'chai'; import { it, before, describe } from 'mocha'; -import { ROUTE_KEY, RESOURCE_KEYS } from './fixtures/data'; - import Route from '../route'; import Router from '../index'; +import type Controller from '../../controller'; -import setType from '../../../utils/set-type'; import { getTestApp } from '../../../../test/utils/get-test-app'; import type { Request } from '../../server'; +const CONTROLLER_MISSING_MESSAGE = /Could not resolve controller by name '.+'/; + describe('module "router"', () => { describe('class Router', () => { - let subject: Router; + let controller: Controller; + let controllers; before(async () => { - const { controllers } = await getTestApp(); + const app = await getTestApp(); - subject = new Router({ - controllers, - controller: setType(() => controllers.get('application')), + controllers = app.controllers; - routes() { - this.resource('posts'); - this.resource('users', { - only: ['index'] - }); - } + // $FlowIgnore + controller = controllers.get('application'); + }); + + describe('- defining a single route', () => { + it('works as expected', () => { + const subject = new Router({ + controller, + controllers, + + routes() { + this.resource('users', { + only: ['index'] + }); + } + }); + + expect(subject.has('GET:/users')).to.be.true; }); }); - it('can define a single route', () => { - expect(subject.has(ROUTE_KEY)).to.be.true; + describe('- defining a complete resource', () => { + it('works as expected', () => { + const subject = new Router({ + controller, + controllers, + + routes() { + this.resource('posts'); + } + }); + + expect(subject.has('GET:/posts')).to.be.true; + expect(subject.has('GET:/posts/:dynamic')).to.be.true; + expect(subject.has('POST:/posts')).to.be.true; + expect(subject.has('PATCH:/posts/:dynamic')).to.be.true; + expect(subject.has('DELETE:/posts/:dynamic')).to.be.true; + expect(subject.has('HEAD:/posts')).to.be.true; + expect(subject.has('HEAD:/posts/:dynamic')).to.be.true; + expect(subject.has('OPTIONS:/posts')).to.be.true; + expect(subject.has('OPTIONS:/posts/:dynamic')).to.be.true; + }); + + it('throws an error when a controller is missing', () => { + expect(() => { + new Router({ + controller, + controllers, + + routes() { + this.resource('articles'); + } + }); + }).to.throw(ReferenceError, CONTROLLER_MISSING_MESSAGE); + }); }); - it('can define a complete resource', () => { - RESOURCE_KEYS.forEach(key => { - expect(subject.has(key)).to.be.true; + describe('- defining a complete namespace', () => { + it('works as expected', () => { + const subject = new Router({ + controller, + controllers, + + routes() { + this.namespace('admin', function () { + this.resource('posts'); + }); + } + }); + + expect(subject.has('GET:/admin/posts')).to.be.true; + expect(subject.has('GET:/admin/posts/:dynamic')).to.be.true; + expect(subject.has('POST:/admin/posts')).to.be.true; + expect(subject.has('PATCH:/admin/posts/:dynamic')).to.be.true; + expect(subject.has('DELETE:/admin/posts/:dynamic')).to.be.true; + expect(subject.has('HEAD:/admin/posts')).to.be.true; + expect(subject.has('HEAD:/admin/posts/:dynamic')).to.be.true; + expect(subject.has('OPTIONS:/admin/posts')).to.be.true; + expect(subject.has('OPTIONS:/admin/posts/:dynamic')).to.be.true; + }); + + it('throws an error when a controller is missing', () => { + expect(() => { + new Router({ + controller, + controllers, + + routes() { + this.namespace('v1', function () { + this.resource('posts'); + }); + } + }); + }).to.throw(ReferenceError, CONTROLLER_MISSING_MESSAGE); }); }); describe('#match()', () => { + let subject: Router; + + before(() => { + subject = new Router({ + controller, + controllers, + + routes() { + this.resource('posts'); + } + }); + }); + it('can match a route for a request with a dynamic url', () => { - const req: Request = setType(() => ({ + // $FlowIgnore + const req: Request = { method: 'GET', - url: { pathname: '/posts/1' } - })); + }; expect(subject.match(req)).to.be.an.instanceof(Route); }); it('can match a route for a request with a non-dynamic url', () => { - const req: Request = setType(() => ({ + // $FlowIgnore + const req: Request = { method: 'GET', - url: { pathname: '/posts' } - })); + }; expect(subject.match(req)).to.be.an.instanceof(Route); });