@cinnabun/testing 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/mock-providers.test.ts +88 -0
- package/__tests__/test-app.test.ts +135 -0
- package/__tests__/testing-module.test.ts +154 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/interfaces/testing-module.d.ts +13 -0
- package/dist/interfaces/testing-module.js +1 -0
- package/dist/interfaces/testing-options.d.ts +13 -0
- package/dist/interfaces/testing-options.js +1 -0
- package/dist/mock-providers.d.ts +3 -0
- package/dist/mock-providers.js +13 -0
- package/dist/test-app.d.ts +3 -0
- package/dist/test-app.js +83 -0
- package/dist/testing-module.d.ts +4 -0
- package/dist/testing-module.js +50 -0
- package/package.json +23 -0
- package/src/index.ts +8 -0
- package/src/interfaces/testing-module.ts +23 -0
- package/src/interfaces/testing-options.ts +15 -0
- package/src/mock-providers.ts +19 -0
- package/src/test-app.ts +108 -0
- package/src/testing-module.ts +65 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect, afterEach } from "bun:test";
|
|
3
|
+
import { Service } from "@cinnabun/core";
|
|
4
|
+
import { mockProviders } from "../src/mock-providers.js";
|
|
5
|
+
import { createTestingModule } from "../src/testing-module.js";
|
|
6
|
+
|
|
7
|
+
afterEach(() => {});
|
|
8
|
+
|
|
9
|
+
describe("mockProviders", () => {
|
|
10
|
+
it("returns ProviderOverride array for each provider", () => {
|
|
11
|
+
@Service()
|
|
12
|
+
class UserService {
|
|
13
|
+
findAll() {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const overrides = mockProviders(UserService);
|
|
19
|
+
expect(overrides).toHaveLength(1);
|
|
20
|
+
expect(overrides[0].provide).toBe(UserService);
|
|
21
|
+
expect(overrides[0].useValue).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("stubbed methods return undefined by default", async () => {
|
|
25
|
+
@Service()
|
|
26
|
+
class UserService {
|
|
27
|
+
findAll() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
findById(_id: string) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const overrides = mockProviders(UserService);
|
|
37
|
+
const mock = overrides[0].useValue as Record<string, () => undefined>;
|
|
38
|
+
|
|
39
|
+
expect(mock.findAll()).toBeUndefined();
|
|
40
|
+
expect(mock.findById("1")).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("works with createTestingModule overrides", async () => {
|
|
44
|
+
@Service()
|
|
45
|
+
class DatabaseService {
|
|
46
|
+
query() {
|
|
47
|
+
return ["real"];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Service()
|
|
52
|
+
class UserService {
|
|
53
|
+
constructor(public db: DatabaseService) {}
|
|
54
|
+
|
|
55
|
+
findAll() {
|
|
56
|
+
return this.db.query();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const module = await createTestingModule({
|
|
61
|
+
providers: [UserService, DatabaseService],
|
|
62
|
+
overrides: mockProviders(DatabaseService),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const userService = module.resolve(UserService);
|
|
66
|
+
expect(userService.db).toBeDefined();
|
|
67
|
+
expect(userService.db.query()).toBeUndefined();
|
|
68
|
+
|
|
69
|
+
await module.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("handles multiple providers", () => {
|
|
73
|
+
@Service()
|
|
74
|
+
class ServiceA {
|
|
75
|
+
a() {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Service()
|
|
79
|
+
class ServiceB {
|
|
80
|
+
b() {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const overrides = mockProviders(ServiceA, ServiceB);
|
|
84
|
+
expect(overrides).toHaveLength(2);
|
|
85
|
+
expect(overrides[0].provide).toBe(ServiceA);
|
|
86
|
+
expect(overrides[1].provide).toBe(ServiceB);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect, afterEach } from "bun:test";
|
|
3
|
+
import {
|
|
4
|
+
RestController,
|
|
5
|
+
GetMapping,
|
|
6
|
+
PostMapping,
|
|
7
|
+
PutMapping,
|
|
8
|
+
DeleteMapping,
|
|
9
|
+
HttpCode,
|
|
10
|
+
} from "@cinnabun/core";
|
|
11
|
+
import { createTestApp } from "../src/test-app.js";
|
|
12
|
+
|
|
13
|
+
let app: Awaited<ReturnType<typeof createTestApp>> | null = null;
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
if (app) {
|
|
17
|
+
await app.close();
|
|
18
|
+
app = null;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("createTestApp", () => {
|
|
23
|
+
it("serves HTTP requests", async () => {
|
|
24
|
+
@RestController("/api/items")
|
|
25
|
+
class ItemController {
|
|
26
|
+
@GetMapping("/")
|
|
27
|
+
findAll() {
|
|
28
|
+
return [{ id: 1, name: "Widget" }];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
app = await createTestApp({
|
|
33
|
+
controllers: [ItemController],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const res = await app.get("/api/items");
|
|
37
|
+
expect(res.status).toBe(200);
|
|
38
|
+
|
|
39
|
+
const body = await res.json();
|
|
40
|
+
expect(body).toEqual([{ id: 1, name: "Widget" }]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("get() prepends base URL", async () => {
|
|
44
|
+
@RestController("/health")
|
|
45
|
+
class HealthController {
|
|
46
|
+
@GetMapping("/")
|
|
47
|
+
check() {
|
|
48
|
+
return { ok: true };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
app = await createTestApp({
|
|
53
|
+
controllers: [HealthController],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const res = await app.get("/health");
|
|
57
|
+
expect(res.status).toBe(200);
|
|
58
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("post() sends JSON body", async () => {
|
|
62
|
+
@RestController("/api/users")
|
|
63
|
+
class UserController {
|
|
64
|
+
@PostMapping("/")
|
|
65
|
+
@HttpCode(201)
|
|
66
|
+
create(_body: { name: string }) {
|
|
67
|
+
return { created: true };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
app = await createTestApp({
|
|
72
|
+
controllers: [UserController],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const res = await app.post("/api/users", { name: "Bob" });
|
|
76
|
+
expect(res.status).toBe(201);
|
|
77
|
+
expect(await res.json()).toEqual({ created: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("put() sends JSON body", async () => {
|
|
81
|
+
@RestController("/api/users")
|
|
82
|
+
class UserController {
|
|
83
|
+
@PutMapping("/:id")
|
|
84
|
+
update(_body: { name: string }) {
|
|
85
|
+
return { updated: true };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
app = await createTestApp({
|
|
90
|
+
controllers: [UserController],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const res = await app.put("/api/users/1", { name: "Alice" });
|
|
94
|
+
expect(res.status).toBe(200);
|
|
95
|
+
expect(await res.json()).toEqual({ updated: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("delete() works", async () => {
|
|
99
|
+
@RestController("/api/users")
|
|
100
|
+
class UserController {
|
|
101
|
+
@DeleteMapping("/:id")
|
|
102
|
+
@HttpCode(204)
|
|
103
|
+
remove() {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
app = await createTestApp({
|
|
109
|
+
controllers: [UserController],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const res = await app.delete("/api/users/1");
|
|
113
|
+
expect(res.status).toBe(204);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("close() shuts down server without error", async () => {
|
|
117
|
+
@RestController("/")
|
|
118
|
+
class RootController {
|
|
119
|
+
@GetMapping("/")
|
|
120
|
+
index() {
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
app = await createTestApp({
|
|
126
|
+
controllers: [RootController],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const resBefore = await app.get("/");
|
|
130
|
+
expect(resBefore.status).toBe(200);
|
|
131
|
+
|
|
132
|
+
await expect(app.close()).resolves.toBeUndefined();
|
|
133
|
+
app = null;
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect, afterEach } from "bun:test";
|
|
3
|
+
import { Service, PostConstruct, PreDestroy } from "@cinnabun/core";
|
|
4
|
+
import { createTestingModule } from "../src/testing-module.js";
|
|
5
|
+
|
|
6
|
+
afterEach(() => {});
|
|
7
|
+
|
|
8
|
+
describe("createTestingModule", () => {
|
|
9
|
+
it("resolves providers correctly", async () => {
|
|
10
|
+
@Service()
|
|
11
|
+
class UserService {
|
|
12
|
+
findAll() {
|
|
13
|
+
return [{ id: 1, name: "Alice" }];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const module = await createTestingModule({
|
|
18
|
+
providers: [UserService],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const userService = module.resolve(UserService);
|
|
22
|
+
expect(userService).toBeInstanceOf(UserService);
|
|
23
|
+
expect(userService.findAll()).toHaveLength(1);
|
|
24
|
+
expect(userService.findAll()[0].name).toBe("Alice");
|
|
25
|
+
|
|
26
|
+
await module.close();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("replaces real implementation with useValue override", async () => {
|
|
30
|
+
@Service()
|
|
31
|
+
class DatabaseService {
|
|
32
|
+
query() {
|
|
33
|
+
return [{ id: 1, name: "real" }];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Service()
|
|
38
|
+
class UserService {
|
|
39
|
+
constructor(public db: DatabaseService) {}
|
|
40
|
+
|
|
41
|
+
findAll() {
|
|
42
|
+
return this.db.query();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mockDb = {
|
|
47
|
+
query: () => [{ id: 1, name: "mocked" }],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const module = await createTestingModule({
|
|
51
|
+
providers: [UserService, DatabaseService],
|
|
52
|
+
overrides: [
|
|
53
|
+
{ provide: DatabaseService, useValue: mockDb },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const userService = module.resolve(UserService);
|
|
58
|
+
expect(userService.findAll()).toEqual([{ id: 1, name: "mocked" }]);
|
|
59
|
+
|
|
60
|
+
await module.close();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("replaces real implementation with useClass override", async () => {
|
|
64
|
+
@Service()
|
|
65
|
+
class DatabaseService {
|
|
66
|
+
query() {
|
|
67
|
+
return ["real"];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Service()
|
|
72
|
+
class MockDatabaseService {
|
|
73
|
+
query() {
|
|
74
|
+
return ["mocked"];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Service()
|
|
79
|
+
class UserService {
|
|
80
|
+
constructor(public db: DatabaseService) {}
|
|
81
|
+
|
|
82
|
+
findAll() {
|
|
83
|
+
return this.db.query();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const module = await createTestingModule({
|
|
88
|
+
providers: [UserService, DatabaseService],
|
|
89
|
+
overrides: [
|
|
90
|
+
{ provide: DatabaseService, useClass: MockDatabaseService },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const userService = module.resolve(UserService);
|
|
95
|
+
expect(userService.findAll()).toEqual(["mocked"]);
|
|
96
|
+
|
|
97
|
+
await module.close();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("replaces real implementation with useFactory override", async () => {
|
|
101
|
+
@Service()
|
|
102
|
+
class DatabaseService {
|
|
103
|
+
query() {
|
|
104
|
+
return ["real"];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Service()
|
|
109
|
+
class UserService {
|
|
110
|
+
constructor(public db: DatabaseService) {}
|
|
111
|
+
|
|
112
|
+
findAll() {
|
|
113
|
+
return this.db.query();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const module = await createTestingModule({
|
|
118
|
+
providers: [UserService, DatabaseService],
|
|
119
|
+
overrides: [
|
|
120
|
+
{
|
|
121
|
+
provide: DatabaseService,
|
|
122
|
+
useFactory: () => ({ query: () => ["factory"] }),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const userService = module.resolve(UserService);
|
|
128
|
+
expect(userService.findAll()).toEqual(["factory"]);
|
|
129
|
+
|
|
130
|
+
await module.close();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("close() calls @PreDestroy hooks", async () => {
|
|
134
|
+
let preDestroyCalled = false;
|
|
135
|
+
|
|
136
|
+
@Service()
|
|
137
|
+
class TestService {
|
|
138
|
+
@PreDestroy()
|
|
139
|
+
cleanup() {
|
|
140
|
+
preDestroyCalled = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const module = await createTestingModule({
|
|
145
|
+
providers: [TestService],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
module.resolve(TestService);
|
|
149
|
+
expect(preDestroyCalled).toBe(false);
|
|
150
|
+
|
|
151
|
+
await module.close();
|
|
152
|
+
expect(preDestroyCalled).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createTestingModule } from "./testing-module.js";
|
|
2
|
+
export { createTestApp } from "./test-app.js";
|
|
3
|
+
export { mockProviders } from "./mock-providers.js";
|
|
4
|
+
export type { TestingModuleOptions, ProviderOverride, } from "./interfaces/testing-options.js";
|
|
5
|
+
export type { TestingModule, TestApp } from "./interfaces/testing-module.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Container, Constructor } from "@cinnabun/core";
|
|
2
|
+
export interface TestingModule {
|
|
3
|
+
container: Container;
|
|
4
|
+
resolve<T>(target: Constructor<T>): T;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface TestApp extends TestingModule {
|
|
8
|
+
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
9
|
+
get(path: string, headers?: Record<string, string>): Promise<Response>;
|
|
10
|
+
post(path: string, body?: unknown, headers?: Record<string, string>): Promise<Response>;
|
|
11
|
+
put(path: string, body?: unknown, headers?: Record<string, string>): Promise<Response>;
|
|
12
|
+
delete(path: string, headers?: Record<string, string>): Promise<Response>;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Constructor } from "@cinnabun/core";
|
|
2
|
+
export interface ProviderOverride {
|
|
3
|
+
provide: Constructor;
|
|
4
|
+
useValue?: unknown;
|
|
5
|
+
useClass?: Constructor;
|
|
6
|
+
useFactory?: () => unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface TestingModuleOptions {
|
|
9
|
+
controllers?: Constructor[];
|
|
10
|
+
providers?: Constructor[];
|
|
11
|
+
imports?: Constructor[];
|
|
12
|
+
overrides?: ProviderOverride[];
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test-app.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { CinnabunApplication } from "@cinnabun/core";
|
|
3
|
+
function applyOverrides(container, overrides) {
|
|
4
|
+
for (const override of overrides) {
|
|
5
|
+
let value;
|
|
6
|
+
if (override.useValue !== undefined) {
|
|
7
|
+
value = override.useValue;
|
|
8
|
+
}
|
|
9
|
+
else if (override.useClass) {
|
|
10
|
+
value = container.resolve(override.useClass);
|
|
11
|
+
}
|
|
12
|
+
else if (override.useFactory) {
|
|
13
|
+
value = override.useFactory();
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
throw new Error(`ProviderOverride for ${override.provide.name} must specify useValue, useClass, or useFactory`);
|
|
17
|
+
}
|
|
18
|
+
container.registerInstance(override.provide, value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function normalizePath(path) {
|
|
22
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
23
|
+
}
|
|
24
|
+
export async function createTestApp(options) {
|
|
25
|
+
const controllers = options.controllers ?? [];
|
|
26
|
+
const providers = options.providers ?? [];
|
|
27
|
+
const app = await CinnabunApplication.create({
|
|
28
|
+
controllers,
|
|
29
|
+
providers,
|
|
30
|
+
preRegister: (container) => {
|
|
31
|
+
if (options.overrides?.length) {
|
|
32
|
+
applyOverrides(container, options.overrides);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
await app.listen(0);
|
|
37
|
+
const port = app.getPort();
|
|
38
|
+
const baseUrl = `http://localhost:${port}`;
|
|
39
|
+
const testApp = {
|
|
40
|
+
container: app.getContainer(),
|
|
41
|
+
resolve: (target) => app.getContainer().resolve(target),
|
|
42
|
+
async close() {
|
|
43
|
+
await app.close();
|
|
44
|
+
},
|
|
45
|
+
async fetch(path, init) {
|
|
46
|
+
const url = `${baseUrl}${normalizePath(path)}`;
|
|
47
|
+
return fetch(url, init);
|
|
48
|
+
},
|
|
49
|
+
async get(path, headers) {
|
|
50
|
+
return testApp.fetch(normalizePath(path), {
|
|
51
|
+
method: "GET",
|
|
52
|
+
headers,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
async post(path, body, headers) {
|
|
56
|
+
return testApp.fetch(normalizePath(path), {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
...headers,
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(body ?? {}),
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
async put(path, body, headers) {
|
|
66
|
+
return testApp.fetch(normalizePath(path), {
|
|
67
|
+
method: "PUT",
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
...headers,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(body ?? {}),
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
async delete(path, headers) {
|
|
76
|
+
return testApp.fetch(normalizePath(path), {
|
|
77
|
+
method: "DELETE",
|
|
78
|
+
headers,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return testApp;
|
|
83
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import type { TestingModuleOptions } from "./interfaces/testing-options.js";
|
|
3
|
+
import type { TestingModule } from "./interfaces/testing-module.js";
|
|
4
|
+
export declare function createTestingModule(options: TestingModuleOptions): Promise<TestingModule>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { Container, resolveModuleTree } from "@cinnabun/core";
|
|
3
|
+
function applyOverrides(container, overrides) {
|
|
4
|
+
for (const override of overrides) {
|
|
5
|
+
let value;
|
|
6
|
+
if (override.useValue !== undefined) {
|
|
7
|
+
value = override.useValue;
|
|
8
|
+
}
|
|
9
|
+
else if (override.useClass) {
|
|
10
|
+
value = container.resolve(override.useClass);
|
|
11
|
+
}
|
|
12
|
+
else if (override.useFactory) {
|
|
13
|
+
value = override.useFactory();
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
throw new Error(`ProviderOverride for ${override.provide.name} must specify useValue, useClass, or useFactory`);
|
|
17
|
+
}
|
|
18
|
+
container.registerInstance(override.provide, value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function createTestingModule(options) {
|
|
22
|
+
const container = new Container();
|
|
23
|
+
let controllers = options.controllers ?? [];
|
|
24
|
+
let providers = options.providers ?? [];
|
|
25
|
+
if (options.imports?.length) {
|
|
26
|
+
for (const mod of options.imports) {
|
|
27
|
+
const resolved = await resolveModuleTree(mod);
|
|
28
|
+
controllers = [...controllers, ...resolved.controllers];
|
|
29
|
+
providers = [...providers, ...resolved.providers];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (options.overrides?.length) {
|
|
33
|
+
applyOverrides(container, options.overrides);
|
|
34
|
+
}
|
|
35
|
+
for (const provider of providers) {
|
|
36
|
+
container.resolve(provider);
|
|
37
|
+
}
|
|
38
|
+
for (const controller of controllers) {
|
|
39
|
+
container.resolve(controller);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
container,
|
|
43
|
+
resolve(target) {
|
|
44
|
+
return container.resolve(target);
|
|
45
|
+
},
|
|
46
|
+
async close() {
|
|
47
|
+
await container.shutdown();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cinnabun/testing",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "bun test",
|
|
12
|
+
"build": "tsc"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@cinnabun/core": "^0.0.3"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@cinnabun/core": "workspace:*",
|
|
19
|
+
"@types/bun": "latest",
|
|
20
|
+
"reflect-metadata": "^0.2.2",
|
|
21
|
+
"typescript": "^5.9.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createTestingModule } from "./testing-module.js";
|
|
2
|
+
export { createTestApp } from "./test-app.js";
|
|
3
|
+
export { mockProviders } from "./mock-providers.js";
|
|
4
|
+
export type {
|
|
5
|
+
TestingModuleOptions,
|
|
6
|
+
ProviderOverride,
|
|
7
|
+
} from "./interfaces/testing-options.js";
|
|
8
|
+
export type { TestingModule, TestApp } from "./interfaces/testing-module.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Container, Constructor } from "@cinnabun/core";
|
|
2
|
+
|
|
3
|
+
export interface TestingModule {
|
|
4
|
+
container: Container;
|
|
5
|
+
resolve<T>(target: Constructor<T>): T;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TestApp extends TestingModule {
|
|
10
|
+
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
11
|
+
get(path: string, headers?: Record<string, string>): Promise<Response>;
|
|
12
|
+
post(
|
|
13
|
+
path: string,
|
|
14
|
+
body?: unknown,
|
|
15
|
+
headers?: Record<string, string>,
|
|
16
|
+
): Promise<Response>;
|
|
17
|
+
put(
|
|
18
|
+
path: string,
|
|
19
|
+
body?: unknown,
|
|
20
|
+
headers?: Record<string, string>,
|
|
21
|
+
): Promise<Response>;
|
|
22
|
+
delete(path: string, headers?: Record<string, string>): Promise<Response>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Constructor } from "@cinnabun/core";
|
|
2
|
+
|
|
3
|
+
export interface ProviderOverride {
|
|
4
|
+
provide: Constructor;
|
|
5
|
+
useValue?: unknown;
|
|
6
|
+
useClass?: Constructor;
|
|
7
|
+
useFactory?: () => unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TestingModuleOptions {
|
|
11
|
+
controllers?: Constructor[];
|
|
12
|
+
providers?: Constructor[];
|
|
13
|
+
imports?: Constructor[];
|
|
14
|
+
overrides?: ProviderOverride[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Constructor } from "@cinnabun/core";
|
|
2
|
+
import type { ProviderOverride } from "./interfaces/testing-options.js";
|
|
3
|
+
|
|
4
|
+
function createMock(): Record<string, () => undefined> {
|
|
5
|
+
return new Proxy({} as Record<string, () => undefined>, {
|
|
6
|
+
get(_, prop: string) {
|
|
7
|
+
return () => undefined;
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function mockProviders(
|
|
13
|
+
...providers: Constructor[]
|
|
14
|
+
): ProviderOverride[] {
|
|
15
|
+
return providers.map((provide) => ({
|
|
16
|
+
provide,
|
|
17
|
+
useValue: createMock(),
|
|
18
|
+
}));
|
|
19
|
+
}
|
package/src/test-app.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { CinnabunApplication } from "@cinnabun/core";
|
|
3
|
+
import type { Container } from "@cinnabun/core";
|
|
4
|
+
import type { ProviderOverride } from "./interfaces/testing-options.js";
|
|
5
|
+
import type { TestApp } from "./interfaces/testing-module.js";
|
|
6
|
+
|
|
7
|
+
function applyOverrides(container: Container, overrides: ProviderOverride[]): void {
|
|
8
|
+
for (const override of overrides) {
|
|
9
|
+
let value: unknown;
|
|
10
|
+
if (override.useValue !== undefined) {
|
|
11
|
+
value = override.useValue;
|
|
12
|
+
} else if (override.useClass) {
|
|
13
|
+
value = container.resolve(override.useClass);
|
|
14
|
+
} else if (override.useFactory) {
|
|
15
|
+
value = override.useFactory();
|
|
16
|
+
} else {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`ProviderOverride for ${override.provide.name} must specify useValue, useClass, or useFactory`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
container.registerInstance(override.provide, value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizePath(path: string): string {
|
|
26
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function createTestApp(
|
|
30
|
+
options: import("./interfaces/testing-options.js").TestingModuleOptions,
|
|
31
|
+
): Promise<TestApp> {
|
|
32
|
+
const controllers = options.controllers ?? [];
|
|
33
|
+
const providers = options.providers ?? [];
|
|
34
|
+
|
|
35
|
+
const app = await CinnabunApplication.create({
|
|
36
|
+
controllers,
|
|
37
|
+
providers,
|
|
38
|
+
preRegister: (container) => {
|
|
39
|
+
if (options.overrides?.length) {
|
|
40
|
+
applyOverrides(container, options.overrides);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await app.listen(0);
|
|
46
|
+
const port = app.getPort();
|
|
47
|
+
const baseUrl = `http://localhost:${port}`;
|
|
48
|
+
|
|
49
|
+
const testApp: TestApp = {
|
|
50
|
+
container: app.getContainer(),
|
|
51
|
+
resolve: (target) => app.getContainer().resolve(target),
|
|
52
|
+
async close() {
|
|
53
|
+
await app.close();
|
|
54
|
+
},
|
|
55
|
+
async fetch(path: string, init?: RequestInit): Promise<Response> {
|
|
56
|
+
const url = `${baseUrl}${normalizePath(path)}`;
|
|
57
|
+
return fetch(url, init);
|
|
58
|
+
},
|
|
59
|
+
async get(
|
|
60
|
+
path: string,
|
|
61
|
+
headers?: Record<string, string>,
|
|
62
|
+
): Promise<Response> {
|
|
63
|
+
return testApp.fetch(normalizePath(path), {
|
|
64
|
+
method: "GET",
|
|
65
|
+
headers,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
async post(
|
|
69
|
+
path: string,
|
|
70
|
+
body?: unknown,
|
|
71
|
+
headers?: Record<string, string>,
|
|
72
|
+
): Promise<Response> {
|
|
73
|
+
return testApp.fetch(normalizePath(path), {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
...headers,
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify(body ?? {}),
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
async put(
|
|
83
|
+
path: string,
|
|
84
|
+
body?: unknown,
|
|
85
|
+
headers?: Record<string, string>,
|
|
86
|
+
): Promise<Response> {
|
|
87
|
+
return testApp.fetch(normalizePath(path), {
|
|
88
|
+
method: "PUT",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
...headers,
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(body ?? {}),
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
async delete(
|
|
97
|
+
path: string,
|
|
98
|
+
headers?: Record<string, string>,
|
|
99
|
+
): Promise<Response> {
|
|
100
|
+
return testApp.fetch(normalizePath(path), {
|
|
101
|
+
method: "DELETE",
|
|
102
|
+
headers,
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return testApp;
|
|
108
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { Container, resolveModuleTree } from "@cinnabun/core";
|
|
3
|
+
import type { Constructor } from "@cinnabun/core";
|
|
4
|
+
import type { TestingModuleOptions, ProviderOverride } from "./interfaces/testing-options.js";
|
|
5
|
+
import type { TestingModule } from "./interfaces/testing-module.js";
|
|
6
|
+
|
|
7
|
+
function applyOverrides(
|
|
8
|
+
container: Container,
|
|
9
|
+
overrides: ProviderOverride[],
|
|
10
|
+
): void {
|
|
11
|
+
for (const override of overrides) {
|
|
12
|
+
let value: unknown;
|
|
13
|
+
if (override.useValue !== undefined) {
|
|
14
|
+
value = override.useValue;
|
|
15
|
+
} else if (override.useClass) {
|
|
16
|
+
value = container.resolve(override.useClass);
|
|
17
|
+
} else if (override.useFactory) {
|
|
18
|
+
value = override.useFactory();
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`ProviderOverride for ${override.provide.name} must specify useValue, useClass, or useFactory`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
container.registerInstance(override.provide, value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function createTestingModule(
|
|
29
|
+
options: TestingModuleOptions,
|
|
30
|
+
): Promise<TestingModule> {
|
|
31
|
+
const container = new Container();
|
|
32
|
+
|
|
33
|
+
let controllers = options.controllers ?? [];
|
|
34
|
+
let providers = options.providers ?? [];
|
|
35
|
+
|
|
36
|
+
if (options.imports?.length) {
|
|
37
|
+
for (const mod of options.imports) {
|
|
38
|
+
const resolved = await resolveModuleTree(mod);
|
|
39
|
+
controllers = [...controllers, ...resolved.controllers];
|
|
40
|
+
providers = [...providers, ...resolved.providers];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options.overrides?.length) {
|
|
45
|
+
applyOverrides(container, options.overrides);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const provider of providers) {
|
|
49
|
+
container.resolve(provider);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const controller of controllers) {
|
|
53
|
+
container.resolve(controller);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
container,
|
|
58
|
+
resolve<T>(target: Constructor<T>): T {
|
|
59
|
+
return container.resolve(target);
|
|
60
|
+
},
|
|
61
|
+
async close(): Promise<void> {
|
|
62
|
+
await container.shutdown();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"experimentalDecorators": true,
|
|
12
|
+
"emitDecoratorMetadata": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"types": ["bun"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"],
|
|
17
|
+
"exclude": ["src/__tests__"]
|
|
18
|
+
}
|