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(); 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); }); });