Skip to content

Commit

Permalink
Merge pull request #196 from expressots/feature/micro-api
Browse files Browse the repository at this point in the history
feat: implement Express Micro API adapter with route management and I…
  • Loading branch information
rsaz authored Nov 24, 2024
2 parents ad26682 + 82895c2 commit e0a50ae
Show file tree
Hide file tree
Showing 12 changed files with 808 additions and 2 deletions.
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const config: JestConfigWithTsJest = {
roots: ["<rootDir>/src"],
testRegex: ".*\\.spec\\.ts$",
testPathIgnorePatterns: ["/node_modules/", "/lib/"],
collectCoverageFrom: ["src/**/*.ts", "!**/*.spec.ts", "src/**/index.ts"],
collectCoverageFrom: ["src/**/*.ts", "!**/*.spec.ts", "src/**/index.ts", "!src/di/**"],
moduleNameMapper: {
"^@src/(.*)$": "<rootDir>/src/$1",
},
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"@codecov/vite-plugin": "^0.0.1-beta.6",
"@commitlint/cli": "19.2.1",
"@commitlint/config-conventional": "19.2.2",
"@expressots/core": "3.0.0-beta.1",
"@expressots/core": "^3.0.0-beta.1",
"@expressots/shared": "0.2.0",
"@release-it/conventional-changelog": "8.0.1",
"@types/express": "4.17.21",
Expand Down
1 change: 1 addition & 0 deletions src/adapter-express/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./express-utils";
export { AppExpress } from "./application-express";
export * from "./micro-api";
export {
IWebServerPublic,
IWebServer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Unit tests for: addScoped

import { IOC } from "../application-express-micro-container";

// Mock definitions
type MockNewable<T> = { new (...args: any[]): T };

class MockContainer {
bind = jest.fn().mockReturnThis();
toSelf = jest.fn().mockReturnThis();
inRequestScope = jest.fn().mockReturnThis();
}

interface MockContainerOptions {}

describe("IOC.addScoped() addScoped method", () => {
let mockContainer: MockContainer;
let ioc: IOC;

beforeEach(() => {
mockContainer = new MockContainer() as any;
ioc = new IOC({} as MockContainerOptions) as any;
(ioc as any).container = mockContainer;
});

describe("Happy Paths", () => {
it("should bind a class to the container in request scope", () => {
// Arrange
class TestClass {}
const mockNewable: MockNewable<TestClass> = TestClass as any;

// Act
ioc.addScoped(mockNewable);

// Assert
expect(mockContainer.bind).toHaveBeenCalledWith(mockNewable);
expect(mockContainer.toSelf).toHaveBeenCalled();
expect(mockContainer.inRequestScope).toHaveBeenCalled();
});
});

describe("Edge Cases", () => {
it("should handle binding a class with no constructor parameters", () => {
// Arrange
class NoParamClass {}
const mockNewable: MockNewable<NoParamClass> = NoParamClass as any;

// Act
ioc.addScoped(mockNewable);

// Assert
expect(mockContainer.bind).toHaveBeenCalledWith(mockNewable);
expect(mockContainer.toSelf).toHaveBeenCalled();
expect(mockContainer.inRequestScope).toHaveBeenCalled();
});

it("should handle binding a class with multiple constructor parameters", () => {
// Arrange
class MultiParamClass {
constructor(
public param1: string,
public param2: number,
) {}
}
const mockNewable: MockNewable<MultiParamClass> = MultiParamClass as any;

// Act
ioc.addScoped(mockNewable);

// Assert
expect(mockContainer.bind).toHaveBeenCalledWith(mockNewable);
expect(mockContainer.toSelf).toHaveBeenCalled();
expect(mockContainer.inRequestScope).toHaveBeenCalled();
});

it("should handle binding a class with complex constructor parameters", () => {
// Arrange
class ComplexParamClass {
constructor(
public param1: { key: string },
public param2: () => void,
) {}
}
const mockNewable: MockNewable<ComplexParamClass> = ComplexParamClass as any;

// Act
ioc.addScoped(mockNewable);

// Assert
expect(mockContainer.bind).toHaveBeenCalledWith(mockNewable);
expect(mockContainer.toSelf).toHaveBeenCalled();
expect(mockContainer.inRequestScope).toHaveBeenCalled();
});
});
});

// End of unit tests for: addScoped
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Container, injectable } from "@expressots/core";
import { IOC } from "../application-express-micro-container";

let ioc: IOC;

describe("IOC.addSingleton() method", () => {
beforeEach(() => {
ioc = new IOC();
});

it("should bind a class as a singleton", () => {
// Arrange
@injectable()
class TestClass {}
const concrete = TestClass;

// Act
ioc.addSingleton(concrete);

// Assert
expect(ioc.get(concrete)).toBeInstanceOf(TestClass);
});

it("should handle binding a class with no constructor parameters", () => {
// Arrange
@injectable()
class NoParamClass {}
const concrete = NoParamClass;

// Act
ioc.addSingleton(concrete);

// Assert
expect(ioc.get(concrete)).toBeInstanceOf(NoParamClass);
});

it("should not throw an error when binding a class that is already a singleton", () => {
// Arrange
@injectable()
class ExistingSingletonClass {}
const concrete = ExistingSingletonClass;

// Act & Assert
expect(() => ioc.addSingleton(concrete)).not.toThrow();

// Assert
const instance = ioc.get(concrete);
expect(instance).toBeInstanceOf(ExistingSingletonClass);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Container, injectable } from "@expressots/core";
import { IOC } from "../application-express-micro-container";

let ioc: IOC;

describe("IOC.addTransient() method", () => {
beforeEach(() => {
ioc = new IOC();
});

it("should bind a class to the container in transient scope", () => {
// Arrange
@injectable()
class TestClass {}
const concrete = TestClass;

// Act
ioc.addTransient(concrete);

// Assert
const instance1 = ioc.get(concrete);
const instance2 = ioc.get(concrete);
expect(instance1).toBeInstanceOf(TestClass);
expect(instance2).toBeInstanceOf(TestClass);
expect(instance1).not.toBe(instance2); // Different instances for transient scope
});

it("should handle binding a class with no constructor parameters", () => {
// Arrange
@injectable()
class NoParamClass {}
const concrete = NoParamClass;

// Act
ioc.addTransient(concrete);

// Assert
const instance1 = ioc.get(concrete);
const instance2 = ioc.get(concrete);
expect(instance1).toBeInstanceOf(NoParamClass);
expect(instance2).toBeInstanceOf(NoParamClass);
expect(instance1).not.toBe(instance2); // Different instances for transient scope
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Container, injectable } from "@expressots/core";
import { IOC } from "../application-express-micro-container";

let ioc: IOC;

describe("IOC.get() get method", () => {
let mockContainer: Container;
let ioc: IOC;

beforeEach(() => {
ioc = new IOC();
mockContainer = new Container();
});

describe("Happy paths", () => {
it("should return an instance when a valid identifier is provided", () => {
// Arrange
@injectable()
class MockService {}
mockContainer.bind(MockService).toSelf().inRequestScope();

// Act
const mock = mockContainer.get(MockService);

// Assert
expect(mock).toBeInstanceOf(MockService);
});
});
});

// End of unit tests for: get
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Container, interfaces } from "@expressots/core";

/**
* Inversion of Control Container interface
* @public API
*/
export interface IIOC {
addSingleton<T>(concrete: interfaces.Newable<T>): void;
addTransient<T>(concrete: interfaces.Newable<T>): void;
addScoped<T>(concrete: interfaces.Newable<T>): void;
get<I>(identifier: interfaces.ServiceIdentifier<I>): I;
}

/**
* Inversion of Control Container
* @public API
*/
export class IOC {
private container: Container;

constructor(containerOptions?: interfaces.ContainerOptions) {
this.container = new Container(containerOptions);
}

/**
* Add a singleton to the container
* @param identifierOrConcrete - The identifier or concrete class
* @param concrete - The concrete class if identifier is provided
* @public API
*/
public addSingleton<T>(concrete: interfaces.Newable<T>): void {
this.container.bind(concrete).toSelf().inSingletonScope();
}

/**
* Add a transient to the container
* @param identifierOrConcrete - The identifier or concrete class
* @param concrete - The concrete class if identifier is provided
* @public API
*/
public addTransient<T>(concrete: interfaces.Newable<T>): void {
this.container.bind(concrete).toSelf().inTransientScope();
}

/**
* Add a scoped to the container
* @param identifierOrConcrete - The identifier or concrete class
* @param concrete - The concrete class if identifier is provided
* @public API
*/
public addScoped<T>(concrete: interfaces.Newable<T>): void {
this.container.bind(concrete).toSelf().inRequestScope();
}

/**
* Get an instance from the container
* @param identifier - The identifier for the instance
* @returns The resolved instance
* @public API
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public get(identifier: interfaces.ServiceIdentifier): any {
return this.container.get(identifier);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Unit tests for: post

import express from "express";
import { Route } from "../application-express-micro-route";

// Mock definitions
type MockMiddleware = jest.Mock;
class MockLogger {
info = jest.fn();
}

describe("Route.post() post method", () => {
let mockApp: express.Application;
let route: Route;
let mockLogger: MockLogger;

beforeEach(() => {
mockApp = express() as any;
mockLogger = new MockLogger() as any;
route = new Route(mockApp as any);
(route as any).logger = mockLogger;
});

describe("Happy Paths", () => {
it("should define a POST route with the correct path and handler", () => {
const mockHandler = jest.fn();
const mockMiddleware: MockMiddleware = jest.fn();

route.post("/test", mockHandler, mockMiddleware as any);

expect((route as any).routes).toHaveLength(1);
expect((route as any).routes[0]).toEqual({
method: "post",
path: "/test",
handler: mockHandler,
middleware: [mockMiddleware],
} as any);
});

it("should apply global prefix to the route path", () => {
const mockHandler = jest.fn();
route.setGlobalRoutePrefix("/api");

route.post("/test", mockHandler);

expect((route as any).routes[0].path).toBe("/api/test");
});
});

describe("Edge Cases", () => {
it("should handle empty middleware array gracefully", () => {
const mockHandler = jest.fn();

route.post("/test", mockHandler);

expect((route as any).routes[0].middleware).toEqual([]);
});

it("should handle leading and trailing slashes in global prefix and path", () => {
const mockHandler = jest.fn();
route.setGlobalRoutePrefix("/api/");

route.post("/test/", mockHandler);

expect((route as any).routes[0].path).toBe("/api/test/");
});

it("should handle multiple middleware functions", () => {
const mockHandler = jest.fn();
const mockMiddleware1: MockMiddleware = jest.fn();
const mockMiddleware2: MockMiddleware = jest.fn();

route.post("/test", mockHandler, mockMiddleware1 as any, mockMiddleware2 as any);

expect((route as any).routes[0].middleware).toEqual([mockMiddleware1, mockMiddleware2]);
});
});
});

// End of unit tests for: post
Loading

0 comments on commit e0a50ae

Please sign in to comment.