Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align="center">
<h1 align="center">@nestbolt/factory</h1>
<p align="center">Laravel-like model factories and database seeders for NestJS with TypeORM.</p>
<p align="center">Model factories and database seeders for NestJS with TypeORM.</p>
</p>

<p align="center">
Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
49 changes: 47 additions & 2 deletions test/factory-builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,7 +27,7 @@ describe("FactoryBuilder", () => {
{
provide: FACTORY_OPTIONS,
useValue: {
factories: [UserFactory, PostFactory, CommentFactory],
factories: [UserFactory, PostFactory, CommentFactory, UserFactoryWithHooks],
},
},
FactoryService,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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>(FactoryService);

await expect(noDbService.use(UserFactory).create()).rejects.toThrow(
"DataSource is not available",
);

await noDbModule.close();
});
});
});
60 changes: 59 additions & 1 deletion test/factory.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>(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();
});
});
});
24 changes: 24 additions & 0 deletions test/helpers/test-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,30 @@ export class CommentFactory extends BaseFactory<Comment> {
}
}

export class UserFactoryWithHooks extends BaseFactory<User> {
get entity() {
return User;
}

definition(faker: Faker): Partial<User> {
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<void> {
entity.name = `[made] ${entity.name}`;
}

async afterCreate(entity: User): Promise<void> {
entity.name = `[created] ${entity.name}`;
}
}

export class DatabaseSeeder implements Seeder {
order = 0;

Expand Down
78 changes: 78 additions & 0 deletions test/seeder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(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>(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);
Expand Down
Loading