Skip to content

Commit

Permalink
feat: Added unit tests for ctx and decorators and middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
m-mdy-m committed Sep 14, 2024
1 parent 524f086 commit 97f3d21
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 0 deletions.
57 changes: 57 additions & 0 deletions lib/core/__test__/context.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import sinon from 'sinon';
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { WebContext } from '../context';
import { IncomingMessage } from 'http';
import { ServerResponse } from 'http';
import { RQ, RS } from '../../types';
describe('CONTEXT', () => {
let webContext: WebContext;
let mockRequest: sinon.SinonStubbedInstance<IncomingMessage & RQ>;
let mockResponse: sinon.SinonStubbedInstance<ServerResponse & RS>;
beforeEach(() => {
mockRequest = sinon.createStubInstance<IncomingMessage & RQ>(IncomingMessage);
mockResponse = sinon.createStubInstance<ServerResponse & RS>(ServerResponse);
webContext = new WebContext(mockRequest, mockResponse);
});

afterEach(() => {
sinon.restore();
});
it('should initialize the context with default values', () => {
expect(webContext.body).to.be.null;
expect(webContext.params).to.deep.equal({});
expect(webContext.query).to.deep.equal({});
});
it('should return response property when accessed via proxy', () => {
// Mocking a response property (e.g., "statusCode")
mockResponse.statusCode = 200;

const statusCode = webContext.ctx.statusCode;

expect(statusCode).to.equal(200);
});

it('should set request or response properties correctly', () => {
// Setting a response property
webContext.ctx.statusCode = 404;
expect(mockResponse.statusCode).to.equal(404);

// Setting a request property
webContext.ctx.method = 'POST';
expect(webContext.ctx.method).to.equal('POST');
});
it('should return true if property exists in request or response', () => {
mockRequest.method = 'GET';
expect('method' in webContext.ctx).to.be.true;
});

it('should return false if property does not exist in request or response', () => {
expect('nonexistent' in webContext.ctx).to.be.false;
});

it('should delete properties from request or response', () => {
expect(delete webContext.ctx.method).to.be.true;
expect(webContext.ctx.method).to.be.undefined;
});
});
59 changes: 59 additions & 0 deletions lib/core/__test__/decorators.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import { mid, mids, Route, getEx } from '../decorators';
import { ServerUtils } from '../../helper';
import Reflect from '../../metadata';
import { routes } from '../router';

describe('Decorators', () => {
let reflectInitStub: sinon.SinonStub;
let serverUtilsNormalizeStub: sinon.SinonStub;

beforeEach(() => {
reflectInitStub = sinon.stub(Reflect, 'init');
serverUtilsNormalizeStub = sinon.stub(ServerUtils, 'normalize').callsFake((middleware: any) => (Array.isArray(middleware) ? middleware : [middleware]));
});

afterEach(() => {
sinon.restore();
});

describe('mid decorator', () => {
it('should add middleware to the method using Reflect', () => {
const middleware = sinon.stub();
const target = {};
const propertyKey = 'someMethod';
const descriptor = {};
const mockGet = sinon.stub(Reflect, 'get').returns([]);

mid(middleware)(target, propertyKey, descriptor as PropertyDescriptor);

expect(mockGet.calledOnceWith('middlewares', target.constructor.prototype, propertyKey)).to.be.true;
expect(serverUtilsNormalizeStub.calledOnceWith(middleware)).to.be.true;
expect(reflectInitStub.calledOnceWith('middlewares', [middleware], target.constructor.prototype, propertyKey)).to.be.true;

mockGet.restore();
});
});
describe('mids decorator', () => {
it('should add class-level middleware using Reflect', () => {
const middlewareArray = [sinon.stub(), sinon.stub()];
class SomeClass {}
mids(middlewareArray)(SomeClass);

expect(serverUtilsNormalizeStub.calledOnceWith(middlewareArray)).to.be.true;
expect(reflectInitStub.calledOnceWith('classMiddlewares', middlewareArray, SomeClass.prototype)).to.be.true;
});
});

describe('Route decorator', () => {
it('should register the route and path in Reflect and routes', () => {
const path = '/test-path';
class SomeClass {}
Route(path)(SomeClass);

expect(reflectInitStub.calledOnceWith('route', path, SomeClass.prototype)).to.be.true;
expect(routes.get(path)).to.equal(SomeClass);
});
});
});
78 changes: 78 additions & 0 deletions lib/core/__test__/middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import { Gmids, midManager } from '../middleware';
import { ServerUtils } from '../../helper';
import { Gland } from '../../types/index';

describe('Gmids and midManager', () => {
let serverUtilsNormalizeStub: sinon.SinonStub;

beforeEach(() => {
serverUtilsNormalizeStub = sinon.stub(ServerUtils, 'normalize').callsFake((middleware: any) => (Array.isArray(middleware) ? middleware : [middleware]));
(Gmids as any).mids = [];
});

afterEach(() => {
sinon.restore();
});

describe('Gmids', () => {
describe('set', () => {
it('should add middleware to the mids array', () => {
const middleware = sinon.stub();
Gmids.set(middleware);

expect(serverUtilsNormalizeStub.calledOnceWith(middleware)).to.be.true;
expect(Gmids.get()).to.include(middleware);
});

it('should handle multiple middlewares', () => {
const middlewareArray = [sinon.stub(), sinon.stub()];
Gmids.set(middlewareArray);
expect(serverUtilsNormalizeStub.calledOnceWith(middlewareArray)).to.be.true;
expect(Gmids.get()).to.deep.equal(middlewareArray);
});
});

describe('get', () => {
it('should return an empty array when no middlewares are set', () => {
sinon.stub(Gmids, 'get').returns([]);
expect(Gmids.get()).to.deep.equal([]);
});

it('should return the current middlewares', () => {
const middleware = sinon.stub();
Gmids.set(middleware);
expect(Gmids.get()).to.include(middleware);
});
});
});

describe('midManager', () => {
let middlewares: Gland.Middleware[];

beforeEach(() => {
middlewares = [];
});

describe('process', () => {
it('should throw an error for invalid middleware/handler signature', () => {
const path = '/test';
const invalidHandler: any = sinon.stub().callsFake(() => {});

expect(() => midManager.process(path, [invalidHandler], middlewares)).to.throw('Invalid middleware/handler function signature');
});

it('should handle non-string path and add unique middlewares', () => {
const handler1: Gland.GlandMiddleware = sinon.stub();
const handler2: Gland.GlandMiddleware = sinon.stub();

midManager.process(handler1, [handler1, handler2], middlewares);

expect(middlewares).to.have.lengthOf(2);
expect(middlewares).to.include(handler1);
expect(middlewares).to.include(handler2);
});
});
});
});
58 changes: 58 additions & 0 deletions lib/core/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { IncomingMessage, ServerResponse } from 'http';
import { Context, RQ, RS } from '../types';
import './router/request';
import { Queue } from '../helper/queue';
export class WebContext {
public rq: RQ;
public rs: RS;
public ctx: Context;
public body: any;
public params: any;
public query: any;
constructor(request: IncomingMessage, response: ServerResponse) {
this.rq = request;
this.rs = response;
this.body = null;
this.params = {};
this.query = {};
this.ctx = new Proxy(this, this.opts()) as WebContext & Context;
}
private opts() {
return {
get: (t: WebContext, p: string | symbol) => {
// Explicitly handle `on` method
if (p === 'on' || p === 'once') {
// Check if this method is being used in the request or response context
if (typeof t.rq[p as keyof RQ] === 'function') {
return t.rq[p as keyof RQ].bind(t.rq);
}
if (typeof t.rs[p as keyof RS] === 'function') {
return t.rs[p as keyof RS].bind(t.rs);
}
}
if (p in t) return t[p as keyof WebContext];
if (p in t.rs) return typeof t.rs[p as keyof RS] === 'function' ? t.rs[p as keyof RS].bind(t.rs) : t.rs[p as keyof RS];
if (p in t.rq) return t.rq[p as keyof RQ];
},
set: (t: WebContext, p: string | symbol, v: any) => {
if (p in t.rs) {
t.rs[p as keyof RS] = v;
} else if (p in t.rq) {
t.rq[p as keyof RQ] = v;
} else {
t[p as keyof WebContext] = v;
}
return true;
},
has: (t: WebContext, p: string | symbol) => {
return p in t.rs || p in t.rq;
},
deleteperty: (t: any, p: string | symbol) => {
if (p in (t || t.rq)) {
return delete (t || t.rq)[p as keyof WebContext];
}
return false;
},
};
}
}
30 changes: 30 additions & 0 deletions lib/core/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServerUtils } from '../helper';
import Reflect from '../metadata';
import { MidsFn, RouteHandler } from '../types';
import { routes } from './router';
const classes: Set<any> = new Set();
function getEx(): any[] {
return Array.from(classes);
}
export { getEx };
export function mid(middleware: MidsFn | MidsFn[]): MethodDecorator | any {
return (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void => {
const existingMids = Reflect.get('middlewares', target.constructor.prototype, propertyKey) || [];
const newMids = ServerUtils.normalize(middleware);
Reflect.init('middlewares', [...existingMids, ...newMids], target.constructor.prototype, propertyKey);
};
}

export function mids(middlewareArray: MidsFn[] | MidsFn): ClassDecorator {
return (target: any) => {
const newMids = ServerUtils.normalize(middlewareArray);
Reflect.init('classMiddlewares', newMids, target.prototype);
};
}

export function Route(path: string): ClassDecorator {
return (target: Function): void => {
Reflect.init('route', path, target.prototype);
routes.set(path, target as RouteHandler);
};
}
53 changes: 53 additions & 0 deletions lib/core/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ServerUtils } from '../helper';
import { Context, MidsFn, NxtFunction } from '../types';
import { Gland } from '../types';
import { Router } from './router';
export namespace Gmids {
export let mids: MidsFn[] = [];

export function set(middleware: MidsFn | MidsFn[]) {
mids = [...mids, ...ServerUtils.normalize(middleware)];
}

export function get(): MidsFn[] | [] {
return mids || [];
}
}
export namespace midManager {
export function process(path: string | Gland.Middleware, handlers: (Gland.Middleware | Gland.Middleware[])[], middlewares: Gland.Middleware[]) {
if (typeof path === 'string') {
handlers.flat().forEach((handler) => {
Router.set(handler as any, path);
if (handler.length === 2 || handler.length === 3) {
middlewares.push(async (ctx: Context, next: NxtFunction) => {
if (ctx.url!.startsWith(path)) {
if (handler.length === 2) {
await (handler as Gland.GlandMiddleware)(ctx, next);
} else if (handler.length === 3) {
await new Promise<void>((resolve, reject) => {
(handler as Gland.ExpressMiddleware)(ctx.rq, ctx.rs, (err?: any) => {
if (err) reject(err);
else resolve();
});
});
} else {
throw new Error('Invalid middleware/handler function signature');
}
} else {
await next();
}
});
} else if (handler.length === 1) {
middlewares.push(handler as Gland.GlandMiddleware);
} else {
throw new Error('Invalid middleware/handler function signature');
}
});
} else {
// Handle when path is not a string
const allMiddlewares = [path, ...handlers].flat();
const uniqueMiddlewares = Array.from(new Set(allMiddlewares));
middlewares.push(...uniqueMiddlewares);
}
}
}
7 changes: 7 additions & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,10 @@ export type ListenArgs =
| [path: string, backlog?: number, listeningListener?: () => void]
| [path: string, listeningListener?: () => void]
| [options: Gland.ListenOptions, listeningListener?: () => void];
export interface ModuleConfig {
path: string;
recursive?: boolean;
pattern?: string;
cacheModules?: boolean;
logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
}

0 comments on commit 97f3d21

Please sign in to comment.