From 8e7d1b23b0fb25a2f94a0376621724447f009e87 Mon Sep 17 00:00:00 2001 From: sanad haj Date: Wed, 30 Jan 2019 22:37:09 +0200 Subject: [PATCH 1/2] feat(rest): add redirect route add RedirectRoute class by implementing RouteEntry, ResolvedRoute with adding the method redirect for both rest server and app --- packages/rest/src/rest.application.ts | 14 ++++++++ packages/rest/src/rest.server.ts | 15 ++++++++ packages/rest/src/router/index.ts | 2 ++ packages/rest/src/router/redirect-route.ts | 42 ++++++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 packages/rest/src/router/redirect-route.ts diff --git a/packages/rest/src/rest.application.ts b/packages/rest/src/rest.application.ts index d256158c3550..f8db5efafc61 100644 --- a/packages/rest/src/rest.application.ts +++ b/packages/rest/src/rest.application.ts @@ -228,6 +228,20 @@ export class RestApplication extends Application implements HttpServerLike { } } + /** + * Redirect route by invoking a handler function. + * + * ```ts + * app.redirect('/explorer', '/explorer/'); + * ``` + * + * @param source URL path of the redirect endpoint + * @param target URL path of the endpoint + */ + redirect(source: string, target: string): Binding { + return this.restServer.redirect(source, target); + } + /** * Set the OpenAPI specification that defines the REST API schema for this * application. All routes, parameter definitions and return types will be diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 564cdf9d6ce4..d8af148eb36e 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -42,6 +42,7 @@ import { RouteEntry, RoutingTable, StaticAssetsRoute, + RedirectRoute, } from './router'; import {DefaultSequence, SequenceFunction, SequenceHandler} from './sequence'; import { @@ -629,6 +630,20 @@ export class RestServer extends Context implements Server, HttpServerLike { ); } + /** + * Redirect route by invoking a handler function. + * + * ```ts + * server.redirect('/explorer', '/explorer/'); + * ``` + * + * @param source URL path of the redirect endpoint + * @param target URL path of the endpoint + */ + redirect(source: string, target: string): Binding { + return this.route(new RedirectRoute(source, this._basePath + target)); + } + // The route for static assets private _staticAssetRoute = new StaticAssetsRoute(); diff --git a/packages/rest/src/router/index.ts b/packages/rest/src/router/index.ts index 86ded6b6eb6e..f1625998fc2e 100644 --- a/packages/rest/src/router/index.ts +++ b/packages/rest/src/router/index.ts @@ -9,6 +9,8 @@ export * from './base-route'; export * from './controller-route'; export * from './handler-route'; export * from './static-assets-route'; +export * from './redirect-route'; +export * from './swagger-route'; // routers export * from './rest-router'; diff --git a/packages/rest/src/router/redirect-route.ts b/packages/rest/src/router/redirect-route.ts new file mode 100644 index 000000000000..518b30cdd962 --- /dev/null +++ b/packages/rest/src/router/redirect-route.ts @@ -0,0 +1,42 @@ +import {RouteEntry, ResolvedRoute} from '.'; +import {RequestContext} from '../request-context'; +import {OperationObject, SchemasObject} from '@loopback/openapi-v3-types'; +import {OperationArgs, OperationRetval, PathParameterValues} from '../types'; + +export class RedirectRoute implements RouteEntry, ResolvedRoute { + // ResolvedRoute API + readonly pathParams: PathParameterValues = []; + readonly schemas: SchemasObject = {}; + + // RouteEntry implementation + readonly verb: string = 'get'; + readonly path: string = this.sourcePath; + readonly spec: OperationObject = { + description: 'LoopBack Redirect route', + 'x-visibility': 'undocumented', + responses: {}, + }; + + constructor( + public readonly sourcePath: string, + public targetPath: string, + public statusCode: number = 303, + ) { + this.sourcePath = sourcePath; + } + + async invokeHandler( + {response}: RequestContext, + args: OperationArgs, + ): Promise { + response.redirect(this.statusCode, this.targetPath); + } + + updateBindings(requestContext: RequestContext) { + // no-op + } + + describe(): string { + return `RedirectRoute.${this.sourcePath}.${this.targetPath}`; + } +} From 822b625793d1e32ca17d2457ebdba21e3731edf8 Mon Sep 17 00:00:00 2001 From: sanad haj Date: Wed, 30 Jan 2019 23:49:08 +0200 Subject: [PATCH 2/2] test(rest): add new tests add mores tests (unit/integration/acceptance) to cover theRedirectRote using the server and app api (redirect method). --- .../acceptance/routing/routing.acceptance.ts | 47 +++++++++++++++++++ .../rest.application.integration.ts | 22 ++++++++- .../integration/rest.server.integration.ts | 39 +++++++++++++++ .../rest.server/rest.server.redirect.unit.ts | 19 ++++++++ packages/rest/src/rest.application.ts | 4 +- packages/rest/src/rest.server.ts | 6 ++- packages/rest/src/router/index.ts | 1 - packages/rest/src/router/redirect-route.ts | 2 +- 8 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 packages/rest/src/__tests__/unit/rest.server/rest.server.redirect.unit.ts diff --git a/packages/rest/src/__tests__/acceptance/routing/routing.acceptance.ts b/packages/rest/src/__tests__/acceptance/routing/routing.acceptance.ts index 407bad662f12..871eae9e80a3 100644 --- a/packages/rest/src/__tests__/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/src/__tests__/acceptance/routing/routing.acceptance.ts @@ -735,6 +735,53 @@ describe('Routing', () => { .get('/greet/world') .expect(200, 'HELLO WORLD'); }); + + it('gives precedence to redirect routes over controller methods', async () => { + class MyController { + @get('/hello', { + responses: {}, + }) + hello(): string { + return 'hello'; + } + @get('/hello/world') + helloWorld() { + return `hello world`; + } + } + const app = givenAnApplication(); + const server = await givenAServer(app); + server.basePath('/api'); + server.redirect('/test/hello', '/hello/world'); + givenControllerInApp(app, MyController); + const response = await whenIMakeRequestTo(server) + .get('/api/test/hello') + .expect(303); + // new request to verify the redirect target + await whenIMakeRequestTo(server) + .get(response.header.location) + .expect(200, 'hello world'); + }); + + it('gives precedence to redirect routes over route methods', async () => { + const app = new RestApplication(); + app.route( + 'get', + '/greet/{name}', + anOperationSpec() + .withParameter({name: 'name', in: 'path', type: 'string'}) + .build(), + (name: string) => `hello ${name}`, + ); + app.redirect('/hello/john', '/greet/john'); + const response = await whenIMakeRequestTo(app) + .get('/hello/john') + .expect(303); + // new request to verify the redirect target + await whenIMakeRequestTo(app) + .get(response.header.location) + .expect(200, 'hello john'); + }); }); /* ===== HELPERS ===== */ diff --git a/packages/rest/src/__tests__/integration/rest.application.integration.ts b/packages/rest/src/__tests__/integration/rest.application.integration.ts index 1521dae2ba5a..1679248693e7 100644 --- a/packages/rest/src/__tests__/integration/rest.application.integration.ts +++ b/packages/rest/src/__tests__/integration/rest.application.integration.ts @@ -7,7 +7,7 @@ import {anOperationSpec} from '@loopback/openapi-spec-builder'; import {Client, createRestAppClient, expect} from '@loopback/testlab'; import * as fs from 'fs'; import * as path from 'path'; -import {RestApplication, RestServer, RestServerConfig} from '../..'; +import {RestApplication, RestServer, RestServerConfig, get} from '../..'; const ASSETS = path.resolve(__dirname, '../../../fixtures/assets'); @@ -145,8 +145,28 @@ describe('RestApplication (integration)', () => { }); }); + it('can app redirect routes with custom status', async () => { + givenApplication(); + restApp.controller(DummyController); + restApp.redirect('/fake/html', '/html', 304); + await restApp.start(); + client = createRestAppClient(restApp); + const response = await client.get('/fake/html').expect(304); + await client.get(response.header.location).expect(200, 'Hi'); + }); + function givenApplication(options?: {rest: RestServerConfig}) { options = options || {rest: {port: 0, host: '127.0.0.1'}}; restApp = new RestApplication(options); } }); + +class DummyController { + constructor() {} + @get('/html', { + responses: {}, + }) + ping(): string { + return 'Hi'; + } +} diff --git a/packages/rest/src/__tests__/integration/rest.server.integration.ts b/packages/rest/src/__tests__/integration/rest.server.integration.ts index ab1e96002276..46f43e562600 100644 --- a/packages/rest/src/__tests__/integration/rest.server.integration.ts +++ b/packages/rest/src/__tests__/integration/rest.server.integration.ts @@ -735,6 +735,39 @@ paths: ); expect(response.body.servers).to.containEql({url: '/api'}); }); + + it('can redirect routes when it does not exist', async () => { + server.controller(DummyController); + server.redirect('/page/html', '/html'); + const response = await createClientForHandler(server.requestHandler) + .get('/api/page/html') + .expect(303); + await createClientForHandler(server.requestHandler) + .get(response.header.location) + .expect(200, 'Hi'); + }); + + it('can redirect routes', async () => { + server.controller(DummyController); + server.redirect('/hello', '/endpoint'); + const response = await createClientForHandler(server.requestHandler) + .get('/api/hello') + .expect(303); + await createClientForHandler(server.requestHandler) + .get(response.header.location) + .expect(200, 'hello'); + }); + + it('can server redirect routes with custom status', async () => { + server.controller(DummyController); + server.redirect('/hello', '/endpoint', 304); + const response = await createClientForHandler(server.requestHandler) + .get('/api/hello') + .expect(304); + await createClientForHandler(server.requestHandler) + .get(response.header.location) + .expect(200, 'hello'); + }); }); async function givenAServer( @@ -763,6 +796,12 @@ paths: ping(): string { return 'Hi'; } + @get('/endpoint', { + responses: {}, + }) + hello(): string { + return 'hello'; + } } class DummyXmlController { diff --git a/packages/rest/src/__tests__/unit/rest.server/rest.server.redirect.unit.ts b/packages/rest/src/__tests__/unit/rest.server/rest.server.redirect.unit.ts new file mode 100644 index 000000000000..5d4b826c3076 --- /dev/null +++ b/packages/rest/src/__tests__/unit/rest.server/rest.server.redirect.unit.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {Application} from '@loopback/core'; +import {RestServer, RestComponent} from '../../..'; + +describe('RestServer.redirect()', () => { + it('binds the redirect route', async () => { + const app = new Application(); + app.component(RestComponent); + const server = await app.getServer(RestServer); + server.redirect('/test', '/test/'); + let boundRoutes = server.find('routes.*').map(b => b.key); + expect(boundRoutes).to.containEql('routes.get %2Ftest'); + }); +}); diff --git a/packages/rest/src/rest.application.ts b/packages/rest/src/rest.application.ts index f8db5efafc61..555866e342bf 100644 --- a/packages/rest/src/rest.application.ts +++ b/packages/rest/src/rest.application.ts @@ -238,8 +238,8 @@ export class RestApplication extends Application implements HttpServerLike { * @param source URL path of the redirect endpoint * @param target URL path of the endpoint */ - redirect(source: string, target: string): Binding { - return this.restServer.redirect(source, target); + redirect(source: string, target: string, statusCode?: number): Binding { + return this.restServer.redirect(source, target, statusCode); } /** diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index d8af148eb36e..9fde8f34eced 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -640,8 +640,10 @@ export class RestServer extends Context implements Server, HttpServerLike { * @param source URL path of the redirect endpoint * @param target URL path of the endpoint */ - redirect(source: string, target: string): Binding { - return this.route(new RedirectRoute(source, this._basePath + target)); + redirect(source: string, target: string, statusCode?: number): Binding { + return this.route( + new RedirectRoute(source, this._basePath + target, statusCode), + ); } // The route for static assets diff --git a/packages/rest/src/router/index.ts b/packages/rest/src/router/index.ts index f1625998fc2e..559c8969b59f 100644 --- a/packages/rest/src/router/index.ts +++ b/packages/rest/src/router/index.ts @@ -10,7 +10,6 @@ export * from './controller-route'; export * from './handler-route'; export * from './static-assets-route'; export * from './redirect-route'; -export * from './swagger-route'; // routers export * from './rest-router'; diff --git a/packages/rest/src/router/redirect-route.ts b/packages/rest/src/router/redirect-route.ts index 518b30cdd962..333e9746c267 100644 --- a/packages/rest/src/router/redirect-route.ts +++ b/packages/rest/src/router/redirect-route.ts @@ -37,6 +37,6 @@ export class RedirectRoute implements RouteEntry, ResolvedRoute { } describe(): string { - return `RedirectRoute.${this.sourcePath}.${this.targetPath}`; + return `RedirectRoute from "${this.sourcePath}" to "${this.targetPath}"`; } }