generated from expressots/expressots-project-template
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #196 from expressots/feature/micro-api
feat: implement Express Micro API adapter with route management and I…
- Loading branch information
Showing
12 changed files
with
808 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
...-express/micro-api/application-express-micro-container.early.spec/addScoped.early.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
50 changes: 50 additions & 0 deletions
50
...press/micro-api/application-express-micro-container.early.spec/addSingleton.early.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
44 changes: 44 additions & 0 deletions
44
...press/micro-api/application-express-micro-container.early.spec/addTransient.early.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); | ||
}); |
31 changes: 31 additions & 0 deletions
31
...dapter-express/micro-api/application-express-micro-container.early.spec/get.early.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
65 changes: 65 additions & 0 deletions
65
src/adapter-express/micro-api/application-express-micro-container.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
src/adapter-express/micro-api/application-express-micro-route.early.spec/post.early.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.