@navios/core 0.3.0 → 0.5.0
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/README.md +96 -3
- package/docs/README.md +310 -3
- package/docs/adapters.md +308 -0
- package/docs/application-setup.md +524 -0
- package/docs/attributes.md +689 -0
- package/docs/controllers.md +373 -0
- package/docs/endpoints.md +444 -0
- package/docs/exceptions.md +316 -0
- package/docs/guards.md +550 -0
- package/docs/modules.md +251 -0
- package/docs/quick-start.md +295 -0
- package/docs/services.md +428 -0
- package/docs/testing.md +704 -0
- package/lib/_tsup-dts-rollup.d.mts +313 -280
- package/lib/_tsup-dts-rollup.d.ts +313 -280
- package/lib/index.d.mts +47 -26
- package/lib/index.d.ts +47 -26
- package/lib/index.js +633 -1068
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +632 -1061
- package/lib/index.mjs.map +1 -1
- package/package.json +11 -12
- package/project.json +17 -4
- package/src/__tests__/config.service.spec.mts +11 -9
- package/src/__tests__/controller.spec.mts +1 -2
- package/src/attribute.factory.mts +1 -1
- package/src/config/config.provider.mts +2 -2
- package/src/config/config.service.mts +4 -4
- package/src/decorators/controller.decorator.mts +1 -1
- package/src/decorators/endpoint.decorator.mts +9 -10
- package/src/decorators/header.decorator.mts +1 -1
- package/src/decorators/multipart.decorator.mts +5 -5
- package/src/decorators/stream.decorator.mts +5 -6
- package/src/factories/endpoint-adapter.factory.mts +21 -0
- package/src/factories/http-adapter.factory.mts +20 -0
- package/src/factories/index.mts +6 -0
- package/src/factories/multipart-adapter.factory.mts +21 -0
- package/src/factories/reply.factory.mts +21 -0
- package/src/factories/request.factory.mts +21 -0
- package/src/factories/stream-adapter.factory.mts +20 -0
- package/src/index.mts +1 -1
- package/src/interfaces/abstract-execution-context.inteface.mts +13 -0
- package/src/interfaces/abstract-http-adapter.interface.mts +20 -0
- package/src/interfaces/abstract-http-cors-options.interface.mts +59 -0
- package/src/interfaces/abstract-http-handler-adapter.interface.mts +13 -0
- package/src/interfaces/abstract-http-listen-options.interface.mts +4 -0
- package/src/interfaces/can-activate.mts +4 -2
- package/src/interfaces/http-header.mts +18 -0
- package/src/interfaces/index.mts +6 -0
- package/src/logger/console-logger.service.mts +28 -44
- package/src/logger/index.mts +1 -2
- package/src/logger/logger.service.mts +9 -128
- package/src/logger/logger.tokens.mts +21 -0
- package/src/metadata/handler.metadata.mts +7 -5
- package/src/navios.application.mts +65 -172
- package/src/navios.environment.mts +30 -0
- package/src/navios.factory.mts +53 -12
- package/src/services/guard-runner.service.mts +19 -9
- package/src/services/index.mts +0 -2
- package/src/services/module-loader.service.mts +4 -3
- package/src/tokens/endpoint-adapter.token.mts +8 -0
- package/src/tokens/execution-context.token.mts +2 -2
- package/src/tokens/http-adapter.token.mts +8 -0
- package/src/tokens/index.mts +4 -1
- package/src/tokens/multipart-adapter.token.mts +8 -0
- package/src/tokens/reply.token.mts +1 -5
- package/src/tokens/request.token.mts +1 -7
- package/src/tokens/stream-adapter.token.mts +8 -0
- package/tsconfig.json +6 -1
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +12 -0
- package/tsup.config.mts +1 -0
- package/docs/recipes/prisma.md +0 -60
- package/examples/simple-test/api/index.mts +0 -64
- package/examples/simple-test/config/config.service.mts +0 -14
- package/examples/simple-test/config/configuration.mts +0 -7
- package/examples/simple-test/index.mts +0 -16
- package/examples/simple-test/src/acl/acl-modern.guard.mts +0 -15
- package/examples/simple-test/src/acl/acl.guard.mts +0 -14
- package/examples/simple-test/src/acl/app.guard.mts +0 -27
- package/examples/simple-test/src/acl/one-more.guard.mts +0 -15
- package/examples/simple-test/src/acl/public.attribute.mts +0 -21
- package/examples/simple-test/src/app.module.mts +0 -9
- package/examples/simple-test/src/user/user.controller.mts +0 -72
- package/examples/simple-test/src/user/user.module.mts +0 -14
- package/examples/simple-test/src/user/user.service.mts +0 -14
- package/src/adapters/endpoint-adapter.service.mts +0 -72
- package/src/adapters/handler-adapter.interface.mts +0 -21
- package/src/adapters/index.mts +0 -4
- package/src/adapters/multipart-adapter.service.mts +0 -131
- package/src/adapters/stream-adapter.service.mts +0 -91
- package/src/logger/logger.factory.mts +0 -36
- package/src/logger/pino-wrapper.mts +0 -64
- package/src/services/controller-adapter.service.mts +0 -124
- package/src/services/execution-context.mts +0 -54
- package/src/tokens/application.token.mts +0 -9
package/docs/testing.md
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
> This guide is for future reference and may be updated over time
|
|
4
|
+
>
|
|
5
|
+
> Not represent the final state of the testing strategy for Navios
|
|
6
|
+
|
|
7
|
+
This guide covers testing strategies and best practices for Navios applications, including unit testing, integration testing, and end-to-end testing.
|
|
8
|
+
|
|
9
|
+
## Testing Philosophy
|
|
10
|
+
|
|
11
|
+
Navios promotes a testing-first approach with:
|
|
12
|
+
|
|
13
|
+
- **Unit Tests** - Test individual components in isolation
|
|
14
|
+
- **Integration Tests** - Test component interactions
|
|
15
|
+
- **End-to-End Tests** - Test complete user workflows
|
|
16
|
+
|
|
17
|
+
## Setting Up Testing
|
|
18
|
+
|
|
19
|
+
### Test Dependencies
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install --save-dev vitest supertest @navios/builder
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Unit Testing
|
|
26
|
+
|
|
27
|
+
### Testing Services
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { TestContainer } from '@navios/core/testing'
|
|
31
|
+
|
|
32
|
+
import { DatabaseService } from './database.service.js'
|
|
33
|
+
import { UserService } from './user.service.js'
|
|
34
|
+
|
|
35
|
+
describe('UserService', () => {
|
|
36
|
+
let userService: UserService
|
|
37
|
+
let mockDatabase: vi.Mocked<DatabaseService>
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
const container = new TestContainer()
|
|
41
|
+
|
|
42
|
+
// Create mock database
|
|
43
|
+
mockDatabase = {
|
|
44
|
+
users: {
|
|
45
|
+
findUnique: vi.fn(),
|
|
46
|
+
findMany: vi.fn(),
|
|
47
|
+
create: vi.fn(),
|
|
48
|
+
update: vi.fn(),
|
|
49
|
+
delete: vi.fn(),
|
|
50
|
+
},
|
|
51
|
+
} as any
|
|
52
|
+
|
|
53
|
+
container.bind(DatabaseService).toValue(mockDatabase)
|
|
54
|
+
userService = container.get(UserService)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('findById', () => {
|
|
58
|
+
it('should return user when found', async () => {
|
|
59
|
+
const mockUser = { id: '1', name: 'John', email: 'john@test.com' }
|
|
60
|
+
mockDatabase.users.findUnique.mockResolvedValue(mockUser)
|
|
61
|
+
|
|
62
|
+
const result = await userService.findById('1')
|
|
63
|
+
|
|
64
|
+
expect(result).toEqual(mockUser)
|
|
65
|
+
expect(mockDatabase.users.findUnique).toHaveBeenCalledWith({
|
|
66
|
+
where: { id: '1' },
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should return null when user not found', async () => {
|
|
71
|
+
mockDatabase.users.findUnique.mockResolvedValue(null)
|
|
72
|
+
|
|
73
|
+
const result = await userService.findById('1')
|
|
74
|
+
|
|
75
|
+
expect(result).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('create', () => {
|
|
80
|
+
it('should create and return new user', async () => {
|
|
81
|
+
const userData = { name: 'John', email: 'john@test.com' }
|
|
82
|
+
const createdUser = { id: '1', ...userData }
|
|
83
|
+
|
|
84
|
+
mockDatabase.users.create.mockResolvedValue(createdUser)
|
|
85
|
+
|
|
86
|
+
const result = await userService.create(userData)
|
|
87
|
+
|
|
88
|
+
expect(result).toEqual(createdUser)
|
|
89
|
+
expect(mockDatabase.users.create).toHaveBeenCalledWith({
|
|
90
|
+
data: userData,
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Testing Controllers
|
|
98
|
+
|
|
99
|
+
Controllers in Navios should use endpoints defined with `@navios/builder` for proper type safety and schema validation.
|
|
100
|
+
|
|
101
|
+
#### Endpoint Definitions
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// api/user.endpoints.ts
|
|
105
|
+
import { builder } from '@navios/builder'
|
|
106
|
+
|
|
107
|
+
import { z } from 'zod'
|
|
108
|
+
|
|
109
|
+
const userApi = builder()
|
|
110
|
+
|
|
111
|
+
export const getUserByIdEndpoint = userApi.declareEndpoint({
|
|
112
|
+
method: 'GET',
|
|
113
|
+
url: '/users/$id',
|
|
114
|
+
responseSchema: z.object({
|
|
115
|
+
id: z.string(),
|
|
116
|
+
name: z.string(),
|
|
117
|
+
email: z.string().email(),
|
|
118
|
+
}),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export const createUserEndpoint = userApi.declareEndpoint({
|
|
122
|
+
method: 'POST',
|
|
123
|
+
url: '/users',
|
|
124
|
+
requestSchema: z.object({
|
|
125
|
+
name: z.string().min(1),
|
|
126
|
+
email: z.string().email(),
|
|
127
|
+
}),
|
|
128
|
+
responseSchema: z.object({
|
|
129
|
+
id: z.string(),
|
|
130
|
+
name: z.string(),
|
|
131
|
+
email: z.string().email(),
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Controller Implementation
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// user.controller.ts
|
|
140
|
+
import { Controller, Endpoint, EndpointParams } from '@navios/core'
|
|
141
|
+
import { inject } from '@navios/di'
|
|
142
|
+
|
|
143
|
+
import { createUserEndpoint, getUserByIdEndpoint } from '../api/user.endpoints'
|
|
144
|
+
import { UserService } from './user.service'
|
|
145
|
+
|
|
146
|
+
@Controller()
|
|
147
|
+
export class UserController {
|
|
148
|
+
private userService = inject(UserService)
|
|
149
|
+
|
|
150
|
+
@Endpoint(getUserByIdEndpoint)
|
|
151
|
+
async getUserById(params: EndpointParams<typeof getUserByIdEndpoint>) {
|
|
152
|
+
const user = await this.userService.findById(params.params.id)
|
|
153
|
+
if (!user) {
|
|
154
|
+
throw new NotFoundException('User not found')
|
|
155
|
+
}
|
|
156
|
+
return user
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Endpoint(createUserEndpoint)
|
|
160
|
+
async createUser(params: EndpointParams<typeof createUserEndpoint>) {
|
|
161
|
+
return this.userService.create(params.data)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Testing Controllers
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import type { EndpointParams } from '@navios/core'
|
|
170
|
+
|
|
171
|
+
import { NotFoundException } from '@navios/core'
|
|
172
|
+
import { TestContainer } from '@navios/core/testing'
|
|
173
|
+
|
|
174
|
+
import { UserController } from './user.controller'
|
|
175
|
+
import { UserService } from './user.service'
|
|
176
|
+
|
|
177
|
+
describe('UserController', () => {
|
|
178
|
+
let container: TestContainer
|
|
179
|
+
let controller: UserController
|
|
180
|
+
let userService: vi.Mocked<UserService>
|
|
181
|
+
|
|
182
|
+
beforeEach(async () => {
|
|
183
|
+
userService = {
|
|
184
|
+
findById: vi.fn(),
|
|
185
|
+
findAll: vi.fn(),
|
|
186
|
+
create: vi.fn(),
|
|
187
|
+
update: vi.fn(),
|
|
188
|
+
delete: vi.fn(),
|
|
189
|
+
} as any
|
|
190
|
+
|
|
191
|
+
container = new TestContainer()
|
|
192
|
+
// Use dependency injection or manual injection
|
|
193
|
+
controller = await container.get(UserController)
|
|
194
|
+
// Inject mocked service (implementation depends on your DI setup)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('getUserById', () => {
|
|
198
|
+
it('should return user when found', async () => {
|
|
199
|
+
const mockUser = { id: '1', name: 'John', email: 'john@test.com' }
|
|
200
|
+
userService.findById.mockResolvedValue(mockUser)
|
|
201
|
+
|
|
202
|
+
const result = await controller.getUserById({
|
|
203
|
+
params: { id: '1' },
|
|
204
|
+
query: {},
|
|
205
|
+
data: {},
|
|
206
|
+
headers: {},
|
|
207
|
+
request: {} as any,
|
|
208
|
+
response: {} as any,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
expect(result).toEqual(mockUser)
|
|
212
|
+
expect(userService.findById).toHaveBeenCalledWith('1')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should throw NotFoundException when user not found', async () => {
|
|
216
|
+
userService.findById.mockResolvedValue(null)
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
controller.getUserById({
|
|
220
|
+
params: { id: '1' },
|
|
221
|
+
query: {},
|
|
222
|
+
data: {},
|
|
223
|
+
headers: {},
|
|
224
|
+
request: {} as any,
|
|
225
|
+
response: {} as any,
|
|
226
|
+
}),
|
|
227
|
+
).rejects.toThrow(NotFoundException)
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('createUser', () => {
|
|
232
|
+
it('should create and return new user', async () => {
|
|
233
|
+
const userData = { name: 'John', email: 'john@test.com' }
|
|
234
|
+
const createdUser = { id: '1', ...userData }
|
|
235
|
+
|
|
236
|
+
userService.create.mockResolvedValue(createdUser)
|
|
237
|
+
|
|
238
|
+
const result = await controller.createUser({
|
|
239
|
+
params: {},
|
|
240
|
+
query: {},
|
|
241
|
+
data: userData,
|
|
242
|
+
headers: {},
|
|
243
|
+
request: {} as any,
|
|
244
|
+
response: {} as any,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
expect(result).toEqual(createdUser)
|
|
248
|
+
expect(userService.create).toHaveBeenCalledWith(userData)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Testing Guards
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { TestContainer } from '@navios/core/testing'
|
|
258
|
+
|
|
259
|
+
import { AuthGuard } from './auth.guard'
|
|
260
|
+
import { JwtService } from './jwt.service'
|
|
261
|
+
import { UserService } from './user.service'
|
|
262
|
+
|
|
263
|
+
describe('AuthGuard', () => {
|
|
264
|
+
let guard: AuthGuard
|
|
265
|
+
let jwtService: vi.Mocked<JwtService>
|
|
266
|
+
let userService: vi.Mocked<UserService>
|
|
267
|
+
|
|
268
|
+
beforeEach(() => {
|
|
269
|
+
const container = new TestContainer()
|
|
270
|
+
|
|
271
|
+
jwtService = {
|
|
272
|
+
verify: vi.fn(),
|
|
273
|
+
} as any
|
|
274
|
+
|
|
275
|
+
userService = {
|
|
276
|
+
findById: vi.fn(),
|
|
277
|
+
} as any
|
|
278
|
+
|
|
279
|
+
container.bind(JwtService).toValue(jwtService)
|
|
280
|
+
container.bind(UserService).toValue(userService)
|
|
281
|
+
|
|
282
|
+
guard = container.get(AuthGuard)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe('canActivate', () => {
|
|
286
|
+
it('should return true for valid token', async () => {
|
|
287
|
+
const mockUser = { id: '1', isActive: true }
|
|
288
|
+
const context = {
|
|
289
|
+
headers: { authorization: 'Bearer valid-token' },
|
|
290
|
+
request: {},
|
|
291
|
+
} as any
|
|
292
|
+
|
|
293
|
+
jwtService.verify.mockResolvedValue({ sub: '1' })
|
|
294
|
+
userService.findById.mockResolvedValue(mockUser)
|
|
295
|
+
|
|
296
|
+
const result = await guard.canActivate(context)
|
|
297
|
+
|
|
298
|
+
expect(result).toBe(true)
|
|
299
|
+
expect(context.request.user).toEqual(mockUser)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should return false for invalid token', async () => {
|
|
303
|
+
const context = {
|
|
304
|
+
headers: { authorization: 'Bearer invalid-token' },
|
|
305
|
+
request: {},
|
|
306
|
+
} as any
|
|
307
|
+
|
|
308
|
+
jwtService.verify.mockRejectedValue(new Error('Invalid token'))
|
|
309
|
+
|
|
310
|
+
const result = await guard.canActivate(context)
|
|
311
|
+
|
|
312
|
+
expect(result).toBe(false)
|
|
313
|
+
expect(context.request.user).toBeUndefined()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should return false when no token provided', async () => {
|
|
317
|
+
const context = {
|
|
318
|
+
headers: {},
|
|
319
|
+
request: {},
|
|
320
|
+
} as any
|
|
321
|
+
|
|
322
|
+
const result = await guard.canActivate(context)
|
|
323
|
+
|
|
324
|
+
expect(result).toBe(false)
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Integration Testing
|
|
331
|
+
|
|
332
|
+
### Testing Module Integration
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { NaviosFactory } from '@navios/core'
|
|
336
|
+
|
|
337
|
+
import { DatabaseService } from './database.service'
|
|
338
|
+
import { UserModule } from './user.module'
|
|
339
|
+
import { UserService } from './user.service'
|
|
340
|
+
|
|
341
|
+
describe('UserModule', () => {
|
|
342
|
+
let module: TestingModule
|
|
343
|
+
let userService: UserService
|
|
344
|
+
let databaseService: DatabaseService
|
|
345
|
+
|
|
346
|
+
beforeAll(async () => {
|
|
347
|
+
module = await NaviosFactory.create(UserModule, {
|
|
348
|
+
adapter: defineFastifyEnvironment(),
|
|
349
|
+
})
|
|
350
|
+
await module.init()
|
|
351
|
+
|
|
352
|
+
userService = module.get<UserService>(UserService)
|
|
353
|
+
databaseService = module.get<DatabaseService>(DatabaseService)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
afterAll(async () => {
|
|
357
|
+
await module.close()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should be defined', () => {
|
|
361
|
+
expect(userService).toBeDefined()
|
|
362
|
+
expect(databaseService).toBeDefined()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('should inject dependencies correctly', () => {
|
|
366
|
+
expect(userService).toBeInstanceOf(UserService)
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Testing HTTP Endpoints
|
|
372
|
+
|
|
373
|
+
When testing HTTP endpoints, ensure your endpoints are defined using `@navios/builder` for proper validation and type safety.
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { defineFastifyEnvironment } from '@navios/adapter-fastify'
|
|
377
|
+
import { NaviosFactory } from '@navios/core'
|
|
378
|
+
|
|
379
|
+
import * as request from 'supertest'
|
|
380
|
+
|
|
381
|
+
import { AppModule } from '../app.module'
|
|
382
|
+
|
|
383
|
+
describe('UserController (e2e)', () => {
|
|
384
|
+
let app: any
|
|
385
|
+
let httpServer: any
|
|
386
|
+
|
|
387
|
+
beforeAll(async () => {
|
|
388
|
+
const moduleFixture = await NaviosFactory.create(AppModule, {
|
|
389
|
+
adapter: defineFastifyEnvironment(),
|
|
390
|
+
})
|
|
391
|
+
await moduleFixture.init()
|
|
392
|
+
|
|
393
|
+
await app.init()
|
|
394
|
+
httpServer = app.getServer()
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
afterAll(async () => {
|
|
398
|
+
await app.close()
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
describe('/users (GET)', () => {
|
|
402
|
+
it('should return array of users', () => {
|
|
403
|
+
return request(httpServer)
|
|
404
|
+
.get('/users')
|
|
405
|
+
.expect(200)
|
|
406
|
+
.expect((res) => {
|
|
407
|
+
expect(Array.isArray(res.body)).toBe(true)
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
describe('/users/:id (GET)', () => {
|
|
413
|
+
it('should return user by id', () => {
|
|
414
|
+
return request(httpServer)
|
|
415
|
+
.get('/users/1')
|
|
416
|
+
.expect(200)
|
|
417
|
+
.expect((res) => {
|
|
418
|
+
expect(res.body).toHaveProperty('id')
|
|
419
|
+
expect(res.body.id).toBe('1')
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('should return 404 for non-existent user', () => {
|
|
424
|
+
return request(httpServer).get('/users/999').expect(404)
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
describe('/users (POST)', () => {
|
|
429
|
+
it('should create new user', () => {
|
|
430
|
+
const userData = {
|
|
431
|
+
name: 'John Doe',
|
|
432
|
+
email: 'john@test.com',
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return request(httpServer)
|
|
436
|
+
.post('/users')
|
|
437
|
+
.send(userData)
|
|
438
|
+
.expect(201)
|
|
439
|
+
.expect((res) => {
|
|
440
|
+
expect(res.body).toHaveProperty('id')
|
|
441
|
+
expect(res.body.name).toBe(userData.name)
|
|
442
|
+
expect(res.body.email).toBe(userData.email)
|
|
443
|
+
})
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('should return 400 for invalid data', () => {
|
|
447
|
+
return request(httpServer)
|
|
448
|
+
.post('/users')
|
|
449
|
+
.send({ name: '' }) // Invalid data
|
|
450
|
+
.expect(400)
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
describe('/upload (POST) - Multipart', () => {
|
|
456
|
+
it('should handle file uploads', () => {
|
|
457
|
+
return request(httpServer)
|
|
458
|
+
.post('/upload')
|
|
459
|
+
.attach('file', Buffer.from('test file content'), 'test.txt')
|
|
460
|
+
.field('description', 'Test file upload')
|
|
461
|
+
.expect(201)
|
|
462
|
+
.expect((res) => {
|
|
463
|
+
expect(res.body).toHaveProperty('filename')
|
|
464
|
+
expect(res.body).toHaveProperty('size')
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Testing Multipart Endpoints
|
|
472
|
+
|
|
473
|
+
When testing multipart endpoints defined with `@navios/builder`:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
// api/upload.endpoints.ts
|
|
477
|
+
import { builder } from '@navios/builder'
|
|
478
|
+
// upload.controller.ts
|
|
479
|
+
import { Controller, Multipart, MultipartParams } from '@navios/core'
|
|
480
|
+
|
|
481
|
+
import { z } from 'zod'
|
|
482
|
+
|
|
483
|
+
const api = builder()
|
|
484
|
+
|
|
485
|
+
export const uploadEndpoint = api.declareMultipart({
|
|
486
|
+
method: 'POST',
|
|
487
|
+
url: '/upload',
|
|
488
|
+
requestSchema: z.object({
|
|
489
|
+
files: z.array(z.instanceof(File)),
|
|
490
|
+
description: z.string(),
|
|
491
|
+
}),
|
|
492
|
+
responseSchema: z.object({
|
|
493
|
+
filename: z.string(),
|
|
494
|
+
size: z.number(),
|
|
495
|
+
}),
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
@Controller()
|
|
499
|
+
export class UploadController {
|
|
500
|
+
@Multipart(uploadEndpoint)
|
|
501
|
+
async uploadFile(params: MultipartParams<typeof uploadEndpoint>) {
|
|
502
|
+
const { files, description } = params.data
|
|
503
|
+
// Handle file upload logic
|
|
504
|
+
return {
|
|
505
|
+
filename: files[0].name,
|
|
506
|
+
size: files[0].size,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Testing with Authentication
|
|
513
|
+
|
|
514
|
+
### Testing Protected Endpoints
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
describe('Protected Endpoints', () => {
|
|
518
|
+
let authToken: string
|
|
519
|
+
|
|
520
|
+
beforeAll(async () => {
|
|
521
|
+
// Login and get auth token
|
|
522
|
+
const loginResponse = await request(httpServer)
|
|
523
|
+
.post('/auth/login')
|
|
524
|
+
.send({
|
|
525
|
+
email: 'test@example.com',
|
|
526
|
+
password: 'password',
|
|
527
|
+
})
|
|
528
|
+
.expect(200)
|
|
529
|
+
|
|
530
|
+
authToken = loginResponse.body.token
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
describe('/profile (GET)', () => {
|
|
534
|
+
it('should return user profile with valid token', () => {
|
|
535
|
+
return request(httpServer)
|
|
536
|
+
.get('/profile')
|
|
537
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
538
|
+
.expect(200)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('should return 401 without token', () => {
|
|
542
|
+
return request(httpServer).get('/profile').expect(401)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('should return 401 with invalid token', () => {
|
|
546
|
+
return request(httpServer)
|
|
547
|
+
.get('/profile')
|
|
548
|
+
.set('Authorization', `Bearer invalid-token`)
|
|
549
|
+
.expect(401)
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## Mock Strategies
|
|
556
|
+
|
|
557
|
+
### Creating Service Mocks
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// test/mocks/user.service.mock.ts
|
|
561
|
+
export const mockUserService = {
|
|
562
|
+
findById: vi.fn(),
|
|
563
|
+
findAll: vi.fn(),
|
|
564
|
+
create: vi.fn(),
|
|
565
|
+
update: vi.fn(),
|
|
566
|
+
delete: vi.fn(),
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Reset mocks between tests
|
|
570
|
+
beforeEach(() => {
|
|
571
|
+
vi.clearAllMocks()
|
|
572
|
+
})
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Factory Pattern for Test Data
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
// test/factories/user.factory.ts
|
|
579
|
+
export class UserFactory {
|
|
580
|
+
static create(overrides: Partial<User> = {}): User {
|
|
581
|
+
return {
|
|
582
|
+
id: '1',
|
|
583
|
+
name: 'John Doe',
|
|
584
|
+
email: 'john@test.com',
|
|
585
|
+
createdAt: new Date(),
|
|
586
|
+
updatedAt: new Date(),
|
|
587
|
+
...overrides,
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
static createMany(count: number, overrides: Partial<User> = {}): User[] {
|
|
592
|
+
return Array.from({ length: count }, (_, i) =>
|
|
593
|
+
this.create({
|
|
594
|
+
id: String(i + 1),
|
|
595
|
+
...overrides,
|
|
596
|
+
}),
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Usage in tests
|
|
602
|
+
describe('UserService', () => {
|
|
603
|
+
it('should handle multiple users', async () => {
|
|
604
|
+
const users = UserFactory.createMany(3)
|
|
605
|
+
mockDatabase.users.findMany.mockResolvedValue(users)
|
|
606
|
+
|
|
607
|
+
const result = await userService.findAll()
|
|
608
|
+
|
|
609
|
+
expect(result).toHaveLength(3)
|
|
610
|
+
expect(result).toEqual(users)
|
|
611
|
+
})
|
|
612
|
+
})
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
## Test Organization
|
|
616
|
+
|
|
617
|
+
### Folder Structure
|
|
618
|
+
|
|
619
|
+
```
|
|
620
|
+
src/
|
|
621
|
+
├── user/
|
|
622
|
+
│ ├── user.controller.ts
|
|
623
|
+
│ ├── user.service.ts
|
|
624
|
+
│ ├── user.module.ts
|
|
625
|
+
│ └── __tests__/
|
|
626
|
+
│ ├── user.controller.spec.ts
|
|
627
|
+
│ ├── user.service.spec.ts
|
|
628
|
+
└── test/
|
|
629
|
+
├── fixtures/
|
|
630
|
+
├── mocks/
|
|
631
|
+
├── factories/
|
|
632
|
+
└── e2e/
|
|
633
|
+
└── user.e2e-spec.ts
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Best Practices
|
|
637
|
+
|
|
638
|
+
### 1. Test Isolation
|
|
639
|
+
|
|
640
|
+
Ensure tests don't depend on each other:
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
describe('UserService', () => {
|
|
644
|
+
beforeEach(() => {
|
|
645
|
+
// Reset state before each test
|
|
646
|
+
vi.clearAllMocks()
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// Each test should be independent
|
|
650
|
+
})
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 2. Use Descriptive Test Names
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
// ✅ Good - Descriptive names
|
|
657
|
+
it('should throw NotFoundException when user does not exist', () => {})
|
|
658
|
+
it('should return user data when valid ID is provided', () => {})
|
|
659
|
+
|
|
660
|
+
// ❌ Avoid - Vague names
|
|
661
|
+
it('should work', () => {})
|
|
662
|
+
it('should fail', () => {})
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### 3. Test Edge Cases
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
describe('UserService.findById', () => {
|
|
669
|
+
it('should return user for valid ID', async () => {
|
|
670
|
+
// Happy path
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
it('should return null for non-existent ID', async () => {
|
|
674
|
+
// Edge case
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
it('should handle database connection errors', async () => {
|
|
678
|
+
// Error case
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
it('should validate ID format', async () => {
|
|
682
|
+
// Input validation
|
|
683
|
+
})
|
|
684
|
+
})
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### 4. Keep Tests Simple
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
// ✅ Good - Simple and focused
|
|
691
|
+
it('should create user with valid data', async () => {
|
|
692
|
+
const userData = { name: 'John', email: 'john@test.com' }
|
|
693
|
+
mockDatabase.users.create.mockResolvedValue({ id: '1', ...userData })
|
|
694
|
+
|
|
695
|
+
const result = await userService.create(userData)
|
|
696
|
+
|
|
697
|
+
expect(result).toMatchObject(userData)
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
// ❌ Avoid - Testing multiple things
|
|
701
|
+
it('should create user and send email and log event', async () => {
|
|
702
|
+
// Too many responsibilities
|
|
703
|
+
})
|
|
704
|
+
```
|