From 688454b5c2130877354d105f3b8bfc2adb5d4438 Mon Sep 17 00:00:00 2001 From: M1000fr Date: Sun, 11 Jan 2026 17:21:49 +0100 Subject: [PATCH] test: split container.test.ts into modular test files --- test/circular-dependency.test.ts | 73 ++++++ test/container.test.ts | 416 ------------------------------- test/injection.test.ts | 123 +++++++++ test/lifecycle.test.ts | 71 ++++++ test/modules.test.ts | 95 +++++++ test/providers.test.ts | 78 ++++++ 6 files changed, 440 insertions(+), 416 deletions(-) create mode 100644 test/circular-dependency.test.ts delete mode 100644 test/container.test.ts create mode 100644 test/injection.test.ts create mode 100644 test/lifecycle.test.ts create mode 100644 test/modules.test.ts create mode 100644 test/providers.test.ts diff --git a/test/circular-dependency.test.ts b/test/circular-dependency.test.ts new file mode 100644 index 0000000..e54757e --- /dev/null +++ b/test/circular-dependency.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test"; +import "reflect-metadata"; +import { + AlveoFactory, + CircularDependencyError, + forwardRef, + Inject, + Injectable, + Module, +} from "../index"; + +describe("Alveo Circular Dependencies", () => { + test("should detect circular dependencies without forwardRef", async () => { + @Injectable() + class A { + constructor(_b: unknown) {} + } + @Injectable() + class B { + constructor(_a: A) {} + } + + // Manually define metadata to simulate circular dep without forwardRef + Reflect.defineMetadata("design:paramtypes", [Object], A); + Reflect.defineMetadata("design:paramtypes", [A], B); + Reflect.defineMetadata("alveo:inject", [B], A); + + @Module({ + providers: [A, B], + }) + class RootModule {} + + expect(AlveoFactory.create(RootModule)).rejects.toThrow( + CircularDependencyError, + ); + }); + + test("should resolve circular dependencies with forwardRef", async () => { + @Injectable() + class ServiceA { + constructor( + @Inject(forwardRef(() => ServiceB)) + public readonly serviceB: { name: () => string }, + ) {} + name() { + return "A"; + } + } + + @Injectable() + class ServiceB { + constructor( + @Inject(forwardRef(() => ServiceA)) + public readonly serviceA: { name: () => string }, + ) {} + name() { + return "B"; + } + } + + @Module({ + providers: [ServiceA, ServiceB], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + const a = await app.get(ServiceA); + const b = await app.get(ServiceB); + + expect(a.serviceB.name()).toBe("B"); + expect(b.serviceA.name()).toBe("A"); + }); +}); diff --git a/test/container.test.ts b/test/container.test.ts deleted file mode 100644 index 4f0937b..0000000 --- a/test/container.test.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import "reflect-metadata"; -import { - AlveoFactory, - CircularDependencyError, - Container, - forwardRef, - Inject, - Injectable, - Module, - ProviderNotFoundError, -} from "../index"; - -describe("Alveo Container & DI", () => { - test("should resolve a simple singleton service", async () => { - @Injectable() - class SimpleService { - getValue() { - return "hello"; - } - } - - @Module({ - providers: [SimpleService], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - const service = await app.get(SimpleService); - - expect(service).toBeInstanceOf(SimpleService); - expect(service.getValue()).toBe("hello"); - }); - - test("should inject dependencies via constructor", async () => { - @Injectable() - class Dependency { - name = "dep"; - } - - @Injectable() - class MainService { - constructor(public readonly dep: Dependency) {} - } - - @Module({ - providers: [Dependency, MainService], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - const main = await app.get(MainService); - - expect(main.dep).toBeInstanceOf(Dependency); - expect(main.dep.name).toBe("dep"); - }); - - test("should support Value Providers", async () => { - const TOKEN = Symbol("CONFIG"); - - @Injectable() - class Service { - constructor( - @Inject(TOKEN) public readonly config: { apiKey: string }, - ) {} - } - - @Module({ - providers: [ - Service, - { provide: TOKEN, useValue: { apiKey: "123" } }, - ], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - const service = await app.get(Service); - - expect(service.config.apiKey).toBe("123"); - }); - - test("should support Factory Providers", async () => { - const TOKEN = "FACTORY"; - - @Module({ - providers: [ - { - provide: TOKEN, - useFactory: () => "dynamic_value", - }, - ], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - const value = await app.get(TOKEN); - - expect(value).toBe("dynamic_value"); - }); - - test("should support Asynchronous Factory Providers", async () => { - const TOKEN = "ASYNC_FACTORY"; - - @Module({ - providers: [ - { - provide: TOKEN, - useFactory: async () => { - await new Promise((resolve) => setTimeout(resolve, 10)); - return "async_value"; - }, - }, - ], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - const value = await app.get(TOKEN); - - expect(value).toBe("async_value"); - }); - - test("should handle concurrent resolution of the same async provider", async () => { - const TOKEN = "CONCURRENT_ASYNC"; - let callCount = 0; - - @Module({ - providers: [ - { - provide: TOKEN, - useFactory: async () => { - callCount++; - await new Promise((resolve) => setTimeout(resolve, 20)); - return { id: Math.random() }; - }, - }, - ], - }) - class RootModule {} - - // We use a fresh container to test concurrent resolution - // bypassing the sequential resolveAll() of AlveoFactory.create() - const container = new Container(); - await container.registerRootModule(RootModule); - - const [val1, val2] = await Promise.all([ - container.get(TOKEN), - container.get(TOKEN), - ]); - - expect(val1).toBe(val2); - expect(callCount).toBe(1); - }); - - test("should handle Module imports and exports", async () => { - @Injectable() - class SharedService { - identify() { - return "shared"; - } - } - - @Module({ - providers: [SharedService], - exports: [SharedService], - }) - class LibModule {} - - @Module({ - imports: [LibModule], - }) - class AppModule {} - - const app = await AlveoFactory.create(AppModule); - const service = await app.get(SharedService); - - expect(service.identify()).toBe("shared"); - }); - - test("should throw ProviderNotFoundError when dependency is missing", async () => { - @Injectable() - class MissingDep {} - - @Injectable() - class Target { - constructor(_dep: MissingDep) {} - } - - @Module({ - providers: [Target], // MissingDep is not provided - }) - class RootModule {} - - expect(AlveoFactory.create(RootModule)).rejects.toThrow( - ProviderNotFoundError, - ); - }); - - test("should detect circular dependencies without forwardRef", async () => { - @Injectable() - class A { - constructor(_b: unknown) {} - } - @Injectable() - class B { - constructor(_a: A) {} - } - - // Manually define metadata to simulate circular dep without forwardRef - Reflect.defineMetadata("design:paramtypes", [Object], A); - Reflect.defineMetadata("design:paramtypes", [A], B); - Reflect.defineMetadata("alveo:inject", [B], A); - - @Module({ - providers: [A, B], - }) - class RootModule {} - - expect(AlveoFactory.create(RootModule)).rejects.toThrow( - CircularDependencyError, - ); - }); - - test("should resolve circular dependencies with forwardRef", async () => { - @Injectable() - class ServiceA { - constructor( - @Inject(forwardRef(() => ServiceB)) - public readonly serviceB: { name: () => string }, - ) {} - name() { - return "A"; - } - } - - @Injectable() - class ServiceB { - constructor( - @Inject(forwardRef(() => ServiceA)) - public readonly serviceA: { name: () => string }, - ) {} - name() { - return "B"; - } - } - - @Module({ - providers: [ServiceA, ServiceB], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - const a = await app.get(ServiceA); - const b = await app.get(ServiceB); - - expect(a.serviceB.name()).toBe("B"); - expect(b.serviceA.name()).toBe("A"); - }); - - test("should call lifecycle hooks", async () => { - let initCalled = false; - let destroyCalled = false; - - @Injectable() - class LifecycleService { - async onModuleInit() { - initCalled = true; - } - async onModuleDestroy() { - destroyCalled = true; - } - } - - @Module({ - providers: [LifecycleService], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - expect(initCalled).toBe(true); - - await app.close(); - expect(destroyCalled).toBe(true); - }); - - test("should support existing providers (aliases)", async () => { - const ALIAS = "ALIAS_TOKEN"; - - @Injectable() - class RealService { - ok() { - return true; - } - } - - @Module({ - providers: [ - RealService, - { provide: ALIAS, useExisting: RealService }, - ], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - const real = await app.get(RealService); - const aliased = await app.get(ALIAS); - - expect(aliased).toBe(real); - expect((aliased as RealService).ok()).toBe(true); - }); - - test("should support Synchronous Dynamic Modules", async () => { - @Injectable() - class DynamicService { - constructor(@Inject("CONFIG") public readonly config: string) {} - } - - @Module({}) - class DynamicModule { - static forRoot(config: string) { - return { - module: DynamicModule, - providers: [ - DynamicService, - { provide: "CONFIG", useValue: config }, - ], - exports: [DynamicService], - }; - } - } - - @Module({ - imports: [DynamicModule.forRoot("dynamic_config")], - }) - class AppModule {} - - const app = await AlveoFactory.create(AppModule); - const service = await app.get(DynamicService); - - expect(service.config).toBe("dynamic_config"); - }); - - test("should support Asynchronous Dynamic Modules", async () => { - @Injectable() - class AsyncDynamicService { - constructor( - @Inject("ASYNC_CONFIG") public readonly config: string, - ) {} - } - - @Module({}) - class AsyncDynamicModule { - static async forRoot(config: string) { - await new Promise((resolve) => setTimeout(resolve, 10)); - return { - module: AsyncDynamicModule, - providers: [ - AsyncDynamicService, - { provide: "ASYNC_CONFIG", useValue: config }, - ], - exports: [AsyncDynamicService], - }; - } - } - - @Module({ - imports: [AsyncDynamicModule.forRoot("async_dynamic_config")], - }) - class AppModule {} - - const app = await AlveoFactory.create(AppModule); - const service = await app.get(AsyncDynamicService); - - expect(service.config).toBe("async_dynamic_config"); - }); - - test("should call all lifecycle hooks in the correct order", async () => { - const callOrder: string[] = []; - - @Injectable() - class FullLifecycleService { - async onModuleInit() { - callOrder.push("onModuleInit"); - } - async onApplicationBootstrap() { - callOrder.push("onApplicationBootstrap"); - } - async onModuleDestroy() { - callOrder.push("onModuleDestroy"); - } - async beforeApplicationShutdown(signal?: string) { - callOrder.push(`beforeApplicationShutdown:${signal}`); - } - async onApplicationShutdown(signal?: string) { - callOrder.push(`onApplicationShutdown:${signal}`); - } - } - - @Module({ - providers: [FullLifecycleService], - }) - class RootModule {} - - const app = await AlveoFactory.create(RootModule); - expect(callOrder).toEqual(["onModuleInit", "onApplicationBootstrap"]); - - await app.close("SIGTERM"); - expect(callOrder).toEqual([ - "onModuleInit", - "onApplicationBootstrap", - "onModuleDestroy", - "beforeApplicationShutdown:SIGTERM", - "onApplicationShutdown:SIGTERM", - ]); - }); -}); diff --git a/test/injection.test.ts b/test/injection.test.ts new file mode 100644 index 0000000..b68b599 --- /dev/null +++ b/test/injection.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, test } from "bun:test"; +import "reflect-metadata"; +import { + AlveoFactory, + Inject, + Injectable, + Module, + ProviderNotFoundError, +} from "../index"; + +describe("Alveo Injection", () => { + test("should resolve a simple singleton service", async () => { + @Injectable() + class SimpleService { + getValue() { + return "hello"; + } + } + + @Module({ + providers: [SimpleService], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + const service = await app.get(SimpleService); + + expect(service).toBeInstanceOf(SimpleService); + expect(service.getValue()).toBe("hello"); + }); + + test("should inject dependencies via constructor", async () => { + @Injectable() + class Dependency { + name = "dep"; + } + + @Injectable() + class MainService { + constructor(public readonly dep: Dependency) {} + } + + @Module({ + providers: [Dependency, MainService], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + const main = await app.get(MainService); + + expect(main.dep).toBeInstanceOf(Dependency); + expect(main.dep.name).toBe("dep"); + }); + + test("should support Value Providers", async () => { + const TOKEN = Symbol("CONFIG"); + + @Injectable() + class Service { + constructor( + @Inject(TOKEN) public readonly config: { apiKey: string }, + ) {} + } + + @Module({ + providers: [ + Service, + { provide: TOKEN, useValue: { apiKey: "123" } }, + ], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + const service = await app.get(Service); + + expect(service.config.apiKey).toBe("123"); + }); + + test("should support existing providers (aliases)", async () => { + const ALIAS = "ALIAS_TOKEN"; + + @Injectable() + class RealService { + ok() { + return true; + } + } + + @Module({ + providers: [ + RealService, + { provide: ALIAS, useExisting: RealService }, + ], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + const real = await app.get(RealService); + const aliased = await app.get(ALIAS); + + expect(aliased).toBe(real); + expect((aliased as RealService).ok()).toBe(true); + }); + + test("should throw ProviderNotFoundError when dependency is missing", async () => { + @Injectable() + class MissingDep {} + + @Injectable() + class Target { + constructor(_dep: MissingDep) {} + } + + @Module({ + providers: [Target], + }) + class RootModule {} + + expect(AlveoFactory.create(RootModule)).rejects.toThrow( + ProviderNotFoundError, + ); + }); +}); diff --git a/test/lifecycle.test.ts b/test/lifecycle.test.ts new file mode 100644 index 0000000..38905b7 --- /dev/null +++ b/test/lifecycle.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "bun:test"; +import "reflect-metadata"; +import { AlveoFactory, Injectable, Module } from "../index"; + +describe("Alveo Lifecycles", () => { + test("should call lifecycle hooks", async () => { + let initCalled = false; + let destroyCalled = false; + + @Injectable() + class LifecycleService { + async onModuleInit() { + initCalled = true; + } + async onModuleDestroy() { + destroyCalled = true; + } + } + + @Module({ + providers: [LifecycleService], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + expect(initCalled).toBe(true); + + await app.close(); + expect(destroyCalled).toBe(true); + }); + + test("should call all lifecycle hooks in the correct order", async () => { + const callOrder: string[] = []; + + @Injectable() + class FullLifecycleService { + async onModuleInit() { + callOrder.push("onModuleInit"); + } + async onApplicationBootstrap() { + callOrder.push("onApplicationBootstrap"); + } + async onModuleDestroy() { + callOrder.push("onModuleDestroy"); + } + async beforeApplicationShutdown(signal?: string) { + callOrder.push(`beforeApplicationShutdown:${signal}`); + } + async onApplicationShutdown(signal?: string) { + callOrder.push(`onApplicationShutdown:${signal}`); + } + } + + @Module({ + providers: [FullLifecycleService], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + expect(callOrder).toEqual(["onModuleInit", "onApplicationBootstrap"]); + + await app.close("SIGTERM"); + expect(callOrder).toEqual([ + "onModuleInit", + "onApplicationBootstrap", + "onModuleDestroy", + "beforeApplicationShutdown:SIGTERM", + "onApplicationShutdown:SIGTERM", + ]); + }); +}); diff --git a/test/modules.test.ts b/test/modules.test.ts new file mode 100644 index 0000000..fef539e --- /dev/null +++ b/test/modules.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, test } from "bun:test"; +import "reflect-metadata"; +import { AlveoFactory, Inject, Injectable, Module } from "../index"; + +describe("Alveo Modules", () => { + test("should handle Module imports and exports", async () => { + @Injectable() + class SharedService { + identify() { + return "shared"; + } + } + + @Module({ + providers: [SharedService], + exports: [SharedService], + }) + class LibModule {} + + @Module({ + imports: [LibModule], + }) + class AppModule {} + + const app = await AlveoFactory.create(AppModule); + const service = await app.get(SharedService); + + expect(service.identify()).toBe("shared"); + }); + + test("should support Synchronous Dynamic Modules", async () => { + @Injectable() + class DynamicService { + constructor(@Inject("CONFIG") public readonly config: string) {} + } + + @Module({}) + class DynamicModule { + static forRoot(config: string) { + return { + module: DynamicModule, + providers: [ + DynamicService, + { provide: "CONFIG", useValue: config }, + ], + exports: [DynamicService], + }; + } + } + + @Module({ + imports: [DynamicModule.forRoot("dynamic_config")], + }) + class AppModule {} + + const app = await AlveoFactory.create(AppModule); + const service = await app.get(DynamicService); + + expect(service.config).toBe("dynamic_config"); + }); + + test("should support Asynchronous Dynamic Modules", async () => { + @Injectable() + class AsyncDynamicService { + constructor( + @Inject("ASYNC_CONFIG") public readonly config: string, + ) {} + } + + @Module({}) + class AsyncDynamicModule { + static async forRoot(config: string) { + await new Promise((resolve) => setTimeout(resolve, 10)); + return { + module: AsyncDynamicModule, + providers: [ + AsyncDynamicService, + { provide: "ASYNC_CONFIG", useValue: config }, + ], + exports: [AsyncDynamicService], + }; + } + } + + @Module({ + imports: [AsyncDynamicModule.forRoot("async_dynamic_config")], + }) + class AppModule {} + + const app = await AlveoFactory.create(AppModule); + const service = await app.get(AsyncDynamicService); + + expect(service.config).toBe("async_dynamic_config"); + }); +}); diff --git a/test/providers.test.ts b/test/providers.test.ts new file mode 100644 index 0000000..32aae32 --- /dev/null +++ b/test/providers.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "bun:test"; +import "reflect-metadata"; +import { AlveoFactory, Container, Module } from "../index"; + +describe("Alveo Providers", () => { + test("should support Factory Providers", async () => { + const TOKEN = "FACTORY"; + + @Module({ + providers: [ + { + provide: TOKEN, + useFactory: () => "dynamic_value", + }, + ], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + const value = await app.get(TOKEN); + + expect(value).toBe("dynamic_value"); + }); + + test("should support Asynchronous Factory Providers", async () => { + const TOKEN = "ASYNC_FACTORY"; + + @Module({ + providers: [ + { + provide: TOKEN, + useFactory: async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return "async_value"; + }, + }, + ], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + const value = await app.get(TOKEN); + + expect(value).toBe("async_value"); + }); + + test("should handle concurrent resolution of the same async provider", async () => { + const TOKEN = "CONCURRENT_ASYNC"; + let callCount = 0; + + @Module({ + providers: [ + { + provide: TOKEN, + useFactory: async () => { + callCount++; + await new Promise((resolve) => setTimeout(resolve, 20)); + return { id: Math.random() }; + }, + }, + ], + }) + class RootModule {} + + // We use a fresh container to test concurrent resolution + // bypassing the sequential resolveAll() of AlveoFactory.create() + const container = new Container(); + await container.registerRootModule(RootModule); + + const [val1, val2] = await Promise.all([ + container.get(TOKEN), + container.get(TOKEN), + ]); + + expect(val1).toBe(val2); + expect(callCount).toBe(1); + }); +});