@venizia/ignis-docs 0.0.4-1 → 0.0.4-2

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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/wiki/best-practices/api-usage-examples.md +1 -0
  3. package/wiki/best-practices/code-style-standards/advanced-patterns.md +259 -0
  4. package/wiki/best-practices/code-style-standards/constants-configuration.md +225 -0
  5. package/wiki/best-practices/code-style-standards/control-flow.md +245 -0
  6. package/wiki/best-practices/code-style-standards/documentation.md +221 -0
  7. package/wiki/best-practices/code-style-standards/function-patterns.md +142 -0
  8. package/wiki/best-practices/code-style-standards/index.md +110 -0
  9. package/wiki/best-practices/code-style-standards/naming-conventions.md +174 -0
  10. package/wiki/best-practices/code-style-standards/route-definitions.md +150 -0
  11. package/wiki/best-practices/code-style-standards/tooling.md +155 -0
  12. package/wiki/best-practices/code-style-standards/type-safety.md +165 -0
  13. package/wiki/best-practices/common-pitfalls.md +164 -3
  14. package/wiki/best-practices/contribution-workflow.md +1 -1
  15. package/wiki/best-practices/data-modeling.md +102 -2
  16. package/wiki/best-practices/error-handling.md +468 -0
  17. package/wiki/best-practices/index.md +204 -21
  18. package/wiki/best-practices/performance-optimization.md +180 -0
  19. package/wiki/best-practices/security-guidelines.md +249 -0
  20. package/wiki/best-practices/testing-strategies.md +620 -0
  21. package/wiki/changelogs/2026-01-05-range-queries-content-range.md +184 -0
  22. package/wiki/changelogs/2026-01-06-basic-authentication.md +103 -0
  23. package/wiki/changelogs/2026-01-07-controller-route-customization.md +209 -0
  24. package/wiki/changelogs/index.md +3 -0
  25. package/wiki/guides/core-concepts/components-guide.md +1 -1
  26. package/wiki/guides/core-concepts/persistent/models.md +10 -0
  27. package/wiki/guides/tutorials/complete-installation.md +1 -1
  28. package/wiki/guides/tutorials/testing.md +1 -1
  29. package/wiki/references/base/components.md +47 -29
  30. package/wiki/references/base/controllers.md +215 -18
  31. package/wiki/references/base/filter-system/fields-order-pagination.md +84 -0
  32. package/wiki/references/base/middlewares.md +33 -1
  33. package/wiki/references/base/models.md +40 -2
  34. package/wiki/references/base/repositories/index.md +2 -0
  35. package/wiki/references/components/authentication.md +261 -247
  36. package/wiki/references/helpers/index.md +1 -1
  37. package/wiki/references/src-details/core.md +1 -1
  38. package/wiki/best-practices/code-style-standards.md +0 -1193
@@ -0,0 +1,620 @@
1
+ # Testing Strategies
2
+
3
+ Comprehensive testing guide for Ignis applications using Bun's built-in test runner.
4
+
5
+ ## Testing Philosophy
6
+
7
+ | Principle | Description |
8
+ |-----------|-------------|
9
+ | **Test Behavior** | Test what the code does, not how it does it |
10
+ | **Isolation** | Each test should be independent |
11
+ | **Fast Feedback** | Tests should run quickly |
12
+ | **Meaningful Coverage** | Cover critical paths and edge cases |
13
+
14
+ ## 1. Project Setup
15
+
16
+ ### Configure Test Environment
17
+
18
+ **`bunfig.toml`:**
19
+ ```toml
20
+ [test]
21
+ preload = ["./test/setup.ts"]
22
+ coverage = true
23
+ coverageDir = "coverage"
24
+ ```
25
+
26
+ **`test/setup.ts`:**
27
+ ```typescript
28
+ import { beforeAll, afterAll, afterEach } from 'bun:test';
29
+ import { TestDatabase } from './helpers/test-database';
30
+
31
+ // Global setup
32
+ beforeAll(async () => {
33
+ await TestDatabase.initialize();
34
+ });
35
+
36
+ // Clean up after each test
37
+ afterEach(async () => {
38
+ await TestDatabase.truncateAll();
39
+ });
40
+
41
+ // Global teardown
42
+ afterAll(async () => {
43
+ await TestDatabase.close();
44
+ });
45
+ ```
46
+
47
+ **`test/helpers/test-database.ts`:**
48
+ ```typescript
49
+ import { drizzle } from 'drizzle-orm/node-postgres';
50
+ import { Pool } from 'pg';
51
+ import * as schema from '@/models';
52
+
53
+ export class TestDatabase {
54
+ private static pool: Pool;
55
+ private static db: ReturnType<typeof drizzle>;
56
+
57
+ static async initialize() {
58
+ this.pool = new Pool({
59
+ host: process.env.TEST_DB_HOST ?? 'localhost',
60
+ port: Number(process.env.TEST_DB_PORT ?? 5433),
61
+ user: process.env.TEST_DB_USER ?? 'test',
62
+ password: process.env.TEST_DB_PASSWORD ?? 'test',
63
+ database: process.env.TEST_DB_NAME ?? 'ignis_test',
64
+ });
65
+ this.db = drizzle({ client: this.pool, schema });
66
+ }
67
+
68
+ static getDb() {
69
+ return this.db;
70
+ }
71
+
72
+ static async truncateAll() {
73
+ const tables = Object.keys(schema);
74
+ for (const table of tables) {
75
+ await this.db.execute(sql`TRUNCATE TABLE ${sql.identifier(table)} CASCADE`);
76
+ }
77
+ }
78
+
79
+ static async close() {
80
+ await this.pool.end();
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### Running Tests
86
+
87
+ ```bash
88
+ # Run all tests
89
+ bun test
90
+
91
+ # Run specific test file
92
+ bun test src/__tests__/user.service.test.ts
93
+
94
+ # Run tests matching pattern
95
+ bun test --grep "UserService"
96
+
97
+ # Watch mode
98
+ bun test --watch
99
+
100
+ # With coverage
101
+ bun test --coverage
102
+
103
+ # With specific environment
104
+ NODE_ENV=test bun test --env-file=.env.test
105
+ ```
106
+
107
+ ## 2. Unit Testing Services
108
+
109
+ Test business logic in isolation by mocking dependencies.
110
+
111
+ **`src/services/__tests__/user.service.test.ts`:**
112
+ ```typescript
113
+ import { describe, it, expect, beforeEach, mock } from 'bun:test';
114
+ import { UserService } from '../user.service';
115
+ import type { IUserRepository } from '@/repositories';
116
+
117
+ describe('UserService', () => {
118
+ let service: UserService;
119
+ let mockRepo: IUserRepository;
120
+
121
+ beforeEach(() => {
122
+ // Create mock repository
123
+ mockRepo = {
124
+ findById: mock(() => Promise.resolve({ data: null })),
125
+ findOne: mock(() => Promise.resolve({ data: null })),
126
+ create: mock((opts) => Promise.resolve({ data: { id: 'new-id', ...opts.data }, count: 1 })),
127
+ updateById: mock(() => Promise.resolve({ data: null, count: 0 })),
128
+ } as unknown as IUserRepository;
129
+
130
+ // Inject mock
131
+ service = new UserService(mockRepo);
132
+ });
133
+
134
+ describe('createUser', () => {
135
+ it('should create a user with valid data', async () => {
136
+ const userData = { email: 'test@example.com', name: 'Test User' };
137
+
138
+ const result = await service.createUser(userData);
139
+
140
+ expect(result.data).toMatchObject({
141
+ id: 'new-id',
142
+ email: 'test@example.com',
143
+ name: 'Test User',
144
+ });
145
+ expect(mockRepo.create).toHaveBeenCalledTimes(1);
146
+ });
147
+
148
+ it('should throw error for duplicate email', async () => {
149
+ mockRepo.findOne = mock(() => Promise.resolve({
150
+ data: { id: 'existing', email: 'test@example.com' },
151
+ }));
152
+
153
+ await expect(
154
+ service.createUser({ email: 'test@example.com', name: 'Test' })
155
+ ).rejects.toThrow('Email already exists');
156
+ });
157
+
158
+ it('should hash password before storing', async () => {
159
+ const userData = { email: 'test@example.com', name: 'Test', password: 'secret123' };
160
+
161
+ await service.createUser(userData);
162
+
163
+ const createCall = (mockRepo.create as ReturnType<typeof mock>).mock.calls[0][0];
164
+ expect(createCall.data.password).not.toBe('secret123');
165
+ expect(createCall.data.password).toMatch(/^\$2[aby]?\$/); // bcrypt hash
166
+ });
167
+ });
168
+
169
+ describe('updateUser', () => {
170
+ it('should throw NotFound when user does not exist', async () => {
171
+ mockRepo.findById = mock(() => Promise.resolve({ data: null }));
172
+
173
+ await expect(
174
+ service.updateUser('nonexistent', { name: 'New Name' })
175
+ ).rejects.toThrow('User not found');
176
+ });
177
+
178
+ it('should only update provided fields', async () => {
179
+ mockRepo.findById = mock(() => Promise.resolve({
180
+ data: { id: '1', email: 'old@test.com', name: 'Old Name' },
181
+ }));
182
+ mockRepo.updateById = mock((opts) => Promise.resolve({
183
+ data: { ...opts.data, id: opts.id },
184
+ count: 1,
185
+ }));
186
+
187
+ await service.updateUser('1', { name: 'New Name' });
188
+
189
+ const updateCall = (mockRepo.updateById as ReturnType<typeof mock>).mock.calls[0][0];
190
+ expect(updateCall.data).toEqual({ name: 'New Name' });
191
+ expect(updateCall.data.email).toBeUndefined();
192
+ });
193
+ });
194
+ });
195
+ ```
196
+
197
+ ## 3. Integration Testing Repositories
198
+
199
+ Test repositories with a real (test) database.
200
+
201
+ **`src/repositories/__tests__/user.repository.test.ts`:**
202
+ ```typescript
203
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
204
+ import { UserRepository } from '../user.repository';
205
+ import { TestDatabase } from '@test/helpers/test-database';
206
+ import { User } from '@/models';
207
+
208
+ describe('UserRepository', () => {
209
+ let repo: UserRepository;
210
+
211
+ beforeEach(async () => {
212
+ const db = TestDatabase.getDb();
213
+ repo = new UserRepository(db);
214
+ });
215
+
216
+ afterEach(async () => {
217
+ await TestDatabase.truncateAll();
218
+ });
219
+
220
+ describe('create', () => {
221
+ it('should create a user and return with generated id', async () => {
222
+ const result = await repo.create({
223
+ data: { email: 'test@example.com', name: 'Test User' },
224
+ });
225
+
226
+ expect(result.data).toMatchObject({
227
+ email: 'test@example.com',
228
+ name: 'Test User',
229
+ });
230
+ expect(result.data?.id).toBeDefined();
231
+ expect(result.count).toBe(1);
232
+ });
233
+
234
+ it('should enforce unique email constraint', async () => {
235
+ await repo.create({
236
+ data: { email: 'test@example.com', name: 'First' },
237
+ });
238
+
239
+ await expect(
240
+ repo.create({ data: { email: 'test@example.com', name: 'Second' } })
241
+ ).rejects.toThrow(); // Unique constraint violation
242
+ });
243
+ });
244
+
245
+ describe('find', () => {
246
+ beforeEach(async () => {
247
+ // Seed test data
248
+ await repo.createMany({
249
+ data: [
250
+ { email: 'alice@test.com', name: 'Alice', status: 'ACTIVE' },
251
+ { email: 'bob@test.com', name: 'Bob', status: 'ACTIVE' },
252
+ { email: 'charlie@test.com', name: 'Charlie', status: 'INACTIVE' },
253
+ ],
254
+ });
255
+ });
256
+
257
+ it('should filter by status', async () => {
258
+ const result = await repo.find({
259
+ filter: { where: { status: 'ACTIVE' } },
260
+ });
261
+
262
+ expect(result.data).toHaveLength(2);
263
+ expect(result.data.map(u => u.name)).toContain('Alice');
264
+ expect(result.data.map(u => u.name)).toContain('Bob');
265
+ });
266
+
267
+ it('should support pagination', async () => {
268
+ const page1 = await repo.find({
269
+ filter: { limit: 2, offset: 0, order: ['name ASC'] },
270
+ });
271
+ const page2 = await repo.find({
272
+ filter: { limit: 2, offset: 2, order: ['name ASC'] },
273
+ });
274
+
275
+ expect(page1.data).toHaveLength(2);
276
+ expect(page1.data[0].name).toBe('Alice');
277
+ expect(page2.data).toHaveLength(1);
278
+ expect(page2.data[0].name).toBe('Charlie');
279
+ });
280
+
281
+ it('should support complex filters', async () => {
282
+ const result = await repo.find({
283
+ filter: {
284
+ where: {
285
+ or: [
286
+ { name: { like: 'A%' } },
287
+ { status: 'INACTIVE' },
288
+ ],
289
+ },
290
+ },
291
+ });
292
+
293
+ expect(result.data).toHaveLength(2);
294
+ expect(result.data.map(u => u.name)).toContain('Alice');
295
+ expect(result.data.map(u => u.name)).toContain('Charlie');
296
+ });
297
+ });
298
+
299
+ describe('relations', () => {
300
+ it('should load related entities', async () => {
301
+ // Assuming User has Posts relation
302
+ const user = await repo.create({
303
+ data: { email: 'author@test.com', name: 'Author' },
304
+ });
305
+
306
+ // Create posts for the user
307
+ const postRepo = new PostRepository(TestDatabase.getDb());
308
+ await postRepo.createMany({
309
+ data: [
310
+ { title: 'Post 1', authorId: user.data!.id },
311
+ { title: 'Post 2', authorId: user.data!.id },
312
+ ],
313
+ });
314
+
315
+ const result = await repo.findById({
316
+ id: user.data!.id,
317
+ filter: { include: [{ relation: 'posts' }] },
318
+ });
319
+
320
+ expect(result.data?.posts).toHaveLength(2);
321
+ });
322
+ });
323
+ });
324
+ ```
325
+
326
+ ## 4. E2E Testing Controllers
327
+
328
+ Test full request/response cycle using Hono's test client.
329
+
330
+ **`src/controllers/__tests__/user.controller.test.ts`:**
331
+ ```typescript
332
+ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'bun:test';
333
+ import { testClient } from 'hono/testing';
334
+ import { Application } from '@/application';
335
+ import { TestDatabase } from '@test/helpers/test-database';
336
+
337
+ describe('UserController E2E', () => {
338
+ let app: Application;
339
+ let client: ReturnType<typeof testClient>;
340
+
341
+ beforeAll(async () => {
342
+ await TestDatabase.initialize();
343
+ app = new Application();
344
+ await app.boot();
345
+ client = testClient(app.server);
346
+ });
347
+
348
+ afterEach(async () => {
349
+ await TestDatabase.truncateAll();
350
+ });
351
+
352
+ afterAll(async () => {
353
+ await TestDatabase.close();
354
+ });
355
+
356
+ describe('POST /api/users', () => {
357
+ it('should create a user with valid data', async () => {
358
+ const response = await client.api.users.$post({
359
+ json: {
360
+ email: 'test@example.com',
361
+ name: 'Test User',
362
+ password: 'SecurePass123!',
363
+ },
364
+ });
365
+
366
+ expect(response.status).toBe(201);
367
+ const body = await response.json();
368
+ expect(body.email).toBe('test@example.com');
369
+ expect(body.id).toBeDefined();
370
+ expect(body.password).toBeUndefined(); // Should not expose password
371
+ });
372
+
373
+ it('should return 422 for invalid email', async () => {
374
+ const response = await client.api.users.$post({
375
+ json: {
376
+ email: 'not-an-email',
377
+ name: 'Test',
378
+ password: 'SecurePass123!',
379
+ },
380
+ });
381
+
382
+ expect(response.status).toBe(422);
383
+ const body = await response.json();
384
+ expect(body.details.cause[0].path).toBe('email');
385
+ });
386
+
387
+ it('should return 409 for duplicate email', async () => {
388
+ // First create
389
+ await client.api.users.$post({
390
+ json: { email: 'test@example.com', name: 'First', password: 'Pass123!' },
391
+ });
392
+
393
+ // Duplicate
394
+ const response = await client.api.users.$post({
395
+ json: { email: 'test@example.com', name: 'Second', password: 'Pass123!' },
396
+ });
397
+
398
+ expect(response.status).toBe(409);
399
+ });
400
+ });
401
+
402
+ describe('GET /api/users/:id', () => {
403
+ it('should return user by id', async () => {
404
+ // Create user first
405
+ const createRes = await client.api.users.$post({
406
+ json: { email: 'test@example.com', name: 'Test', password: 'Pass123!' },
407
+ });
408
+ const created = await createRes.json();
409
+
410
+ const response = await client.api.users[':id'].$get({
411
+ param: { id: created.id },
412
+ });
413
+
414
+ expect(response.status).toBe(200);
415
+ const body = await response.json();
416
+ expect(body.id).toBe(created.id);
417
+ expect(body.email).toBe('test@example.com');
418
+ });
419
+
420
+ it('should return 404 for nonexistent user', async () => {
421
+ const response = await client.api.users[':id'].$get({
422
+ param: { id: 'nonexistent-uuid' },
423
+ });
424
+
425
+ expect(response.status).toBe(404);
426
+ });
427
+ });
428
+
429
+ describe('Protected Routes', () => {
430
+ let authToken: string;
431
+
432
+ beforeEach(async () => {
433
+ // Create user and get token
434
+ await client.api.users.$post({
435
+ json: { email: 'auth@test.com', name: 'Auth User', password: 'Pass123!' },
436
+ });
437
+ const loginRes = await client.api.auth.login.$post({
438
+ json: { email: 'auth@test.com', password: 'Pass123!' },
439
+ });
440
+ const { token } = await loginRes.json();
441
+ authToken = token;
442
+ });
443
+
444
+ it('should return 401 without token', async () => {
445
+ const response = await client.api.users.me.$get();
446
+ expect(response.status).toBe(401);
447
+ });
448
+
449
+ it('should return user profile with valid token', async () => {
450
+ const response = await client.api.users.me.$get({
451
+ headers: { Authorization: `Bearer ${authToken}` },
452
+ });
453
+
454
+ expect(response.status).toBe(200);
455
+ const body = await response.json();
456
+ expect(body.email).toBe('auth@test.com');
457
+ });
458
+ });
459
+ });
460
+ ```
461
+
462
+ ## 5. Mocking Patterns
463
+
464
+ ### Mock External Services
465
+
466
+ ```typescript
467
+ import { mock } from 'bun:test';
468
+
469
+ // Mock email service
470
+ const mockEmailService = {
471
+ send: mock(() => Promise.resolve({ messageId: 'mock-id' })),
472
+ sendBulk: mock(() => Promise.resolve({ sent: 10, failed: 0 })),
473
+ };
474
+
475
+ // Mock Redis
476
+ const mockRedis = {
477
+ get: mock((key: string) => Promise.resolve(null)),
478
+ set: mock(() => Promise.resolve('OK')),
479
+ del: mock(() => Promise.resolve(1)),
480
+ };
481
+
482
+ // Reset mocks between tests
483
+ beforeEach(() => {
484
+ mockEmailService.send.mockClear();
485
+ mockRedis.get.mockClear();
486
+ });
487
+ ```
488
+
489
+ ### Mock Time
490
+
491
+ ```typescript
492
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
493
+
494
+ describe('Time-dependent tests', () => {
495
+ const realDate = Date;
496
+
497
+ beforeEach(() => {
498
+ // Mock Date to fixed time
499
+ const mockDate = new Date('2024-01-15T10:00:00Z');
500
+ global.Date = class extends realDate {
501
+ constructor() {
502
+ super();
503
+ return mockDate;
504
+ }
505
+ static now() {
506
+ return mockDate.getTime();
507
+ }
508
+ } as DateConstructor;
509
+ });
510
+
511
+ afterEach(() => {
512
+ global.Date = realDate;
513
+ });
514
+
515
+ it('should use mocked time', () => {
516
+ const now = new Date();
517
+ expect(now.toISOString()).toBe('2024-01-15T10:00:00.000Z');
518
+ });
519
+ });
520
+ ```
521
+
522
+ ### Spy on Methods
523
+
524
+ ```typescript
525
+ import { spyOn } from 'bun:test';
526
+
527
+ it('should call logger on error', async () => {
528
+ const loggerSpy = spyOn(service.logger, 'error');
529
+
530
+ await expect(service.riskyOperation()).rejects.toThrow();
531
+
532
+ expect(loggerSpy).toHaveBeenCalledWith(
533
+ expect.stringContaining('Operation failed'),
534
+ expect.any(Error)
535
+ );
536
+ });
537
+ ```
538
+
539
+ ## 6. Test Organization
540
+
541
+ ### File Structure
542
+
543
+ ```
544
+ src/
545
+ ├── services/
546
+ │ ├── user.service.ts
547
+ │ └── __tests__/
548
+ │ └── user.service.test.ts
549
+ ├── repositories/
550
+ │ ├── user.repository.ts
551
+ │ └── __tests__/
552
+ │ └── user.repository.test.ts
553
+ └── controllers/
554
+ ├── user.controller.ts
555
+ └── __tests__/
556
+ └── user.controller.test.ts
557
+ test/
558
+ ├── setup.ts
559
+ ├── helpers/
560
+ │ ├── test-database.ts
561
+ │ └── fixtures.ts
562
+ └── e2e/
563
+ └── full-flow.test.ts
564
+ ```
565
+
566
+ ### Test Naming Convention
567
+
568
+ ```typescript
569
+ describe('UserService', () => {
570
+ describe('createUser', () => {
571
+ it('should create user with valid data', () => {});
572
+ it('should throw ValidationError for invalid email', () => {});
573
+ it('should throw ConflictError for duplicate email', () => {});
574
+ });
575
+
576
+ describe('updateUser', () => {
577
+ describe('when user exists', () => {
578
+ it('should update provided fields only', () => {});
579
+ });
580
+
581
+ describe('when user does not exist', () => {
582
+ it('should throw NotFoundError', () => {});
583
+ });
584
+ });
585
+ });
586
+ ```
587
+
588
+ ## 7. Coverage Guidelines
589
+
590
+ | Category | Target | Priority |
591
+ |----------|--------|----------|
592
+ | Services (business logic) | > 80% | High |
593
+ | Repositories (data access) | > 70% | Medium |
594
+ | Controllers (E2E) | > 60% | Medium |
595
+ | Utilities/Helpers | > 90% | High |
596
+
597
+ **Generate coverage report:**
598
+ ```bash
599
+ bun test --coverage
600
+
601
+ # Coverage summary will be in ./coverage/
602
+ ```
603
+
604
+ ## Testing Checklist
605
+
606
+ | Category | Check |
607
+ |----------|-------|
608
+ | **Setup** | Test database configured and isolated |
609
+ | **Unit** | Services tested with mocked dependencies |
610
+ | **Integration** | Repositories tested with real database |
611
+ | **E2E** | Critical user flows covered |
612
+ | **Edge Cases** | Error conditions and boundaries tested |
613
+ | **Security** | Auth/authz scenarios tested |
614
+ | **Performance** | Slow tests identified and optimized |
615
+
616
+ ## See Also
617
+
618
+ - [Error Handling](./error-handling) - Test error scenarios
619
+ - [Common Pitfalls](./common-pitfalls) - Testing mistakes to avoid
620
+ - [API Usage Examples](./api-usage-examples) - What to test