Skip to content

Commit

Permalink
Merge pull request #14 from medishen/dev/v2
Browse files Browse the repository at this point in the history
feat: Fix routing for multi-language support and improve RequestInfo robustness
  • Loading branch information
0xii00 authored Jan 16, 2025
2 parents 7191af7 + 38ade37 commit 09579a1
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 61 deletions.
6 changes: 5 additions & 1 deletion lib/router/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ export class Router {
const multiLangRoutes = Reflector.get(RouterMetadataKeys.MULTI_LANG, route.constructor);
if (multiLangRoutes) {
const lang = ctx.language;
const langPath = multiLangRoutes[lang];
let langPath = multiLangRoutes[lang];
if (!langPath) {
langPath = multiLangRoutes.default;
}
if (!langPath) continue;
route.path = `${this.apiPrefix}${langPath}`;
}
const { path: routePath, constructor } = route;
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/router.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class RequestInfo {
return ips[0].trim();
}
}
return this.req.socket.remoteAddress || 'unknown';
return this.req?.socket?.remoteAddress || 'unknown';
}

get headers(): IncomingHttpHeaders {
Expand Down
325 changes: 266 additions & 59 deletions test/unit/Router/Router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,85 +3,292 @@ import { Router } from '../../../dist/router/Router';
import sinon from 'sinon';
import { expect } from 'chai';
import { EventSystem } from '../../../dist/events/EventSystem';

Check failure on line 5 in test/unit/Router/Router.spec.ts

View workflow job for this annotation

GitHub Actions / test-unit

Cannot find module '../../../dist/events/EventSystem' or its corresponding type declarations.
import { MiddlewareStack } from '../../../dist/middleware/index';
import { RouteDefinition } from '../../../dist/common/interfaces';
import { RouteDefinition, ServerRequest } from '../../../dist/common/interfaces';

Check failure on line 6 in test/unit/Router/Router.spec.ts

View workflow job for this annotation

GitHub Actions / test-unit

Cannot find module '../../../dist/common/interfaces' or its corresponding type declarations.
import Reflector from '../../../dist/metadata';

Check failure on line 7 in test/unit/Router/Router.spec.ts

View workflow job for this annotation

GitHub Actions / test-unit

Cannot find module '../../../dist/metadata' or its corresponding type declarations.
import { RouterMetadataKeys } from '../../../dist/common/enums';
import { HttpStatus } from '../../../dist/common/enums';

Check failure on line 8 in test/unit/Router/Router.spec.ts

View workflow job for this annotation

GitHub Actions / test-unit

Cannot find module '../../../dist/common/enums' or its corresponding type declarations.
import { MiddlewareFn } from '../../../dist/common/types';

Check failure on line 9 in test/unit/Router/Router.spec.ts

View workflow job for this annotation

GitHub Actions / test-unit

Cannot find module '../../../dist/common/types' or its corresponding type declarations.
import { ContextFactory } from '../../../dist/context/context-factory';

Check failure on line 10 in test/unit/Router/Router.spec.ts

View workflow job for this annotation

GitHub Actions / test-unit

Cannot find module '../../../dist/context/context-factory' or its corresponding type declarations.
import { ActionHandler } from '../../../dist/utils';

Check failure on line 11 in test/unit/Router/Router.spec.ts

View workflow job for this annotation

GitHub Actions / test-unit

Cannot find module '../../../dist/utils' or its corresponding type declarations.

describe('Router', () => {
let router: Router;
const apiPrefix = '/api';
let events: EventSystem;
let mockEvents: sinon.SinonStubbedInstance<EventSystem>;
let mockReflector: sinon.SinonStubbedInstance<typeof Reflector>;

beforeEach(() => {
events = new EventSystem();
router = new Router(apiPrefix, events);
sinon.stub(MiddlewareStack.prototype, 'use');
sinon.stub(MiddlewareStack.prototype, 'execute');
});
mockEvents = sinon.createStubInstance(EventSystem);

mockReflector = {
getRoutes: sinon.stub(),
get: sinon.stub(),
update: sinon.stub(),
} as any;

// Override Reflector with the stubbed version
Reflector.getRoutes = mockReflector.getRoutes;
Reflector.get = mockReflector.get;
Reflector.update = mockReflector.update;
router = new Router('', mockEvents);
});
afterEach(() => {
sinon.restore();
});
describe('findMatch()', () => {
it('should return matched route based on method and path', () => {
const ctx: ServerRequest = {
req: { method: 'GET', url: '/api/test' } as any,
res: {} as any,
status: HttpStatus.OK,
language: 'en',
params: {},
query: {},
server: {} as any,
body: null,
bodySize: 0,
bodyRaw: Buffer.from(''),
clientIp: '127.0.0.1',
error: null,
cache: null as any,
redirect: sinon.stub(),
send: sinon.stub(),
settings: sinon.stub(),
};

describe('findMatch', () => {
it('should find a route that matches the method, path, and language', () => {
const routes: RouteDefinition[] = [
{
query: {},
params: {},
method: 'GET',
path: '/users/:id',
action: () => {},
constructor: class {},
middlewares: [],
},
];
sinon.stub(Reflector, 'getRoutes').returns(routes);
sinon.stub(Reflector, 'get').withArgs(RouterMetadataKeys.MULTI_LANG, routes[0].constructor).returns({
en: '/users/:id',
fr: '/utilisateurs/:id',
});

const match = router.findMatch('GET', '/users/123', 'en');
expect(match).to.deep.include({
const route: RouteDefinition = {
method: 'GET',
path: '/api/users/:id',
params: { id: '123' },
});
path: '/api/test',
constructor: class Test {},
action: sinon.stub(),
params: {},
query: {},
middlewares: [sinon.stub() as MiddlewareFn],
};
route.middlewares!.push(sinon.stub() as MiddlewareFn);
mockReflector.getRoutes.returns([route]);
const matchedRoute = router.findMatch(ctx);

expect(matchedRoute).to.deep.equal(route);
});

it('should return null if no route is matched', () => {
const ctx: ServerRequest = {
req: { method: 'POST', url: '/api/unknown' } as any,
res: {} as any,
status: HttpStatus.OK,
language: 'en',
params: {},
query: {},
server: {} as any,
body: null,
bodySize: 0,
bodyRaw: Buffer.from(''),
clientIp: '127.0.0.1',
error: null,
cache: null as any,
redirect: sinon.stub(),
send: sinon.stub(),
settings: sinon.stub(),
};
const route: RouteDefinition = { method: 'GET', path: '/api/test', constructor: class Test {}, action: sinon.stub(), params: {}, query: {}, middlewares: [sinon.stub() as MiddlewareFn] };
mockReflector.getRoutes.returns([route]);

const matchedRoute = router.findMatch(ctx);
expect(matchedRoute).to.be.null;
});
it('should return null if no routes match', () => {
mockReflector.getRoutes.returns([]);

const ctx: ServerRequest = {
req: { url: '/nonexistent', method: 'GET' } as any,
res: {} as any,
status: HttpStatus.OK,
language: 'en',
params: {},
query: {},
server: {} as any,
body: null,
bodySize: 0,
bodyRaw: Buffer.from(''),
clientIp: '127.0.0.1',
error: null,
cache: null as any,
redirect: sinon.stub(),
send: sinon.stub(),
settings: sinon.stub(),
};

const result = router.findMatch(ctx);
expect(result).to.be.null;
});

it('should return the matching route', () => {
const routes: RouteDefinition[] = [{ method: 'GET', path: '/test', constructor: {}, middlewares: [], action: sinon.stub(), params: {}, query: {} }];
mockReflector.getRoutes.returns(routes);

it('should return null if no route matches', () => {
const routes: RouteDefinition[] = [];
sinon.stub(Reflector, 'getRoutes').returns(routes);
const ctx: ServerRequest = {
req: { url: '/test', method: 'GET' } as any,
res: {} as any,
status: HttpStatus.OK,
language: 'en',
params: {},
query: {},
server: {} as any,
body: null,
bodySize: 0,
bodyRaw: Buffer.from(''),
clientIp: '127.0.0.1',
error: null,
cache: null as any,
redirect: sinon.stub(),
send: sinon.stub(),
settings: sinon.stub(),
};

const match = router.findMatch('GET', '/users/123', 'en');
expect(match).to.be.null;
const result = router.findMatch(ctx);
expect(result).to.deep.equal(routes[0]);
});

it('should handle multi-language routes correctly', () => {
const routes: RouteDefinition[] = [
{
query: {},
method: 'GET',
params: {},
path: '/users/:id',
action: () => {},
constructor: class {},
middlewares: [],
},
];
sinon.stub(Reflector, 'getRoutes').returns(routes);
sinon.stub(Reflector, 'get').withArgs(RouterMetadataKeys.MULTI_LANG, routes[0].constructor).returns({
en: '/users/:id',
fr: '/utilisateurs/:id',
it('should handle multi-language routes', () => {
const routes: RouteDefinition[] = [{ method: 'GET', path: '/test', constructor: {}, middlewares: [], action: sinon.stub(), params: {}, query: {} }];
const translations = { en: '/test', fr: '/essai', default: '/test' };

mockReflector.getRoutes.returns(routes);
mockReflector.get.returns(translations);

const ctx: ServerRequest = {
req: { url: '/test', method: 'GET', headers: { 'accept-language': 'en' } } as any,
res: {} as any,
language: 'en',
status: HttpStatus.OK,
params: {},
query: {},
server: {} as any,
body: null,
bodySize: 0,
bodyRaw: Buffer.from(''),
clientIp: '127.0.0.1',
error: null,
cache: null as any,
redirect: sinon.stub(),
send: sinon.stub(),
settings: sinon.stub(),
};

const result = router.findMatch(ctx);
expect(result?.path).to.equal('/test');

const ctx_fr: ServerRequest = {
...ctx,
language: 'fr',
req: { url: '/essai', method: 'GET', headers: { 'accept-language': 'fr' } } as any,
};
const result_fr = router.findMatch(ctx_fr);
expect(result_fr?.path).to.equal('/essai');

const ctx_default: ServerRequest = {
...ctx,
language: 'es',
req: { url: '/test', method: 'GET', headers: { 'accept-language': 'es' } } as any,
};
const result_default = router.findMatch(ctx_default);
expect(result_default?.path).to.equal('/test');
});
});
describe('run()', () => {
let mockCtx: ServerRequest;
let mockActionHandler: sinon.SinonStubbedInstance<ActionHandler>;
let sendSpy: sinon.SinonSpy;
beforeEach(() => {
mockActionHandler = {
wrappedAction: sinon.stub(),
} as unknown as sinon.SinonStubbedInstance<ActionHandler>;

mockCtx = {
req: { method: 'GET', url: '/api/test', socket: { remoteAddress: '127.0.0.1' } } as any,
res: { statusCode: 200, end: sinon.spy() } as any,
status: HttpStatus.OK,
language: 'en',
params: {},
query: {},
server: {} as any,
body: null,
bodySize: 0,
bodyRaw: Buffer.from(''),
clientIp: '127.0.0.1',
error: null,
cache: null as any,
redirect: sinon.stub(),
send: sinon.spy(),
settings: sinon.stub(),
};

sendSpy = mockCtx.send as sinon.SinonSpy;

sinon.stub(ContextFactory, 'createRouteContext').returns({ statusCode: HttpStatus.OK, ctx: mockCtx });
});
it('should return 304 if the response is fresh', async () => {
const route: RouteDefinition = {
method: 'GET',
path: '/api/test',
constructor: class Test {},
middlewares: [],
params: {},
query: {},
action: sinon.stub(),
};

mockReflector.getRoutes.returns([route]);
mockCtx.res.statusCode = 304;

// Run the router
await router.run(mockCtx);

// Assert the correct call to send with the expected response
sinon.assert.calledWith(sendSpy, {
statusCode: HttpStatus.NOT_MODIFIED,
message: 'Not Modified',
data: null,
});
});

it('should handle errors and return 500', async () => {
const route: RouteDefinition = {
method: 'GET',
path: '/api/test',
constructor: class Test {},
middlewares: [],
params: {},
query: {},
action: sinon.stub(),
};

mockReflector.getRoutes.returns([route]);
mockCtx.error = new Error('Test error');
await router.run(mockCtx);
sinon.assert.calledWith(sendSpy, {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Internal Server Error',
error: mockCtx.error,
});
});

const match = router.findMatch('GET', '/utilisateurs/123', 'fr');
expect(match).to.deep.include({
it('should handle 304 Not Modified', async () => {
mockCtx.res.statusCode = 304;
const route: RouteDefinition = {
method: 'GET',
path: '/api/utilisateurs/:id',
params: { id: '123' },
path: '/api/test',
constructor: class Test {},
middlewares: [],
params: {},
query: {},
action: sinon.stub(),
};
mockReflector.getRoutes.returns([route]);
await router.run(mockCtx);
sinon.assert.calledWith(sendSpy, {
statusCode: HttpStatus.NOT_MODIFIED,
message: 'Not Modified',
data: null,
});
});
});
Expand Down

0 comments on commit 09579a1

Please sign in to comment.