@venizia/ignis-docs 0.0.2 → 0.0.4-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.
Files changed (134) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -2
  3. package/wiki/best-practices/api-usage-examples.md +591 -0
  4. package/wiki/best-practices/architectural-patterns.md +415 -0
  5. package/wiki/best-practices/architecture-decisions.md +488 -0
  6. package/wiki/{get-started/best-practices → best-practices}/code-style-standards.md +647 -182
  7. package/wiki/{get-started/best-practices → best-practices}/common-pitfalls.md +109 -4
  8. package/wiki/{get-started/best-practices → best-practices}/contribution-workflow.md +34 -7
  9. package/wiki/best-practices/data-modeling.md +376 -0
  10. package/wiki/best-practices/deployment-strategies.md +698 -0
  11. package/wiki/best-practices/index.md +27 -0
  12. package/wiki/best-practices/performance-optimization.md +196 -0
  13. package/wiki/best-practices/security-guidelines.md +218 -0
  14. package/wiki/{get-started/best-practices → best-practices}/troubleshooting-tips.md +97 -1
  15. package/wiki/changelogs/2025-12-16-initial-architecture.md +1 -1
  16. package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +1 -1
  17. package/wiki/changelogs/2025-12-17-refactor.md +1 -1
  18. package/wiki/changelogs/2025-12-18-performance-optimizations.md +5 -5
  19. package/wiki/changelogs/2025-12-18-repository-validation-security.md +13 -7
  20. package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +86 -0
  21. package/wiki/changelogs/2025-12-26-transaction-support.md +57 -0
  22. package/wiki/changelogs/2025-12-29-dynamic-binding-registration.md +104 -0
  23. package/wiki/changelogs/2025-12-29-snowflake-uid-helper.md +100 -0
  24. package/wiki/changelogs/2025-12-30-repository-enhancements.md +214 -0
  25. package/wiki/changelogs/2025-12-31-json-path-filtering-array-operators.md +214 -0
  26. package/wiki/changelogs/2025-12-31-string-id-custom-generator.md +137 -0
  27. package/wiki/changelogs/2026-01-02-default-filter-and-repository-mixins.md +418 -0
  28. package/wiki/changelogs/index.md +8 -1
  29. package/wiki/changelogs/planned-schema-migrator.md +2 -10
  30. package/wiki/{get-started/core-concepts → guides/core-concepts/application}/bootstrapping.md +18 -5
  31. package/wiki/{get-started/core-concepts/application.md → guides/core-concepts/application/index.md} +47 -104
  32. package/wiki/guides/core-concepts/components-guide.md +509 -0
  33. package/wiki/guides/core-concepts/components.md +122 -0
  34. package/wiki/{get-started → guides}/core-concepts/controllers.md +30 -13
  35. package/wiki/{get-started → guides}/core-concepts/dependency-injection.md +97 -0
  36. package/wiki/guides/core-concepts/persistent/datasources.md +179 -0
  37. package/wiki/guides/core-concepts/persistent/index.md +119 -0
  38. package/wiki/guides/core-concepts/persistent/models.md +241 -0
  39. package/wiki/guides/core-concepts/persistent/repositories.md +219 -0
  40. package/wiki/guides/core-concepts/persistent/transactions.md +170 -0
  41. package/wiki/{get-started → guides}/core-concepts/services.md +26 -3
  42. package/wiki/{get-started → guides/get-started}/5-minute-quickstart.md +59 -14
  43. package/wiki/guides/get-started/philosophy.md +682 -0
  44. package/wiki/guides/get-started/setup.md +157 -0
  45. package/wiki/guides/index.md +89 -0
  46. package/wiki/guides/reference/glossary.md +243 -0
  47. package/wiki/{get-started → guides/reference}/mcp-docs-server.md +0 -10
  48. package/wiki/{get-started → guides/tutorials}/building-a-crud-api.md +134 -132
  49. package/wiki/{get-started/quickstart.md → guides/tutorials/complete-installation.md} +107 -71
  50. package/wiki/guides/tutorials/ecommerce-api.md +1399 -0
  51. package/wiki/guides/tutorials/realtime-chat.md +1261 -0
  52. package/wiki/guides/tutorials/testing.md +723 -0
  53. package/wiki/index.md +176 -37
  54. package/wiki/references/base/application.md +27 -0
  55. package/wiki/references/base/bootstrapping.md +30 -26
  56. package/wiki/references/base/components.md +532 -31
  57. package/wiki/references/base/controllers.md +136 -38
  58. package/wiki/references/base/datasources.md +108 -5
  59. package/wiki/references/base/dependency-injection.md +39 -3
  60. package/wiki/references/base/filter-system/application-usage.md +224 -0
  61. package/wiki/references/base/filter-system/array-operators.md +132 -0
  62. package/wiki/references/base/filter-system/comparison-operators.md +109 -0
  63. package/wiki/references/base/filter-system/default-filter.md +428 -0
  64. package/wiki/references/base/filter-system/fields-order-pagination.md +155 -0
  65. package/wiki/references/base/filter-system/index.md +127 -0
  66. package/wiki/references/base/filter-system/json-filtering.md +197 -0
  67. package/wiki/references/base/filter-system/list-operators.md +71 -0
  68. package/wiki/references/base/filter-system/logical-operators.md +156 -0
  69. package/wiki/references/base/filter-system/null-operators.md +58 -0
  70. package/wiki/references/base/filter-system/pattern-matching.md +108 -0
  71. package/wiki/references/base/filter-system/quick-reference.md +431 -0
  72. package/wiki/references/base/filter-system/range-operators.md +63 -0
  73. package/wiki/references/base/filter-system/tips.md +190 -0
  74. package/wiki/references/base/filter-system/use-cases.md +452 -0
  75. package/wiki/references/base/index.md +90 -0
  76. package/wiki/references/base/middlewares.md +602 -0
  77. package/wiki/references/base/models.md +215 -23
  78. package/wiki/references/base/providers.md +732 -0
  79. package/wiki/references/base/repositories/advanced.md +555 -0
  80. package/wiki/references/base/repositories/index.md +228 -0
  81. package/wiki/references/base/repositories/mixins.md +331 -0
  82. package/wiki/references/base/repositories/relations.md +486 -0
  83. package/wiki/references/base/repositories.md +40 -549
  84. package/wiki/references/base/services.md +28 -4
  85. package/wiki/references/components/authentication.md +22 -2
  86. package/wiki/references/components/health-check.md +12 -0
  87. package/wiki/references/components/index.md +23 -0
  88. package/wiki/references/components/mail.md +687 -0
  89. package/wiki/references/components/request-tracker.md +16 -0
  90. package/wiki/references/components/socket-io.md +18 -0
  91. package/wiki/references/components/static-asset.md +14 -26
  92. package/wiki/references/components/swagger.md +17 -0
  93. package/wiki/references/configuration/environment-variables.md +427 -0
  94. package/wiki/references/configuration/index.md +73 -0
  95. package/wiki/references/helpers/cron.md +14 -0
  96. package/wiki/references/helpers/crypto.md +15 -0
  97. package/wiki/references/helpers/env.md +16 -0
  98. package/wiki/references/helpers/error.md +17 -0
  99. package/wiki/references/helpers/index.md +15 -0
  100. package/wiki/references/helpers/inversion.md +24 -4
  101. package/wiki/references/helpers/logger.md +19 -0
  102. package/wiki/references/helpers/network.md +11 -0
  103. package/wiki/references/helpers/queue.md +19 -0
  104. package/wiki/references/helpers/redis.md +21 -0
  105. package/wiki/references/helpers/socket-io.md +24 -5
  106. package/wiki/references/helpers/storage.md +18 -10
  107. package/wiki/references/helpers/testing.md +18 -0
  108. package/wiki/references/helpers/types.md +167 -0
  109. package/wiki/references/helpers/uid.md +167 -0
  110. package/wiki/references/helpers/worker-thread.md +16 -0
  111. package/wiki/references/index.md +177 -0
  112. package/wiki/references/quick-reference.md +634 -0
  113. package/wiki/references/src-details/boot.md +3 -3
  114. package/wiki/references/src-details/dev-configs.md +0 -4
  115. package/wiki/references/src-details/docs.md +2 -2
  116. package/wiki/references/src-details/index.md +86 -0
  117. package/wiki/references/src-details/inversion.md +1 -6
  118. package/wiki/references/src-details/mcp-server.md +3 -15
  119. package/wiki/references/utilities/index.md +86 -10
  120. package/wiki/references/utilities/jsx.md +577 -0
  121. package/wiki/references/utilities/request.md +0 -2
  122. package/wiki/references/utilities/statuses.md +740 -0
  123. package/wiki/changelogs/planned-transaction-support.md +0 -216
  124. package/wiki/get-started/best-practices/api-usage-examples.md +0 -266
  125. package/wiki/get-started/best-practices/architectural-patterns.md +0 -170
  126. package/wiki/get-started/best-practices/data-modeling.md +0 -177
  127. package/wiki/get-started/best-practices/deployment-strategies.md +0 -121
  128. package/wiki/get-started/best-practices/performance-optimization.md +0 -88
  129. package/wiki/get-started/best-practices/security-guidelines.md +0 -99
  130. package/wiki/get-started/core-concepts/components.md +0 -98
  131. package/wiki/get-started/core-concepts/persistent.md +0 -543
  132. package/wiki/get-started/index.md +0 -65
  133. package/wiki/get-started/philosophy.md +0 -296
  134. package/wiki/get-started/prerequisites.md +0 -113
@@ -0,0 +1,723 @@
1
+ # Testing Your Ignis Application
2
+
3
+ This guide shows you how to write tests for your Ignis application.
4
+
5
+ **⏱️ Time to Complete:** ~30 minutes
6
+
7
+ ## Choose Your Test Framework
8
+
9
+ **Ignis works with any test framework.** You can use whichever testing tool you prefer:
10
+
11
+ | Framework | Description |
12
+ |-----------|-------------|
13
+ | **Jest** | Popular, feature-rich testing framework |
14
+ | **Vitest** | Fast, Vite-native testing framework |
15
+ | **Bun Test** | Built-in test runner for Bun |
16
+ | **Playwright** | End-to-end testing for web applications |
17
+ | **node:test** | Node.js native test module |
18
+ | **Mocha** | Flexible testing framework |
19
+ | **Any other** | All test frameworks work with Ignis |
20
+
21
+ Since Ignis is just a TypeScript/JavaScript application framework, you can test it with any tool that supports TypeScript.
22
+
23
+ ::: tip IGNIS Testing Extension
24
+ IGNIS provides its own testing utilities built on `node:test`. These utilities (`TestPlan`, `TestCase`, `TestCaseHandler`) offer a structured approach for organizing tests with lifecycle hooks and shared context. This is optional — use it if you prefer this pattern, or use your favorite test framework directly.
25
+ :::
26
+
27
+ ## Prerequisites
28
+
29
+ Before starting, ensure you have:
30
+ - A working Ignis application (see [Building a CRUD API](./building-a-crud-api.md))
31
+ - Basic understanding of [Controllers](../core-concepts/controllers.md) and [Repositories](../core-concepts/persistent/)
32
+
33
+ ## Quick Examples with Popular Frameworks
34
+
35
+ ### Using Vitest
36
+
37
+ ```typescript
38
+ // __tests__/todo.test.ts
39
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
40
+ import { app } from '../src/application';
41
+
42
+ describe('Todo API', () => {
43
+ beforeAll(async () => {
44
+ // Setup: start server, seed database, etc.
45
+ });
46
+
47
+ afterAll(async () => {
48
+ // Cleanup: close connections
49
+ });
50
+
51
+ it('should return list of todos', async () => {
52
+ const response = await app.request('/api/todos', { method: 'GET' });
53
+
54
+ expect(response.status).toBe(200);
55
+ const body = await response.json();
56
+ expect(Array.isArray(body)).toBe(true);
57
+ });
58
+
59
+ it('should create a new todo', async () => {
60
+ const response = await app.request('/api/todos', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ title: 'Test Todo' }),
64
+ });
65
+
66
+ expect(response.status).toBe(201);
67
+ const body = await response.json();
68
+ expect(body.title).toBe('Test Todo');
69
+ });
70
+ });
71
+ ```
72
+
73
+ ### Using Jest
74
+
75
+ ```typescript
76
+ // __tests__/todo.test.ts
77
+ import { app } from '../src/application';
78
+
79
+ describe('Todo API', () => {
80
+ it('should return list of todos', async () => {
81
+ const response = await app.request('/api/todos', { method: 'GET' });
82
+
83
+ expect(response.status).toBe(200);
84
+ const body = await response.json();
85
+ expect(Array.isArray(body)).toBe(true);
86
+ });
87
+ });
88
+ ```
89
+
90
+ ### Using Bun Test
91
+
92
+ ```typescript
93
+ // __tests__/todo.test.ts
94
+ import { describe, it, expect } from 'bun:test';
95
+ import { app } from '../src/application';
96
+
97
+ describe('Todo API', () => {
98
+ it('should return list of todos', async () => {
99
+ const response = await app.request('/api/todos', { method: 'GET' });
100
+
101
+ expect(response.status).toBe(200);
102
+ const body = await response.json();
103
+ expect(Array.isArray(body)).toBe(true);
104
+ });
105
+ });
106
+ ```
107
+
108
+ ### Using Playwright (E2E)
109
+
110
+ ```typescript
111
+ // e2e/todo.spec.ts
112
+ import { test, expect } from '@playwright/test';
113
+
114
+ test.describe('Todo Application', () => {
115
+ test('should display todo list', async ({ request }) => {
116
+ const response = await request.get('http://localhost:3000/api/todos');
117
+
118
+ expect(response.ok()).toBeTruthy();
119
+ const todos = await response.json();
120
+ expect(Array.isArray(todos)).toBe(true);
121
+ });
122
+ });
123
+ ```
124
+
125
+ ## Using IGNIS Testing Extension
126
+
127
+ IGNIS provides its own testing utilities built on `node:test` for a more structured approach.
128
+
129
+ ### 1. Create Your First Test
130
+
131
+ Create a test file in your project:
132
+
133
+ ```typescript
134
+ // __tests__/hello.test.ts
135
+ import {
136
+ TestPlan,
137
+ TestDescribe,
138
+ TestCase,
139
+ TestCaseHandler,
140
+ TestCaseDecisions,
141
+ } from '@venizia/ignis-helpers';
142
+
143
+ // Step 1: Define a Test Handler
144
+ class HelloHandler extends TestCaseHandler {
145
+ async execute() {
146
+ // The action to test
147
+ const message = 'Hello, Ignis!';
148
+ return { message };
149
+ }
150
+
151
+ getValidator() {
152
+ // Validate the result
153
+ return (result: { message: string }) => {
154
+ if (result.message === 'Hello, Ignis!') {
155
+ return TestCaseDecisions.SUCCESS;
156
+ }
157
+ return TestCaseDecisions.FAIL;
158
+ };
159
+ }
160
+ }
161
+
162
+ // Step 2: Create a Test Plan
163
+ const helloTestPlan = TestPlan.newInstance({
164
+ scope: 'Hello World Tests',
165
+ testCases: [
166
+ TestCase.withOptions({
167
+ code: 'HELLO-001',
168
+ description: 'Should return greeting message',
169
+ expectation: 'Message equals "Hello, Ignis!"',
170
+ handler: new HelloHandler({ context: {} as any }),
171
+ }),
172
+ ],
173
+ });
174
+
175
+ // Step 3: Run the Test
176
+ TestDescribe.withTestPlan({ testPlan: helloTestPlan }).run();
177
+ ```
178
+
179
+ ### 2. Run Tests
180
+
181
+ ```bash
182
+ # Using Bun
183
+ bun test
184
+
185
+ # Using Node.js
186
+ node --test __tests__/*.test.ts
187
+ ```
188
+
189
+ ## Core Concepts
190
+
191
+ ### Test Framework Components
192
+
193
+ | Component | Purpose |
194
+ |-----------|---------|
195
+ | **TestPlan** | Organizes a test suite with lifecycle hooks and shared context |
196
+ | **TestCase** | A single test unit with code, description, and handler |
197
+ | **TestCaseHandler** | Encapsulates test execution and validation logic |
198
+ | **TestDescribe** | Runs test plans using `node:test` |
199
+
200
+ ### Test Case Decisions
201
+
202
+ | Decision | Meaning |
203
+ |----------|---------|
204
+ | `TestCaseDecisions.SUCCESS` | Test passed |
205
+ | `TestCaseDecisions.FAIL` | Test failed |
206
+ | `TestCaseDecisions.UNKNOWN` | Result undetermined |
207
+
208
+ ### Lifecycle Hooks
209
+
210
+ | Hook | When | Use Case |
211
+ |------|------|----------|
212
+ | `before` | Before all tests | Start server, seed database |
213
+ | `after` | After all tests | Close connections, cleanup |
214
+ | `beforeEach` | Before each test | Reset state |
215
+ | `afterEach` | After each test | Clear test data |
216
+
217
+ ## Testing Controllers
218
+
219
+ Here's how to test an HTTP controller:
220
+
221
+ ```typescript
222
+ // __tests__/todo.controller.test.ts
223
+ import {
224
+ TestPlan,
225
+ TestDescribe,
226
+ TestCase,
227
+ TestCaseHandler,
228
+ TestCaseDecisions,
229
+ } from '@venizia/ignis-helpers';
230
+ import { app } from '../src/application'; // Your Ignis app
231
+
232
+ // Handler for testing GET /todos
233
+ class GetTodosHandler extends TestCaseHandler {
234
+ async execute() {
235
+ // Make HTTP request to your app
236
+ const response = await app.request('/api/todos', {
237
+ method: 'GET',
238
+ });
239
+
240
+ return {
241
+ status: response.status,
242
+ body: await response.json(),
243
+ };
244
+ }
245
+
246
+ getValidator() {
247
+ return (result: { status: number; body: any }) => {
248
+ // Validate status code
249
+ if (result.status !== 200) {
250
+ return TestCaseDecisions.FAIL;
251
+ }
252
+
253
+ // Validate response is an array
254
+ if (!Array.isArray(result.body)) {
255
+ return TestCaseDecisions.FAIL;
256
+ }
257
+
258
+ return TestCaseDecisions.SUCCESS;
259
+ };
260
+ }
261
+ }
262
+
263
+ // Handler for testing POST /todos
264
+ class CreateTodoHandler extends TestCaseHandler {
265
+ async execute() {
266
+ const response = await app.request('/api/todos', {
267
+ method: 'POST',
268
+ headers: { 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({
270
+ title: 'Test Todo',
271
+ description: 'Created by test',
272
+ }),
273
+ });
274
+
275
+ return {
276
+ status: response.status,
277
+ body: await response.json(),
278
+ };
279
+ }
280
+
281
+ getValidator() {
282
+ return (result: { status: number; body: any }) => {
283
+ if (result.status !== 201) {
284
+ return TestCaseDecisions.FAIL;
285
+ }
286
+
287
+ if (result.body.title !== 'Test Todo') {
288
+ return TestCaseDecisions.FAIL;
289
+ }
290
+
291
+ return TestCaseDecisions.SUCCESS;
292
+ };
293
+ }
294
+ }
295
+
296
+ // Create test plan
297
+ const todoControllerTests = TestPlan.newInstance({
298
+ scope: 'Todo Controller',
299
+ hooks: {
300
+ before: async () => {
301
+ console.log('Setting up Todo controller tests...');
302
+ // Start server or setup test database
303
+ },
304
+ after: async () => {
305
+ console.log('Cleaning up...');
306
+ // Cleanup resources
307
+ },
308
+ },
309
+ testCases: [
310
+ TestCase.withOptions({
311
+ code: 'TODO-001',
312
+ description: 'GET /todos returns list of todos',
313
+ expectation: 'Status 200 with array response',
314
+ handler: new GetTodosHandler({ context: {} as any }),
315
+ }),
316
+ TestCase.withOptions({
317
+ code: 'TODO-002',
318
+ description: 'POST /todos creates a new todo',
319
+ expectation: 'Status 201 with created todo',
320
+ handler: new CreateTodoHandler({ context: {} as any }),
321
+ }),
322
+ ],
323
+ });
324
+
325
+ TestDescribe.withTestPlan({ testPlan: todoControllerTests }).run();
326
+ ```
327
+
328
+ ## Testing with Shared Context
329
+
330
+ Use the test plan's context to share data between tests (like authentication tokens):
331
+
332
+ ```typescript
333
+ // __tests__/auth.test.ts
334
+ import {
335
+ TestPlan,
336
+ TestDescribe,
337
+ TestCase,
338
+ TestCaseHandler,
339
+ TestCaseDecisions,
340
+ ITestContext,
341
+ } from '@venizia/ignis-helpers';
342
+
343
+ // Define context shape
344
+ interface AuthContext {
345
+ token: string;
346
+ userId: string;
347
+ }
348
+
349
+ // Handler that uses shared context
350
+ class SecureEndpointHandler extends TestCaseHandler<AuthContext> {
351
+ async execute() {
352
+ // Get token from context (set in before hook)
353
+ const token = this.context.getSync<string>({ key: 'token' });
354
+
355
+ const response = await app.request('/api/profile', {
356
+ method: 'GET',
357
+ headers: {
358
+ Authorization: `Bearer ${token}`,
359
+ },
360
+ });
361
+
362
+ return {
363
+ status: response.status,
364
+ body: await response.json(),
365
+ };
366
+ }
367
+
368
+ getValidator() {
369
+ return (result: { status: number; body: any }) => {
370
+ if (result.status === 200 && result.body.id) {
371
+ return TestCaseDecisions.SUCCESS;
372
+ }
373
+ return TestCaseDecisions.FAIL;
374
+ };
375
+ }
376
+ }
377
+
378
+ const authTests = TestPlan.newInstance<AuthContext>({
379
+ scope: 'Authentication Tests',
380
+ hooks: {
381
+ before: async (testPlan: ITestContext<AuthContext>) => {
382
+ // Login and store token in context
383
+ const loginResponse = await app.request('/api/auth/login', {
384
+ method: 'POST',
385
+ headers: { 'Content-Type': 'application/json' },
386
+ body: JSON.stringify({
387
+ email: 'test@example.com',
388
+ password: 'password123',
389
+ }),
390
+ });
391
+
392
+ const { token, userId } = await loginResponse.json();
393
+
394
+ // Bind to context for use in test cases
395
+ testPlan.bind({ key: 'token', value: token });
396
+ testPlan.bind({ key: 'userId', value: userId });
397
+ },
398
+ },
399
+ testCases: [
400
+ TestCase.withOptions({
401
+ code: 'AUTH-001',
402
+ description: 'Authenticated user can access profile',
403
+ expectation: 'Returns user profile with status 200',
404
+ handler: new SecureEndpointHandler({ context: {} as any }),
405
+ }),
406
+ ],
407
+ });
408
+
409
+ TestDescribe.withTestPlan({ testPlan: authTests }).run();
410
+ ```
411
+
412
+ ## Testing Repositories
413
+
414
+ Test your data access layer directly:
415
+
416
+ ```typescript
417
+ // __tests__/todo.repository.test.ts
418
+ import {
419
+ TestPlan,
420
+ TestDescribe,
421
+ TestCase,
422
+ TestCaseHandler,
423
+ TestCaseDecisions,
424
+ } from '@venizia/ignis-helpers';
425
+ import { TodoRepository } from '../src/repositories/todo.repository';
426
+ import { Container } from '@venizia/ignis-inversion';
427
+
428
+ // Setup container for DI
429
+ const container = new Container();
430
+
431
+ class CreateTodoRepoHandler extends TestCaseHandler {
432
+ async execute() {
433
+ const todoRepo = container.get<TodoRepository>('repositories.TodoRepository');
434
+
435
+ const created = await todoRepo.create({
436
+ title: 'Repository Test',
437
+ description: 'Testing repository layer',
438
+ isCompleted: false,
439
+ });
440
+
441
+ return { todo: created };
442
+ }
443
+
444
+ getValidator() {
445
+ return (result: { todo: any }) => {
446
+ if (result.todo && result.todo.id && result.todo.title === 'Repository Test') {
447
+ return TestCaseDecisions.SUCCESS;
448
+ }
449
+ return TestCaseDecisions.FAIL;
450
+ };
451
+ }
452
+ }
453
+
454
+ class FindTodoRepoHandler extends TestCaseHandler {
455
+ async execute() {
456
+ const todoRepo = container.get<TodoRepository>('repositories.TodoRepository');
457
+
458
+ const todos = await todoRepo.find({
459
+ where: { isCompleted: false },
460
+ limit: 10,
461
+ });
462
+
463
+ return { todos, count: todos.length };
464
+ }
465
+
466
+ getValidator() {
467
+ return (result: { todos: any[]; count: number }) => {
468
+ if (Array.isArray(result.todos) && result.count >= 0) {
469
+ return TestCaseDecisions.SUCCESS;
470
+ }
471
+ return TestCaseDecisions.FAIL;
472
+ };
473
+ }
474
+ }
475
+
476
+ const repoTests = TestPlan.newInstance({
477
+ scope: 'Todo Repository',
478
+ hooks: {
479
+ before: async () => {
480
+ // Setup DI container and database connection
481
+ container.bind('repositories.TodoRepository').toClass(TodoRepository);
482
+ },
483
+ after: async () => {
484
+ // Cleanup test data
485
+ },
486
+ },
487
+ testCases: [
488
+ TestCase.withOptions({
489
+ code: 'REPO-001',
490
+ description: 'Can create a todo via repository',
491
+ expectation: 'Returns created todo with ID',
492
+ handler: new CreateTodoRepoHandler({ context: {} as any }),
493
+ }),
494
+ TestCase.withOptions({
495
+ code: 'REPO-002',
496
+ description: 'Can find todos with filters',
497
+ expectation: 'Returns array of matching todos',
498
+ handler: new FindTodoRepoHandler({ context: {} as any }),
499
+ }),
500
+ ],
501
+ });
502
+
503
+ TestDescribe.withTestPlan({ testPlan: repoTests }).run();
504
+ ```
505
+
506
+ ## Testing Services
507
+
508
+ Test business logic in isolation:
509
+
510
+ ```typescript
511
+ // __tests__/todo.service.test.ts
512
+ import {
513
+ TestPlan,
514
+ TestDescribe,
515
+ TestCase,
516
+ TestCaseHandler,
517
+ TestCaseDecisions,
518
+ } from '@venizia/ignis-helpers';
519
+ import { TodoService } from '../src/services/todo.service';
520
+
521
+ class CompleteTodoHandler extends TestCaseHandler {
522
+ async execute() {
523
+ const todoService = new TodoService();
524
+
525
+ // Create a todo first
526
+ const todo = await todoService.create({
527
+ title: 'Test completion',
528
+ isCompleted: false,
529
+ });
530
+
531
+ // Mark as complete
532
+ const completed = await todoService.markAsComplete(todo.id);
533
+
534
+ return { original: todo, completed };
535
+ }
536
+
537
+ getValidator() {
538
+ return (result: { original: any; completed: any }) => {
539
+ // Original should be incomplete
540
+ if (result.original.isCompleted !== false) {
541
+ return TestCaseDecisions.FAIL;
542
+ }
543
+
544
+ // Completed should be complete
545
+ if (result.completed.isCompleted !== true) {
546
+ return TestCaseDecisions.FAIL;
547
+ }
548
+
549
+ return TestCaseDecisions.SUCCESS;
550
+ };
551
+ }
552
+ }
553
+
554
+ const serviceTests = TestPlan.newInstance({
555
+ scope: 'Todo Service',
556
+ testCases: [
557
+ TestCase.withOptions({
558
+ code: 'SVC-001',
559
+ description: 'Can mark todo as complete',
560
+ expectation: 'Todo isCompleted changes from false to true',
561
+ handler: new CompleteTodoHandler({ context: {} as any }),
562
+ }),
563
+ ],
564
+ });
565
+
566
+ TestDescribe.withTestPlan({ testPlan: serviceTests }).run();
567
+ ```
568
+
569
+ ## Project Structure
570
+
571
+ Organize your tests alongside your source code:
572
+
573
+ ```
574
+ my-ignis-app/
575
+ ├── src/
576
+ │ ├── controllers/
577
+ │ ├── services/
578
+ │ └── repositories/
579
+ ├── __tests__/
580
+ │ ├── controllers/
581
+ │ │ └── todo.controller.test.ts
582
+ │ ├── services/
583
+ │ │ └── todo.service.test.ts
584
+ │ ├── repositories/
585
+ │ │ └── todo.repository.test.ts
586
+ │ └── integration/
587
+ │ └── auth-flow.test.ts
588
+ └── package.json
589
+ ```
590
+
591
+ ### Package.json Scripts
592
+
593
+ Choose scripts based on your preferred test framework:
594
+
595
+ **Bun Test:**
596
+ ```json
597
+ {
598
+ "scripts": {
599
+ "test": "bun test",
600
+ "test:watch": "bun test --watch",
601
+ "test:coverage": "bun test --coverage"
602
+ }
603
+ }
604
+ ```
605
+
606
+ **Vitest:**
607
+ ```json
608
+ {
609
+ "scripts": {
610
+ "test": "vitest run",
611
+ "test:watch": "vitest",
612
+ "test:coverage": "vitest run --coverage"
613
+ }
614
+ }
615
+ ```
616
+
617
+ **Jest:**
618
+ ```json
619
+ {
620
+ "scripts": {
621
+ "test": "jest",
622
+ "test:watch": "jest --watch",
623
+ "test:coverage": "jest --coverage"
624
+ }
625
+ }
626
+ ```
627
+
628
+ **Playwright (E2E):**
629
+ ```json
630
+ {
631
+ "scripts": {
632
+ "test:e2e": "playwright test",
633
+ "test:e2e:ui": "playwright test --ui"
634
+ }
635
+ }
636
+ ```
637
+
638
+ ## Best Practices
639
+
640
+ ### 1. Use Descriptive Test Codes
641
+
642
+ ```typescript
643
+ TestCase.withOptions({
644
+ code: 'AUTH-LOGIN-001', // Feature-Action-Number
645
+ description: 'User can login with valid credentials',
646
+ expectation: 'Returns JWT token and user ID',
647
+ // ...
648
+ });
649
+ ```
650
+
651
+ ### 2. Isolate Test Data
652
+
653
+ ```typescript
654
+ hooks: {
655
+ beforeEach: async (testPlan) => {
656
+ // Create fresh test data for each test
657
+ const testTodo = await createTestTodo();
658
+ testPlan.bind({ key: 'testTodoId', value: testTodo.id });
659
+ },
660
+ afterEach: async (testPlan) => {
661
+ // Clean up after each test
662
+ const todoId = testPlan.getSync({ key: 'testTodoId' });
663
+ await deleteTestTodo(todoId);
664
+ },
665
+ }
666
+ ```
667
+
668
+ ### 3. Test Edge Cases
669
+
670
+ ```typescript
671
+ // Test empty results
672
+ TestCase.withOptions({
673
+ code: 'TODO-FIND-002',
674
+ description: 'Returns empty array when no todos match filter',
675
+ expectation: 'Empty array with status 200',
676
+ handler: new FindNonExistentHandler({ context: {} as any }),
677
+ });
678
+
679
+ // Test validation errors
680
+ TestCase.withOptions({
681
+ code: 'TODO-CREATE-003',
682
+ description: 'Rejects todo without title',
683
+ expectation: 'Status 400 with validation error',
684
+ handler: new CreateInvalidTodoHandler({ context: {} as any }),
685
+ });
686
+ ```
687
+
688
+ ### 4. Keep Handlers Focused
689
+
690
+ Each handler should test one specific behavior:
691
+
692
+ ```typescript
693
+ // Good: Focused on one behavior
694
+ class CreateTodoHandler extends TestCaseHandler {
695
+ async execute() { /* only create logic */ }
696
+ }
697
+
698
+ // Avoid: Multiple behaviors in one handler
699
+ class CreateAndUpdateAndDeleteHandler extends TestCaseHandler {
700
+ async execute() { /* too many things */ }
701
+ }
702
+ ```
703
+
704
+ ## Next Steps
705
+
706
+ - [Testing Reference](../../references/helpers/testing.md) - Complete API documentation
707
+ - [Best Practices](../../best-practices/code-style-standards.md) - Code quality standards
708
+ - [Troubleshooting](../../best-practices/troubleshooting-tips.md) - Common issues
709
+
710
+ ## Summary
711
+
712
+ | What to Test | How |
713
+ |--------------|-----|
714
+ | **Controllers** | Use `app.request()` to make HTTP calls |
715
+ | **Services** | Instantiate and call methods directly |
716
+ | **Repositories** | Use DI container, test with real/mock DB |
717
+ | **Integration** | Chain multiple operations with shared context |
718
+ | **E2E** | Use Playwright or similar for full flow testing |
719
+
720
+ **Key Takeaways:**
721
+ - Use any test framework you prefer (Jest, Vitest, Bun Test, Playwright, etc.)
722
+ - IGNIS provides optional testing utilities (`TestPlan`, `TestCase`, `TestCaseHandler`) built on `node:test`
723
+ - All frameworks work seamlessly with Ignis applications