diff --git a/README.md b/README.md index 1aad8fe..152d559 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

@nestbolt/factory

-

Laravel-like model factories and database seeders for NestJS with TypeORM.

+

Model factories and database seeders for NestJS with TypeORM.

@@ -407,10 +407,6 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details. If you discover any security-related issues, please report them via [GitHub Issues](https://github.com/nestbolt/factory/issues) with the **security** label instead of using the public issue tracker. -## Credits - -- Inspired by [Laravel's](https://laravel.com) model factories and [database seeders](https://laravel.com/docs/seeding) - ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/package.json b/package.json index a059491..c03cc36 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@nestbolt/factory", "version": "0.1.0", "packageManager": "pnpm@9.15.4", - "description": "Laravel-like model factories and database seeders for NestJS with TypeORM.", + "description": "Model factories and database seeders for NestJS with TypeORM.", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { diff --git a/test/factory-builder.spec.ts b/test/factory-builder.spec.ts index 1b3f28c..869e60a 100644 --- a/test/factory-builder.spec.ts +++ b/test/factory-builder.spec.ts @@ -7,7 +7,7 @@ import { FactoryService } from "../src/factory.service"; import { FactoryBuilder } from "../src/factory-builder"; import { Sequence } from "../src/sequence"; import { FACTORY_OPTIONS } from "../src/factory.constants"; -import { User, Post, UserFactory, PostFactory, CommentFactory } from "./helpers/test-entities"; +import { User, Post, UserFactory, PostFactory, CommentFactory, UserFactoryWithHooks } from "./helpers/test-entities"; describe("FactoryBuilder", () => { let module: TestingModule; @@ -27,7 +27,7 @@ describe("FactoryBuilder", () => { { provide: FACTORY_OPTIONS, useValue: { - factories: [UserFactory, PostFactory, CommentFactory], + factories: [UserFactory, PostFactory, CommentFactory, UserFactoryWithHooks], }, }, FactoryService, @@ -107,6 +107,12 @@ describe("FactoryBuilder", () => { expect(user.role).toBe("admin"); }); + it("should ignore non-existent named state", async () => { + const user = (await service.use(UserFactory).state("nonExistentState").make()) as User; + expect(user).toBeDefined(); + expect(user.role).toBe("user"); + }); + it("should apply object state", async () => { const user = (await service.use(UserFactory).state({ role: "moderator" }).make()) as User; expect(user.role).toBe("moderator"); @@ -205,4 +211,43 @@ describe("FactoryBuilder", () => { expect(callbackEntity!.name).toBe(user.name); }); }); + + describe("factory afterMake hook", () => { + it("should call factory afterMake when defined", async () => { + const user = (await service.use(UserFactoryWithHooks).make()) as User; + expect(user.name).toMatch(/^\[made\] /); + }); + }); + + describe("factory afterCreate hook", () => { + it("should call factory afterCreate when defined", async () => { + const user = (await service.use(UserFactoryWithHooks).create()) as User; + expect(user.name).toMatch(/^\[created\] /); + }); + }); + + describe("create() without DataSource", () => { + it("should throw when DataSource is not available", async () => { + const noDbModule = await Test.createTestingModule({ + providers: [ + { + provide: FACTORY_OPTIONS, + useValue: { + factories: [UserFactory], + }, + }, + FactoryService, + ], + }).compile(); + + await noDbModule.init(); + const noDbService = noDbModule.get(FactoryService); + + await expect(noDbService.use(UserFactory).create()).rejects.toThrow( + "DataSource is not available", + ); + + await noDbModule.close(); + }); + }); }); diff --git a/test/factory.service.spec.ts b/test/factory.service.spec.ts index f3b6c2f..9d8b603 100644 --- a/test/factory.service.spec.ts +++ b/test/factory.service.spec.ts @@ -6,8 +6,9 @@ import { DataSource } from "typeorm"; import { FactoryService } from "../src/factory.service"; import { FactoryBuilder } from "../src/factory-builder"; import { FactoryNotRegisteredException } from "../src/exceptions/factory-not-registered.exception"; +import { FactoryNotInitializedException } from "../src/exceptions/factory-not-initialized.exception"; import { FACTORY_OPTIONS } from "../src/factory.constants"; -import { User, Post, Comment, UserFactory, PostFactory, CommentFactory } from "./helpers/test-entities"; +import { User, Post, Comment, UserFactory, PostFactory, CommentFactory, DatabaseSeeder } from "./helpers/test-entities"; describe("FactoryService", () => { let module: TestingModule; @@ -147,4 +148,61 @@ describe("FactoryService", () => { module = undefined as any; }); }); + + describe("FactoryNotInitializedException", () => { + it("should have correct message and name", () => { + const error = new FactoryNotInitializedException(); + expect(error.message).toContain("FactoryService has not been initialized"); + expect(error.name).toBe("FactoryNotInitializedException"); + expect(error).toBeInstanceOf(Error); + }); + }); + + describe("eventEmitter integration", () => { + it("should emit events when eventEmitter is provided", async () => { + const emitted: { event: string; payload: any }[] = []; + const mockEventEmitter = { + emit(event: string, payload: any) { + emitted.push({ event, payload }); + return true; + }, + }; + + const eventModule = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot({ + type: "better-sqlite3", + database: ":memory:", + entities: [User, Post, Comment], + synchronize: true, + }), + ], + providers: [ + { + provide: FACTORY_OPTIONS, + useValue: { + factories: [UserFactory, PostFactory, CommentFactory], + seeders: [DatabaseSeeder], + }, + }, + { + provide: "EventEmitter2", + useValue: mockEventEmitter, + }, + FactoryService, + ], + }).compile(); + + await eventModule.init(); + const eventService = eventModule.get(FactoryService); + + await eventService.seed(); + + expect(emitted.length).toBeGreaterThan(0); + expect(emitted[0].event).toBe("factory.seed.all.started"); + expect(emitted[emitted.length - 1].event).toBe("factory.seed.all.completed"); + + await eventModule.close(); + }); + }); }); diff --git a/test/helpers/test-entities.ts b/test/helpers/test-entities.ts index 1ef829f..57e125b 100644 --- a/test/helpers/test-entities.ts +++ b/test/helpers/test-entities.ts @@ -109,6 +109,30 @@ export class CommentFactory extends BaseFactory { } } +export class UserFactoryWithHooks extends BaseFactory { + get entity() { + return User; + } + + definition(faker: Faker): Partial { + return { + name: faker.person.fullName(), + email: faker.internet.email(), + role: "user", + age: faker.number.int({ min: 18, max: 65 }), + active: true, + }; + } + + async afterMake(entity: User): Promise { + entity.name = `[made] ${entity.name}`; + } + + async afterCreate(entity: User): Promise { + entity.name = `[created] ${entity.name}`; + } +} + export class DatabaseSeeder implements Seeder { order = 0; diff --git a/test/seeder.spec.ts b/test/seeder.spec.ts index 3d59060..b6b4e87 100644 --- a/test/seeder.spec.ts +++ b/test/seeder.spec.ts @@ -140,6 +140,84 @@ describe("Seeder", () => { }); }); + describe("seed() with no seeders", () => { + it("should complete without error when no seeders configured", async () => { + const noSeederModule = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot({ + type: "better-sqlite3", + database: ":memory:", + entities: [User], + synchronize: true, + }), + ], + providers: [ + { + provide: FACTORY_OPTIONS, + useValue: { + factories: [UserFactory], + }, + }, + FactoryService, + ], + }).compile(); + + await noSeederModule.init(); + const noSeederService = noSeederModule.get(FactoryService); + + await expect(noSeederService.seed()).resolves.toBeUndefined(); + + await noSeederModule.close(); + }); + }); + + describe("seeders without order property", () => { + it("should default order to 0", async () => { + const executionOrder: string[] = []; + + class NoOrderSeederA implements Seeder { + async run() { + executionOrder.push("A"); + } + } + + class NoOrderSeederB implements Seeder { + async run() { + executionOrder.push("B"); + } + } + + const orderedModule = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot({ + type: "better-sqlite3", + database: ":memory:", + entities: [User], + synchronize: true, + }), + ], + providers: [ + { + provide: FACTORY_OPTIONS, + useValue: { + factories: [UserFactory], + seeders: [NoOrderSeederA, NoOrderSeederB], + }, + }, + FactoryService, + ], + }).compile(); + + await orderedModule.init(); + const orderedService = orderedModule.get(FactoryService); + + await orderedService.seed(); + expect(executionOrder).toEqual(["A", "B"]); + + await orderedModule.close(); + }); + }); + describe("seeder creates expected data", () => { it("should create users with expected fields", async () => { await service.runSeeder(DatabaseSeeder);