Skip to content

Commit

Permalink
feat(server): Implemented tests for abstract server
Browse files Browse the repository at this point in the history
  • Loading branch information
zakhenry committed Jun 16, 2016
1 parent 7b60cd7 commit fad41c2
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 46 deletions.
2 changes: 1 addition & 1 deletion docs/guide/middleware.md
Expand Up @@ -53,7 +53,7 @@ Here is an example of a basic middleware that defines a header to copy from the
function forwardHeader(headerName: string): IsolatedMiddlewareFactory {
//use a named function here so the call stack can easily be debugged to show the called middleware
return () => function forwardHeader(request: Request, response: Response): Response {
response.header(headerName, request.headers().get(headerName));
response.header(headerName, request.headers().get(headerName.toLowerCase()));
return response;
}
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -52,7 +52,7 @@
"zone.js": "^0.6.12"
},
"devDependencies": {
"@ubiquits/toolchain": "^0.1.18"
"@ubiquits/toolchain": "^0.1.19"
},
"directories": {
"doc": "docs"
Expand Down
20 changes: 9 additions & 11 deletions src/server/controllers/request.ts
@@ -1,28 +1,26 @@
import { Request as HapiRequest } from 'hapi';
import { Request as ExpressRequest } from 'express';

export type OriginalRequest = HapiRequest|ExpressRequest;
import { IncomingMessage } from 'http';

export class Request {

constructor(private original?: OriginalRequest) {
constructor(protected raw: IncomingMessage = undefined,
protected paramsMap: Map<string, string> = new Map(),
protected headersMap: Map<string, string> = new Map()) {

}

public getOriginal(): OriginalRequest {
return this.original;
public getRaw(): IncomingMessage {
return this.raw;
}

public headers(): Map<string, string> {
return Request.extractMapFromDictionary<string, string>(this.original.headers);
return this.headersMap;
}

public params(): Map<string, string> {
return Request.extractMapFromDictionary<string, string>(this.original.params);
return this.paramsMap;
}


private static extractMapFromDictionary<K, V>(dictionary:Object):Map<K, V> {
public static extractMapFromDictionary<K, V>(dictionary: Object): Map<K, V> {
let map = new Map();

for (let key in dictionary) {
Expand Down
104 changes: 102 additions & 2 deletions src/server/servers/abstract.server.spec.ts
@@ -1,11 +1,17 @@
import { Server, RouteConfig } from './abstract.server';
import { RemoteCli } from '../services/remoteCli.service';
import { Logger } from '../../common/services/logger.service';
import { Injectable } from '@angular/core';
import { Injectable, provide } from '@angular/core';
import { it, inject, beforeEachProviders, expect, describe } from '@angular/core/testing';
import { LoggerMock } from '../../common/services/logger.service.spec';
import { RemoteCliMock } from '../services/remoteCli.service.spec';
import { Request } from '../controllers/request';
import { Response } from '../controllers/response';
import Spy = jasmine.Spy;

@Injectable()
export class ServerMock extends Server {

public getEngine(): any {
return undefined;
}
Expand All @@ -27,3 +33,97 @@ export class ServerMock extends Server {
}

}

describe('Server', () => {

const providers = [
provide(Server, {useClass: ServerMock}),
provide(Logger, {useClass: LoggerMock}),
provide(RemoteCli, {useClass: RemoteCliMock}),
];

beforeEachProviders(() => providers);

let cliSpy: Spy;

beforeEach(() => {
spyOn(ServerMock.prototype, 'initialize')
.and
.callThrough();
cliSpy = spyOn(RemoteCliMock.prototype, 'start');
});

it('initializes the server with port and host', inject([Server], (server: Server) => {

console.log((<any>server).initialize.calls.count());

expect((<any>server).initialize)
.toHaveBeenCalled();

expect(cliSpy)
.toHaveBeenCalledWith(3001);

expect(server.getHost())
.toEqual('http://localhost:3000');

}));

it('returns the engine', inject([Server], (server: Server) => {

expect(server.getEngine())
.toBe(undefined);

}));

it('returns the inner http server instance', inject([Server], (server: Server) => {

expect(server.getHttpServer())
.toBe(undefined);

}));

it('registers routes', inject([Server], (server: Server) => {

const routeConfig: RouteConfig = {
path: '/test',
methodName: 'test',
method: 'GET',
callStack: [],
callStackHandler: null
};

let spy = spyOn(server, 'registerRouteWithEngine');

server.register(routeConfig);

expect(spy)
.toHaveBeenCalledWith(routeConfig);
expect(server.getRoutes())
.toEqual([routeConfig]);
}));

it('starts the server running and returns promise', inject([Server], (server: Server) => {

let spy = spyOn(server, 'start').and.callThrough();

let response = server.start();

expect(spy)
.toHaveBeenCalled();

return response.then((onStart:Server) => {
expect(onStart).toEqual(server);
});

}));

it('starts the server running and returns promise', inject([Server], (server: Server) => {
const response: Response = (<any>server).getDefaultResponse();

expect(response instanceof Response).toBe(true);

}));

});


52 changes: 39 additions & 13 deletions src/server/servers/abstract.server.ts
Expand Up @@ -6,7 +6,7 @@ import { Response } from '../controllers/response';
import { Request } from '../controllers/request';
import { PromiseFactory } from '../../common/util/serialPromise';
import { Application as Express } from 'express';
import {Server as HttpServer} from 'http';
import { Server as HttpServer } from 'http';

export type HttpMethod = 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE';

Expand All @@ -21,16 +21,19 @@ export interface RouteConfig {
@Injectable()
export abstract class Server {

protected host:string;
protected port:number;

protected httpServer:HttpServer;
/** Hostname eg `localhost`, `example.com` */
protected host: string;
/** Port number server is running on */
protected port: number;

/** require('http').Server object from the base class */
protected httpServer: HttpServer;

/** All Configured routes */
public configuredRoutes: RouteConfig[] = [];
/**
* Logger instance for the class, initialized with `server` source
*/
protected logger: Logger;

/** Logger instance for the class, initialized with `server` source */
protected logger: Logger;

constructor(loggerBase: Logger, remoteCli: RemoteCli) {

Expand All @@ -42,7 +45,6 @@ export abstract class Server {

this.initialize();


remoteCli.start(3001);
}

Expand Down Expand Up @@ -72,18 +74,42 @@ export abstract class Server {
* Retrieves the underlying engine for custom calls
* @returns {Hapi|any}
*/
public abstract getEngine():Hapi|Express|any;
public abstract getEngine(): Hapi|Express|any;

public getHttpServer(){
/**
* Retrieve the base instance of require('http').Server
* @returns {HttpServer}
*/
public getHttpServer() {
return this.httpServer;
}

public getHost():string{
/**
* Get the host name (for logging)
* @returns {string}
*/
public getHost(): string {
return `http://${this.host}:${this.port}`;
}

/**
* Retrieve all configured routes
* @returns {RouteConfig[]}
*/
public getRoutes(): RouteConfig[] {
return this.configuredRoutes;
}

/**
* Get the default response object
* @returns {Response}
*/
protected getDefaultResponse(): Response {

return new Response()
// Outputs eg `X-Powered-By: Ubiquits<Angular,Express>`
.header('X-Powered-By', `Ubiquits<Angular,${this.constructor.name.replace('Server', '')}>`);

}

}
33 changes: 24 additions & 9 deletions src/server/servers/express.server.ts
Expand Up @@ -30,7 +30,7 @@ export class ExpressServer extends Server {
* @returns {Express}
*/
protected initialize() {
this.engine = express();
this.engine = express();
this.httpServer = http.createServer(<any>(this.engine));

return this;
Expand All @@ -43,17 +43,16 @@ export class ExpressServer extends Server {
*/
protected registerRouteWithEngine(routeConfig: RouteConfig): this {

console.log(this.engine);

this.engine[routeConfig.method.toLowerCase()](routeConfig.path, (req: ExpressRequest, res: ExpressResponse) => {

let request = new Request(req);
let response = new Response();
let request = new Request(req,
Request.extractMapFromDictionary<string, string>(req.params),
Request.extractMapFromDictionary<string, string>(req.headers));

let response = this.getDefaultResponse();

return routeConfig.callStackHandler(request, response)
.then((response: Response) => {
this.logger.debug('Responding with', response);

return this.send(response, res);
})
.catch((err) => this.sendErr(err, res));
Expand All @@ -74,7 +73,12 @@ export class ExpressServer extends Server {
.then(() => this);
}

private send(response: Response, res: ExpressResponse):void {
/**
* Send the response
* @param response
* @param res
*/
private send(response: Response, res: ExpressResponse): void {

res.status(response.statusCode);

Expand All @@ -85,7 +89,18 @@ export class ExpressServer extends Server {
res.send(response.body);
}

private sendErr(err:any, res: ExpressResponse):void {
/**
* Send the error response
* @param err
* @param res
*/
private sendErr(err: any, res: ExpressResponse): void {

//make sure the status is of error type
if (res.statusCode < 400) {
res.status(500);
}

res.send(err);
}
}
18 changes: 9 additions & 9 deletions src/server/servers/hapi.server.ts
@@ -1,12 +1,10 @@
import { Injectable } from '@angular/core';
import { Server as Hapi } from 'hapi';
import { Request as HapiRequest, IReply, Response as HapiResponse } from 'hapi';
import { Server as Hapi, Request as HapiRequest, IReply, Response as HapiResponse } from 'hapi';
import { Server, RouteConfig } from './abstract.server';
import { RemoteCli } from '../services/remoteCli.service';
import { Logger } from '../../common/services/logger.service';
import { Response } from '../controllers/response';
import { Request } from '../controllers/request';
import { IRoute } from 'hapi';

@Injectable()
export class HapiServer extends Server {
Expand Down Expand Up @@ -49,22 +47,24 @@ export class HapiServer extends Server {
*/
protected registerRouteWithEngine(routeConfig: RouteConfig): this {

if (/[\*\?]/.test(routeConfig.path)){
if (/[\*\?]/.test(routeConfig.path)) {
throw new Error('Hapi syntax for optional or multi-segment parameters is not supported');
}

const config = {
//re-map /path/{param} to /path/{param} (the inverse if needed later is .replace(/{([-_a-zA-Z0-9]+).*?}/g, ':$1')
//re-map /path/{param} to /path/{param} (the inverse if needed later is
// .replace(/{([-_a-zA-Z0-9]+).*?}/g, ':$1')
path: routeConfig.path.replace(/:(.+?)(\/|$)/g, "{$1}$2"),
method: routeConfig.method,
handler: (req: HapiRequest, reply: IReply): Promise<HapiResponse> => {

let request = new Request(req);
let response = new Response();
let request = new Request(<any>req.raw.req, //typings are incorrect, type should be IncomingMessage
Request.extractMapFromDictionary<string, string>(req.params),
Request.extractMapFromDictionary<string, string>(req.headers));
let response = this.getDefaultResponse();

return routeConfig.callStackHandler(request, response)
.then((response: Response) => {
this.logger.debug('Responding with', response);
const res = <HapiResponse>reply(response.body);

res.code(response.statusCode);
Expand All @@ -89,7 +89,7 @@ export class HapiServer extends Server {

return new Promise((resolve, reject) => {
this.engine.start((err) => {
if (err){
if (err) {
return reject(err);
}
return resolve();
Expand Down
1 change: 1 addition & 0 deletions typings.json
Expand Up @@ -4,6 +4,7 @@
"express": "registry:dt/express#4.0.0+20160317120654",
"express-serve-static-core": "registry:dt/express-serve-static-core#0.0.0+20160602151406",
"hapi": "registry:dt/hapi#13.0.0+20160423150146",
"jasmine": "registry:dt/jasmine#2.2.0+20160505161446",
"lodash": "registry:dt/lodash#3.10.0+20160330154726",
"moment-node": "registry:dt/moment-node#2.11.1+20160511043338",
"node": "registry:dt/node#6.0.0+20160514165920",
Expand Down

0 comments on commit fad41c2

Please sign in to comment.