@lenne.tech/cli 1.0.0 → 1.0.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 (37) hide show
  1. package/build/commands/claude/install-commands.js +337 -0
  2. package/build/commands/claude/install-mcps.js +256 -0
  3. package/build/commands/claude/install-skills.js +91 -20
  4. package/build/commands/server/add-property.js +22 -41
  5. package/build/extensions/server.js +142 -46
  6. package/build/lib/mcp-registry.js +71 -0
  7. package/build/templates/claude-commands/code-cleanup.md +82 -0
  8. package/build/templates/claude-commands/commit-message.md +21 -0
  9. package/build/templates/claude-commands/mr-description-clipboard.md +48 -0
  10. package/build/templates/claude-commands/mr-description.md +33 -0
  11. package/build/templates/claude-commands/sec-review.md +62 -0
  12. package/build/templates/claude-commands/skill-optimize.md +481 -0
  13. package/build/templates/claude-commands/test-generate.md +45 -0
  14. package/build/templates/claude-skills/building-stories-with-tdd/SKILL.md +265 -0
  15. package/build/templates/claude-skills/building-stories-with-tdd/code-quality.md +276 -0
  16. package/build/templates/claude-skills/building-stories-with-tdd/database-indexes.md +182 -0
  17. package/build/templates/claude-skills/building-stories-with-tdd/examples.md +1383 -0
  18. package/build/templates/claude-skills/building-stories-with-tdd/handling-existing-tests.md +197 -0
  19. package/build/templates/claude-skills/building-stories-with-tdd/reference.md +1427 -0
  20. package/build/templates/claude-skills/building-stories-with-tdd/security-review.md +307 -0
  21. package/build/templates/claude-skills/building-stories-with-tdd/workflow.md +1004 -0
  22. package/build/templates/claude-skills/generating-nest-servers/SKILL.md +303 -0
  23. package/build/templates/claude-skills/generating-nest-servers/configuration.md +285 -0
  24. package/build/templates/claude-skills/generating-nest-servers/declare-keyword-warning.md +133 -0
  25. package/build/templates/claude-skills/generating-nest-servers/description-management.md +226 -0
  26. package/build/templates/claude-skills/{nest-server-generator → generating-nest-servers}/examples.md +138 -5
  27. package/build/templates/claude-skills/generating-nest-servers/framework-guide.md +259 -0
  28. package/build/templates/claude-skills/generating-nest-servers/quality-review.md +864 -0
  29. package/build/templates/claude-skills/{nest-server-generator → generating-nest-servers}/reference.md +83 -13
  30. package/build/templates/claude-skills/generating-nest-servers/security-rules.md +371 -0
  31. package/build/templates/claude-skills/generating-nest-servers/verification-checklist.md +262 -0
  32. package/build/templates/claude-skills/generating-nest-servers/workflow-process.md +1061 -0
  33. package/build/templates/claude-skills/{lt-cli → using-lt-cli}/SKILL.md +22 -10
  34. package/build/templates/claude-skills/{lt-cli → using-lt-cli}/examples.md +7 -3
  35. package/build/templates/claude-skills/{lt-cli → using-lt-cli}/reference.md +10 -3
  36. package/package.json +2 -2
  37. package/build/templates/claude-skills/nest-server-generator/SKILL.md +0 -2833
@@ -0,0 +1,1383 @@
1
+ ---
2
+ name: story-tdd-examples
3
+ version: 1.0.1
4
+ description: Complete examples for Test-Driven Development workflow with NestJS story tests
5
+ ---
6
+
7
+ # Story-Based TDD Examples
8
+
9
+ ## Table of Contents
10
+ - [Example 1: Simple CRUD Feature - Product Reviews](#example-1-simple-crud-feature---product-reviews)
11
+ - [Example 2: Complex Business Logic - Order Processing](#example-2-complex-business-logic---order-processing)
12
+ - [Example 3: GraphQL Mutation - User Profile Update](#example-3-graphql-mutation---user-profile-update)
13
+ - [Debugging Test Failures](#debugging-test-failures)
14
+ - [Key Takeaways from Examples](#key-takeaways-from-examples)
15
+
16
+ This document provides complete examples of the TDD workflow for different types of user stories.
17
+
18
+ ## Example 1: Simple CRUD Feature - Product Reviews
19
+
20
+ ### Story Requirement
21
+
22
+ ```
23
+ As a user, I want to add reviews to products so that I can share my experience with other customers.
24
+
25
+ Acceptance Criteria:
26
+ - Users can create a review with rating (1-5) and comment
27
+ - Rating is required, comment is optional
28
+ - Only authenticated users can create reviews
29
+ - Users can view all reviews for a product
30
+ - Each review shows author name and creation date
31
+ ```
32
+
33
+ ### Step 1: Story Analysis
34
+
35
+ **Analysis notes:**
36
+ - New feature, likely needs new Review module
37
+ - Needs relationship between Review and Product
38
+ - Security: Only authenticated users (S_USER role minimum)
39
+ - No mention of update/delete, so only CREATE and READ operations
40
+
41
+ **Questions to clarify:**
42
+ - Can users edit their reviews? (Assuming NO for this example)
43
+ - Can users review a product multiple times? (Assuming NO)
44
+ - What validation for rating? (Assuming 1-5 integer)
45
+
46
+ **🔍 Verification of existing API (CRITICAL - Do this BEFORE writing tests!):**
47
+
48
+ 1. **Check Product endpoints:**
49
+ ```
50
+ ✅ Read src/server/modules/product/product.controller.ts
51
+ ✅ Verified: GET /api/products exists (line 23)
52
+ ✅ Verified: POST /api/products exists (line 45)
53
+ ✅ Verified: Returns Product with id, name, price
54
+ ```
55
+
56
+ 2. **Check Review endpoints:**
57
+ ```
58
+ ❌ Review module does NOT exist yet
59
+ ❌ POST /api/reviews needs to be implemented
60
+ ❌ GET /api/products/:id/reviews needs to be implemented
61
+ ```
62
+
63
+ 3. **Plan implementation:**
64
+ - Need to create Review module (service, controller, model)
65
+ - Need to add POST /api/reviews endpoint
66
+ - Need to add GET /api/products/:id/reviews endpoint
67
+
68
+ ### Step 2: Create Story Test
69
+
70
+ **File:** `tests/stories/product-review.story.test.ts`
71
+
72
+ ```typescript
73
+ import {
74
+ ConfigService,
75
+ getObjectIds,
76
+ HttpExceptionLogFilter,
77
+ TestGraphQLType,
78
+ TestHelper,
79
+ } from '@lenne.tech/nest-server';
80
+ import { Test, TestingModule } from '@nestjs/testing';
81
+ import { PubSub } from 'graphql-subscriptions';
82
+ import { MongoClient, ObjectId } from 'mongodb';
83
+
84
+ import envConfig from '../../src/config.env';
85
+ import { RoleEnum } from '../../src/server/common/enums/role.enum';
86
+ import { imports, ServerModule } from '../../src/server/server.module';
87
+
88
+ // ⚠️ NOTE: No Service imports! Tests must use API endpoints only.
89
+ // Services are only accessed indirectly through Controllers/Resolvers.
90
+
91
+ describe('Product Review Story', () => {
92
+ // Test environment properties
93
+ let app;
94
+ let testHelper: TestHelper;
95
+
96
+ // Database
97
+ let connection;
98
+ let db;
99
+
100
+ // Global test data
101
+ let gAdminToken: string;
102
+ let gAdminId: string;
103
+ let gUserToken: string;
104
+ let gUserId: string;
105
+ let gProductId: string;
106
+
107
+ // Track created entities for cleanup
108
+ let createdReviewIds: string[] = [];
109
+
110
+ beforeAll(async () => {
111
+ // Start server for testing
112
+ const moduleFixture: TestingModule = await Test.createTestingModule({
113
+ imports: [...imports, ServerModule],
114
+ providers: [
115
+ {
116
+ provide: 'PUB_SUB',
117
+ useValue: new PubSub(),
118
+ },
119
+ ],
120
+ }).compile();
121
+
122
+ app = moduleFixture.createNestApplication();
123
+ app.useGlobalFilters(new HttpExceptionLogFilter());
124
+ app.setBaseViewsDir(envConfig.templates.path);
125
+ app.setViewEngine(envConfig.templates.engine);
126
+ await app.init();
127
+
128
+ testHelper = new TestHelper(app);
129
+
130
+ // Connection to database
131
+ connection = await MongoClient.connect(envConfig.mongoose.uri);
132
+ db = await connection.db();
133
+
134
+ // Create admin user via API
135
+ const adminPassword = Math.random().toString(36).substring(7);
136
+ const adminEmail = `admin-${adminPassword}@test.com`;
137
+ const adminSignUp = await testHelper.graphQl({
138
+ arguments: {
139
+ input: {
140
+ email: adminEmail,
141
+ firstName: 'Admin',
142
+ password: adminPassword,
143
+ },
144
+ },
145
+ fields: ['token', { user: ['id', 'email', 'roles'] }],
146
+ name: 'signUp',
147
+ type: TestGraphQLType.MUTATION,
148
+ });
149
+ gAdminId = adminSignUp.user.id;
150
+ gAdminToken = adminSignUp.token;
151
+
152
+ // ✅ ALLOWED EXCEPTION: Set admin role via direct DB access (no API endpoint for this)
153
+ await db.collection('users').updateOne(
154
+ { _id: new ObjectId(gAdminId) },
155
+ { $set: { roles: [RoleEnum.ADMIN] } }
156
+ );
157
+
158
+ // Create normal user
159
+ const userPassword = Math.random().toString(36).substring(7);
160
+ const userEmail = `user-${userPassword}@test.com`;
161
+ const userSignUp = await testHelper.graphQl({
162
+ arguments: {
163
+ input: {
164
+ email: userEmail,
165
+ firstName: 'Test',
166
+ password: userPassword,
167
+ },
168
+ },
169
+ fields: ['token', { user: ['id', 'email'] }],
170
+ name: 'signUp',
171
+ type: TestGraphQLType.MUTATION,
172
+ });
173
+ gUserId = userSignUp.user.id;
174
+ gUserToken = userSignUp.token;
175
+
176
+ // Create test product
177
+ const product = await testHelper.rest('/api/products', {
178
+ method: 'POST',
179
+ payload: {
180
+ name: 'Test Product',
181
+ price: 99.99,
182
+ },
183
+ token: gAdminToken,
184
+ });
185
+ gProductId = product.id;
186
+ });
187
+
188
+ afterAll(async () => {
189
+ // 🧹 CLEANUP: Delete all test data created during tests
190
+ try {
191
+ // Delete all created reviews (using getObjectIds for array conversion)
192
+ if (createdReviewIds.length > 0) {
193
+ await db.collection('reviews').deleteMany({
194
+ _id: { $in: getObjectIds(createdReviewIds) }
195
+ });
196
+ }
197
+
198
+ // Delete test product (using getObjectIds for single value - no array needed!)
199
+ if (gProductId) {
200
+ await db.collection('products').deleteOne({ _id: getObjectIds(gProductId) });
201
+ }
202
+
203
+ // Delete test users (using getObjectIds for single values)
204
+ if (gUserId) {
205
+ await db.collection('users').deleteOne({ _id: getObjectIds(gUserId) });
206
+ }
207
+ if (gAdminId) {
208
+ await db.collection('users').deleteOne({ _id: getObjectIds(gAdminId) });
209
+ }
210
+ } catch (error) {
211
+ console.error('Cleanup failed:', error);
212
+ }
213
+
214
+ await connection.close();
215
+ await app.close();
216
+ });
217
+
218
+ describe('Creating Reviews', () => {
219
+ it('should allow authenticated user to create review with rating and comment', async () => {
220
+ const review = await testHelper.rest('/api/reviews', {
221
+ method: 'POST',
222
+ payload: {
223
+ productId: gProductId,
224
+ rating: 5,
225
+ comment: 'Excellent product!',
226
+ },
227
+ token: gUserToken,
228
+ });
229
+
230
+ expect(review).toMatchObject({
231
+ rating: 5,
232
+ comment: 'Excellent product!',
233
+ authorId: gUserId,
234
+ });
235
+ expect(review.id).toBeDefined();
236
+ expect(review.createdAt).toBeDefined();
237
+
238
+ // Track for cleanup
239
+ createdReviewIds.push(review.id);
240
+ });
241
+
242
+ it('should allow review with rating only (no comment)', async () => {
243
+ const review = await testHelper.rest('/api/reviews', {
244
+ method: 'POST',
245
+ payload: {
246
+ productId: gProductId,
247
+ rating: 4,
248
+ },
249
+ token: gUserToken,
250
+ });
251
+
252
+ expect(review.rating).toBe(4);
253
+ expect(review.comment).toBeUndefined();
254
+
255
+ // Track for cleanup
256
+ createdReviewIds.push(review.id);
257
+ });
258
+
259
+ it('should reject review without rating', async () => {
260
+ await testHelper.rest('/api/reviews', {
261
+ method: 'POST',
262
+ payload: {
263
+ productId: gProductId,
264
+ comment: 'Missing rating',
265
+ },
266
+ statusCode: 400,
267
+ token: gUserToken,
268
+ });
269
+ });
270
+
271
+ it('should reject review with invalid rating', async () => {
272
+ await testHelper.rest('/api/reviews', {
273
+ method: 'POST',
274
+ payload: {
275
+ productId: gProductId,
276
+ rating: 6, // Invalid: must be 1-5
277
+ },
278
+ statusCode: 400,
279
+ token: gUserToken,
280
+ });
281
+ });
282
+
283
+ it('should reject unauthenticated review creation', async () => {
284
+ await testHelper.rest('/api/reviews', {
285
+ method: 'POST',
286
+ payload: {
287
+ productId: gProductId,
288
+ rating: 5,
289
+ comment: 'Trying without auth',
290
+ },
291
+ statusCode: 401,
292
+ });
293
+ });
294
+ });
295
+
296
+ describe('Viewing Reviews', () => {
297
+ let createdReviewId: string;
298
+
299
+ beforeAll(async () => {
300
+ // Create a review for testing
301
+ const review = await testHelper.rest('/api/reviews', {
302
+ method: 'POST',
303
+ payload: {
304
+ productId: gProductId,
305
+ rating: 5,
306
+ comment: 'Great product',
307
+ },
308
+ token: gUserToken,
309
+ });
310
+ createdReviewId = review.id;
311
+
312
+ // Track for cleanup
313
+ createdReviewIds.push(review.id);
314
+ });
315
+
316
+ it('should allow anyone to view product reviews', async () => {
317
+ const reviews = await testHelper.rest(`/api/products/${gProductId}/reviews`);
318
+
319
+ expect(reviews).toBeInstanceOf(Array);
320
+ expect(reviews.length).toBeGreaterThan(0);
321
+
322
+ const review = reviews.find(r => r.id === createdReviewId);
323
+ expect(review).toMatchObject({
324
+ rating: 5,
325
+ comment: 'Great product',
326
+ });
327
+ expect(review.author).toBeDefined();
328
+ expect(review.createdAt).toBeDefined();
329
+ });
330
+
331
+ it('should return empty array for product with no reviews', async () => {
332
+ // Create product without reviews
333
+ const newProduct = await testHelper.rest('/api/products', {
334
+ method: 'POST',
335
+ payload: {
336
+ name: 'New Product',
337
+ price: 49.99,
338
+ },
339
+ token: gAdminToken,
340
+ });
341
+
342
+ const reviews = await testHelper.rest(`/api/products/${newProduct.id}/reviews`);
343
+ expect(reviews).toEqual([]);
344
+ });
345
+ });
346
+ });
347
+ ```
348
+
349
+ ### Step 3-5: Implementation Iteration
350
+
351
+ **First run - Expected failures:**
352
+ ```
353
+ ❌ POST /api/reviews → 404 (endpoint doesn't exist)
354
+ ❌ GET /api/products/:id/reviews → 404 (endpoint doesn't exist)
355
+ ```
356
+
357
+ **Implementation (using nest-server-generator):**
358
+ ```bash
359
+ # Create Review module
360
+ lt server module Review --no-interactive
361
+
362
+ # Add properties
363
+ lt server addProp Review productId:string --no-interactive
364
+ lt server addProp Review authorId:string --no-interactive
365
+ lt server addProp Review rating:number --no-interactive
366
+ lt server addProp Review comment:string? --no-interactive
367
+ ```
368
+
369
+ **Manual adjustments needed:**
370
+ - Add validation for rating (1-5 range)
371
+ - Add @Restricted decorator with appropriate roles
372
+ - Add GET endpoint to ProductController for reviews
373
+ - Add relationship between Product and Review
374
+
375
+ **Final run - All tests pass:**
376
+ ```
377
+ ✅ All tests passing (8 scenarios)
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Example 2: Complex Business Logic - Order Processing
383
+
384
+ ### Story Requirement
385
+
386
+ ```
387
+ As a customer, I want to place an order with multiple products so that I can purchase items together.
388
+
389
+ Acceptance Criteria:
390
+ - Order contains multiple products with quantities
391
+ - Order calculates total price automatically
392
+ - Order cannot be created with empty product list
393
+ - Order requires delivery address
394
+ - Order status is initially "pending"
395
+ - Products are checked for availability
396
+ - Insufficient stock prevents order creation
397
+ ```
398
+
399
+ ### Step 1: Story Analysis
400
+
401
+ **Analysis notes:**
402
+ - Needs Order module with relationship to Product
403
+ - Needs OrderItem subobject for quantity tracking
404
+ - Business logic: stock validation
405
+ - Calculated field: total price
406
+ - Complex validation rules
407
+
408
+ **Architecture decisions:**
409
+ - Use SubObject for OrderItem (embedded in Order)
410
+ - Total price should be calculated in service layer
411
+ - Stock check happens in service before saving
412
+
413
+ ### Step 2: Create Story Test
414
+
415
+ **File:** `tests/stories/order-processing.story.test.ts`
416
+
417
+ ```typescript
418
+ import {
419
+ ConfigService,
420
+ getObjectIds,
421
+ HttpExceptionLogFilter,
422
+ TestGraphQLType,
423
+ TestHelper,
424
+ } from '@lenne.tech/nest-server';
425
+ import { Test, TestingModule } from '@nestjs/testing';
426
+ import { PubSub } from 'graphql-subscriptions';
427
+ import { MongoClient, ObjectId } from 'mongodb';
428
+
429
+ import envConfig from '../../src/config.env';
430
+ import { RoleEnum } from '../../src/server/common/enums/role.enum';
431
+ import { imports, ServerModule } from '../../src/server/server.module';
432
+
433
+ // ⚠️ NOTE: No Service imports! Tests must use API endpoints only.
434
+ // Services are only accessed indirectly through Controllers/Resolvers.
435
+
436
+ describe('Order Processing Story', () => {
437
+ // Test environment properties
438
+ let app;
439
+ let testHelper: TestHelper;
440
+
441
+ // Database
442
+ let connection;
443
+ let db;
444
+
445
+ // Global test data
446
+ let gAdminToken: string;
447
+ let gAdminId: string;
448
+ let gCustomerToken: string;
449
+ let gCustomerId: string;
450
+ let gProduct1Id: string;
451
+ let gProduct1Stock: number;
452
+ let gProduct2Id: string;
453
+
454
+ // Track created entities for cleanup
455
+ let createdOrderIds: string[] = [];
456
+ let createdProductIds: string[] = [];
457
+
458
+ beforeAll(async () => {
459
+ // Start server for testing
460
+ const moduleFixture: TestingModule = await Test.createTestingModule({
461
+ imports: [...imports, ServerModule],
462
+ providers: [
463
+ {
464
+ provide: 'PUB_SUB',
465
+ useValue: new PubSub(),
466
+ },
467
+ ],
468
+ }).compile();
469
+
470
+ app = moduleFixture.createNestApplication();
471
+ app.useGlobalFilters(new HttpExceptionLogFilter());
472
+ app.setBaseViewsDir(envConfig.templates.path);
473
+ app.setViewEngine(envConfig.templates.engine);
474
+ await app.init();
475
+
476
+ testHelper = new TestHelper(app);
477
+
478
+ // Connection to database
479
+ connection = await MongoClient.connect(envConfig.mongoose.uri);
480
+ db = await connection.db();
481
+
482
+ // Create admin user via API
483
+ const adminPassword = Math.random().toString(36).substring(7);
484
+ const adminEmail = `admin-${adminPassword}@test.com`;
485
+ const adminSignUp = await testHelper.graphQl({
486
+ arguments: {
487
+ input: {
488
+ email: adminEmail,
489
+ firstName: 'Admin',
490
+ password: adminPassword,
491
+ },
492
+ },
493
+ fields: ['token', { user: ['id', 'email'] }],
494
+ name: 'signUp',
495
+ type: TestGraphQLType.MUTATION,
496
+ });
497
+ gAdminId = adminSignUp.user.id;
498
+ gAdminToken = adminSignUp.token;
499
+
500
+ // ✅ ALLOWED EXCEPTION: Set admin role via direct DB access (no API endpoint for this)
501
+ await db.collection('users').updateOne(
502
+ { _id: new ObjectId(gAdminId) },
503
+ { $set: { roles: [RoleEnum.ADMIN] } }
504
+ );
505
+
506
+ // Create customer user
507
+ const customerPassword = Math.random().toString(36).substring(7);
508
+ const customerEmail = `customer-${customerPassword}@test.com`;
509
+ const customerSignUp = await testHelper.graphQl({
510
+ arguments: {
511
+ input: {
512
+ email: customerEmail,
513
+ firstName: 'Customer',
514
+ password: customerPassword,
515
+ },
516
+ },
517
+ fields: ['token', { user: ['id'] }],
518
+ name: 'signUp',
519
+ type: TestGraphQLType.MUTATION,
520
+ });
521
+ gCustomerId = customerSignUp.user.id;
522
+ gCustomerToken = customerSignUp.token;
523
+
524
+ // Create test products with stock
525
+ const product1 = await testHelper.rest('/api/products', {
526
+ method: 'POST',
527
+ payload: {
528
+ name: 'Product A',
529
+ price: 10.00,
530
+ stock: 100,
531
+ },
532
+ token: gAdminToken,
533
+ });
534
+ gProduct1Id = product1.id;
535
+ gProduct1Stock = product1.stock;
536
+
537
+ const product2 = await testHelper.rest('/api/products', {
538
+ method: 'POST',
539
+ payload: {
540
+ name: 'Product B',
541
+ price: 25.50,
542
+ stock: 50,
543
+ },
544
+ token: gAdminToken,
545
+ });
546
+ gProduct2Id = product2.id;
547
+
548
+ // Track products for cleanup
549
+ createdProductIds.push(gProduct1Id, gProduct2Id);
550
+ });
551
+
552
+ afterAll(async () => {
553
+ // 🧹 CLEANUP: Delete all test data created during tests
554
+ try {
555
+ // Delete all created orders first (child entities) - using getObjectIds for arrays
556
+ if (createdOrderIds.length > 0) {
557
+ await db.collection('orders').deleteMany({
558
+ _id: { $in: getObjectIds(createdOrderIds) }
559
+ });
560
+ }
561
+
562
+ // Delete all created products - using getObjectIds for arrays
563
+ if (createdProductIds.length > 0) {
564
+ await db.collection('products').deleteMany({
565
+ _id: { $in: getObjectIds(createdProductIds) }
566
+ });
567
+ }
568
+
569
+ // Delete test users - using getObjectIds for single values (no array needed!)
570
+ if (gCustomerId) {
571
+ await db.collection('users').deleteOne({ _id: getObjectIds(gCustomerId) });
572
+ }
573
+ if (gAdminId) {
574
+ await db.collection('users').deleteOne({ _id: getObjectIds(gAdminId) });
575
+ }
576
+ } catch (error) {
577
+ console.error('Cleanup failed:', error);
578
+ }
579
+
580
+ await connection.close();
581
+ await app.close();
582
+ });
583
+
584
+ describe('Order Creation - Happy Path', () => {
585
+ it('should create order with multiple products and calculate total', async () => {
586
+ const orderData = {
587
+ items: [
588
+ { productId: gProduct1Id, quantity: 2 },
589
+ { productId: gProduct2Id, quantity: 1 },
590
+ ],
591
+ deliveryAddress: {
592
+ street: '123 Main St',
593
+ city: 'Test City',
594
+ zipCode: '12345',
595
+ country: 'Germany',
596
+ },
597
+ };
598
+
599
+ const order = await testHelper.rest('/api/orders', {
600
+ method: 'POST',
601
+ payload: orderData,
602
+ token: gCustomerToken,
603
+ });
604
+
605
+ expect(order).toMatchObject({
606
+ status: 'pending',
607
+ customerId: gCustomerId,
608
+ totalPrice: 45.50, // (10.00 * 2) + (25.50 * 1)
609
+ deliveryAddress: orderData.deliveryAddress,
610
+ });
611
+
612
+ expect(order.items).toHaveLength(2);
613
+ expect(order.items[0]).toMatchObject({
614
+ productId: gProduct1Id,
615
+ quantity: 2,
616
+ priceAtOrder: 10.00,
617
+ });
618
+
619
+ // Track for cleanup
620
+ createdOrderIds.push(order.id);
621
+ });
622
+
623
+ it('should create order with single product', async () => {
624
+ const orderData = {
625
+ items: [
626
+ { productId: gProduct1Id, quantity: 1 },
627
+ ],
628
+ deliveryAddress: {
629
+ street: '456 Oak Ave',
630
+ city: 'Sample Town',
631
+ zipCode: '54321',
632
+ country: 'Germany',
633
+ },
634
+ };
635
+
636
+ const order = await testHelper.rest('/api/orders', {
637
+ method: 'POST',
638
+ payload: orderData,
639
+ token: gCustomerToken,
640
+ });
641
+
642
+ expect(order.totalPrice).toBe(10.00);
643
+
644
+ // Track for cleanup
645
+ createdOrderIds.push(order.id);
646
+ });
647
+ });
648
+
649
+ describe('Order Validation', () => {
650
+ it('should reject order with empty product list', async () => {
651
+ const orderData = {
652
+ items: [],
653
+ deliveryAddress: {
654
+ street: '123 Main St',
655
+ city: 'Test City',
656
+ zipCode: '12345',
657
+ country: 'Germany',
658
+ },
659
+ };
660
+
661
+ await testHelper.rest('/api/orders', {
662
+ method: 'POST',
663
+ payload: orderData,
664
+ statusCode: 400,
665
+ token: gCustomerToken,
666
+ });
667
+ });
668
+
669
+ it('should reject order without delivery address', async () => {
670
+ const orderData = {
671
+ items: [
672
+ { productId: gProduct1Id, quantity: 1 },
673
+ ],
674
+ };
675
+
676
+ await testHelper.rest('/api/orders', {
677
+ method: 'POST',
678
+ payload: orderData,
679
+ statusCode: 400,
680
+ token: gCustomerToken,
681
+ });
682
+ });
683
+
684
+ it('should reject order with invalid product ID', async () => {
685
+ const orderData = {
686
+ items: [
687
+ { productId: 'invalid-id', quantity: 1 },
688
+ ],
689
+ deliveryAddress: {
690
+ street: '123 Main St',
691
+ city: 'Test City',
692
+ zipCode: '12345',
693
+ country: 'Germany',
694
+ },
695
+ };
696
+
697
+ await testHelper.rest('/api/orders', {
698
+ method: 'POST',
699
+ payload: orderData,
700
+ statusCode: 404,
701
+ token: gCustomerToken,
702
+ });
703
+ });
704
+ });
705
+
706
+ describe('Stock Management', () => {
707
+ it('should reject order when product stock is insufficient', async () => {
708
+ // Create product with limited stock
709
+ const limitedProduct = await testHelper.rest('/api/products', {
710
+ method: 'POST',
711
+ payload: {
712
+ name: 'Limited Product',
713
+ price: 100.00,
714
+ stock: 5,
715
+ },
716
+ token: gAdminToken,
717
+ });
718
+
719
+ // Track for cleanup
720
+ createdProductIds.push(limitedProduct.id);
721
+
722
+ const orderData = {
723
+ items: [
724
+ { productId: limitedProduct.id, quantity: 10 }, // More than available
725
+ ],
726
+ deliveryAddress: {
727
+ street: '123 Main St',
728
+ city: 'Test City',
729
+ zipCode: '12345',
730
+ country: 'Germany',
731
+ },
732
+ };
733
+
734
+ const response = await testHelper.rest('/api/orders', {
735
+ method: 'POST',
736
+ payload: orderData,
737
+ statusCode: 400,
738
+ token: gCustomerToken,
739
+ });
740
+
741
+ expect(response.message).toContain('insufficient stock');
742
+ });
743
+
744
+ it('should reduce product stock after successful order', async () => {
745
+ const initialStock = gProduct1Stock;
746
+
747
+ const orderData = {
748
+ items: [
749
+ { productId: gProduct1Id, quantity: 3 },
750
+ ],
751
+ deliveryAddress: {
752
+ street: '123 Main St',
753
+ city: 'Test City',
754
+ zipCode: '12345',
755
+ country: 'Germany',
756
+ },
757
+ };
758
+
759
+ const order = await testHelper.rest('/api/orders', {
760
+ method: 'POST',
761
+ payload: orderData,
762
+ token: gCustomerToken,
763
+ });
764
+
765
+ // Track for cleanup
766
+ createdOrderIds.push(order.id);
767
+
768
+ // Check product stock was reduced
769
+ const updatedProduct = await testHelper.rest(`/api/products/${gProduct1Id}`, {
770
+ token: gAdminToken,
771
+ });
772
+
773
+ expect(updatedProduct.stock).toBe(initialStock - 3);
774
+
775
+ // Update global stock for subsequent tests
776
+ gProduct1Stock = updatedProduct.stock;
777
+ });
778
+ });
779
+
780
+ describe('Authorization', () => {
781
+ it('should reject unauthenticated order creation', async () => {
782
+ const orderData = {
783
+ items: [
784
+ { productId: gProduct1Id, quantity: 1 },
785
+ ],
786
+ deliveryAddress: {
787
+ street: '123 Main St',
788
+ city: 'Test City',
789
+ zipCode: '12345',
790
+ country: 'Germany',
791
+ },
792
+ };
793
+
794
+ await testHelper.rest('/api/orders', {
795
+ method: 'POST',
796
+ payload: orderData,
797
+ statusCode: 401,
798
+ });
799
+ });
800
+ });
801
+ });
802
+ ```
803
+
804
+ ### Implementation Steps
805
+
806
+ **SubObject creation:**
807
+ ```typescript
808
+ // Create OrderItem SubObject manually
809
+ // File: src/server/modules/order/order-item.subobject.ts
810
+
811
+ @SubObjectType()
812
+ export class OrderItem {
813
+ @UnifiedField({
814
+ description: 'Reference to product',
815
+ mongoose: { index: true, type: String } // ✅ Index for queries by product
816
+ })
817
+ productId: string;
818
+
819
+ @UnifiedField({
820
+ description: 'Quantity ordered',
821
+ mongoose: { type: Number }
822
+ })
823
+ quantity: number;
824
+
825
+ @UnifiedField({
826
+ description: 'Price when order was placed',
827
+ mongoose: { type: Number }
828
+ })
829
+ priceAtOrder: number;
830
+ }
831
+ ```
832
+
833
+ **Model with indexes:**
834
+ ```typescript
835
+ // File: src/server/modules/order/order.model.ts
836
+
837
+ @Schema()
838
+ export class Order {
839
+ @UnifiedField({
840
+ description: 'Customer who placed the order',
841
+ mongoose: { index: true, type: String } // ✅ Frequent queries by customer
842
+ })
843
+ customerId: string;
844
+
845
+ @UnifiedField({
846
+ description: 'Order status',
847
+ mongoose: { index: true, type: String } // ✅ Filtering by status
848
+ })
849
+ status: string;
850
+
851
+ @UnifiedField({
852
+ description: 'Order items',
853
+ mongoose: { type: [OrderItem] }
854
+ })
855
+ items: OrderItem[];
856
+
857
+ @UnifiedField({
858
+ description: 'Total price calculated from items',
859
+ mongoose: { type: Number }
860
+ })
861
+ totalPrice: number;
862
+
863
+ @UnifiedField({
864
+ description: 'Delivery address',
865
+ mongoose: { type: Object }
866
+ })
867
+ deliveryAddress: Address;
868
+ }
869
+ ```
870
+
871
+ **Why these indexes?**
872
+ - `customerId`: Service queries orders by customer → needs index
873
+ - `status`: Service filters by status (pending, completed) → needs index
874
+ - Both indexed individually for flexible querying
875
+
876
+ **Service logic for total calculation and stock validation:**
877
+ ```typescript
878
+ // In OrderService (extends CrudService)
879
+
880
+ async create(input: CreateOrderInput, userId: string): Promise<Order> {
881
+ // Validate items exist
882
+ if (!input.items || input.items.length === 0) {
883
+ throw new BadRequestException('Order must contain at least one item');
884
+ }
885
+
886
+ // Check stock and calculate total
887
+ let totalPrice = 0;
888
+ const orderItems = [];
889
+
890
+ for (const item of input.items) {
891
+ const product = await this.productService.findById(item.productId);
892
+ if (!product) {
893
+ throw new NotFoundException(`Product ${item.productId} not found`);
894
+ }
895
+
896
+ if (product.stock < item.quantity) {
897
+ throw new BadRequestException(
898
+ `Insufficient stock for product ${product.name}`
899
+ );
900
+ }
901
+
902
+ orderItems.push({
903
+ productId: product.id,
904
+ quantity: item.quantity,
905
+ priceAtOrder: product.price,
906
+ });
907
+
908
+ totalPrice += product.price * item.quantity;
909
+ }
910
+
911
+ // Create order
912
+ const order = await super.create({
913
+ ...input,
914
+ items: orderItems,
915
+ totalPrice,
916
+ customerId: userId,
917
+ status: 'pending',
918
+ });
919
+
920
+ // Reduce stock
921
+ for (const item of input.items) {
922
+ await this.productService.reduceStock(item.productId, item.quantity);
923
+ }
924
+
925
+ return order;
926
+ }
927
+ ```
928
+
929
+ ---
930
+
931
+ ## Example 3: GraphQL Mutation - User Profile Update
932
+
933
+ ### Story Requirement
934
+
935
+ ```
936
+ As a user, I want to update my profile information so that my account reflects current details.
937
+
938
+ Acceptance Criteria:
939
+ - Users can update their firstName, lastName, phone
940
+ - Users cannot change their email through this endpoint
941
+ - Users can only update their own profile
942
+ - Admin users can update any profile
943
+ - Phone number must be validated (German format)
944
+ ```
945
+
946
+ ### Step 2: Create Story Test (GraphQL)
947
+
948
+ **File:** `tests/stories/profile-update.story.test.ts`
949
+
950
+ ```typescript
951
+ import {
952
+ ConfigService,
953
+ getObjectIds,
954
+ HttpExceptionLogFilter,
955
+ TestGraphQLType,
956
+ TestHelper,
957
+ } from '@lenne.tech/nest-server';
958
+ import { Test, TestingModule } from '@nestjs/testing';
959
+ import { PubSub } from 'graphql-subscriptions';
960
+ import { MongoClient, ObjectId } from 'mongodb';
961
+
962
+ import envConfig from '../../src/config.env';
963
+ import { RoleEnum } from '../../src/server/common/enums/role.enum';
964
+ import { imports, ServerModule } from '../../src/server/server.module';
965
+
966
+ // ⚠️ NOTE: No Service imports! Tests must use API endpoints only.
967
+ // Services are only accessed indirectly through Controllers/Resolvers.
968
+
969
+ describe('Profile Update Story (GraphQL)', () => {
970
+ // Test environment properties
971
+ let app;
972
+ let testHelper: TestHelper;
973
+
974
+ // Database
975
+ let connection;
976
+ let db;
977
+
978
+ // Global test data
979
+ let gNormalUserId: string;
980
+ let gNormalUserToken: string;
981
+ let gNormalUserEmail: string;
982
+ let gOtherUserId: string;
983
+ let gOtherUserToken: string;
984
+ let gAdminUserId: string;
985
+ let gAdminUserToken: string;
986
+
987
+ // Track created entities for cleanup
988
+ let createdUserIds: string[] = [];
989
+
990
+ beforeAll(async () => {
991
+ // Start server for testing
992
+ const moduleFixture: TestingModule = await Test.createTestingModule({
993
+ imports: [...imports, ServerModule],
994
+ providers: [
995
+ {
996
+ provide: 'PUB_SUB',
997
+ useValue: new PubSub(),
998
+ },
999
+ ],
1000
+ }).compile();
1001
+
1002
+ app = moduleFixture.createNestApplication();
1003
+ app.useGlobalFilters(new HttpExceptionLogFilter());
1004
+ app.setBaseViewsDir(envConfig.templates.path);
1005
+ app.setViewEngine(envConfig.templates.engine);
1006
+ await app.init();
1007
+
1008
+ testHelper = new TestHelper(app);
1009
+
1010
+ // Connection to database
1011
+ connection = await MongoClient.connect(envConfig.mongoose.uri);
1012
+ db = await connection.db();
1013
+
1014
+ // Create normal user
1015
+ const normalPassword = Math.random().toString(36).substring(7);
1016
+ gNormalUserEmail = `user-${normalPassword}@test.com`;
1017
+ const normalSignUp = await testHelper.graphQl({
1018
+ arguments: {
1019
+ input: {
1020
+ email: gNormalUserEmail,
1021
+ firstName: 'John',
1022
+ lastName: 'Doe',
1023
+ password: normalPassword,
1024
+ },
1025
+ },
1026
+ fields: ['token', { user: ['id', 'email'] }],
1027
+ name: 'signUp',
1028
+ type: TestGraphQLType.MUTATION,
1029
+ });
1030
+ gNormalUserId = normalSignUp.user.id;
1031
+ gNormalUserToken = normalSignUp.token;
1032
+
1033
+ // Track for cleanup
1034
+ createdUserIds.push(gNormalUserId);
1035
+
1036
+ // Create other user
1037
+ const otherPassword = Math.random().toString(36).substring(7);
1038
+ const otherEmail = `other-${otherPassword}@test.com`;
1039
+ const otherSignUp = await testHelper.graphQl({
1040
+ arguments: {
1041
+ input: {
1042
+ email: otherEmail,
1043
+ firstName: 'Other',
1044
+ password: otherPassword,
1045
+ },
1046
+ },
1047
+ fields: ['token', { user: ['id'] }],
1048
+ name: 'signUp',
1049
+ type: TestGraphQLType.MUTATION,
1050
+ });
1051
+ gOtherUserId = otherSignUp.user.id;
1052
+ gOtherUserToken = otherSignUp.token;
1053
+
1054
+ // Track for cleanup
1055
+ createdUserIds.push(gOtherUserId);
1056
+
1057
+ // Create admin user
1058
+ const adminPassword = Math.random().toString(36).substring(7);
1059
+ const adminEmail = `admin-${adminPassword}@test.com`;
1060
+ const adminSignUp = await testHelper.graphQl({
1061
+ arguments: {
1062
+ input: {
1063
+ email: adminEmail,
1064
+ firstName: 'Admin',
1065
+ password: adminPassword,
1066
+ },
1067
+ },
1068
+ fields: ['token', { user: ['id'] }],
1069
+ name: 'signUp',
1070
+ type: TestGraphQLType.MUTATION,
1071
+ });
1072
+ gAdminUserId = adminSignUp.user.id;
1073
+ gAdminUserToken = adminSignUp.token;
1074
+
1075
+ // Track for cleanup
1076
+ createdUserIds.push(gAdminUserId);
1077
+
1078
+ // ✅ ALLOWED EXCEPTION: Set admin role via direct DB access (no API endpoint for this)
1079
+ await db.collection('users').updateOne(
1080
+ { _id: new ObjectId(gAdminUserId) },
1081
+ { $set: { roles: [RoleEnum.ADMIN] } }
1082
+ );
1083
+ });
1084
+
1085
+ afterAll(async () => {
1086
+ // 🧹 CLEANUP: Delete all test data created during tests
1087
+ try {
1088
+ // Delete all created users - using getObjectIds
1089
+ if (createdUserIds.length > 0) {
1090
+ await db.collection('users').deleteMany({
1091
+ _id: { $in: getObjectIds(createdUserIds) }
1092
+ });
1093
+ }
1094
+ } catch (error) {
1095
+ console.error('Cleanup failed:', error);
1096
+ }
1097
+
1098
+ await connection.close();
1099
+ await app.close();
1100
+ });
1101
+
1102
+ describe('Own Profile Update', () => {
1103
+ it('should allow user to update own profile', async () => {
1104
+ const result = await testHelper.graphQl({
1105
+ arguments: {
1106
+ id: gNormalUserId,
1107
+ input: {
1108
+ firstName: 'Jane',
1109
+ lastName: 'Smith',
1110
+ phone: '+49 123 456789',
1111
+ },
1112
+ },
1113
+ fields: ['id', 'firstName', 'lastName', 'phone', 'email'],
1114
+ name: 'updateUser',
1115
+ type: TestGraphQLType.MUTATION,
1116
+ }, { token: gNormalUserToken });
1117
+
1118
+ expect(result).toMatchObject({
1119
+ id: gNormalUserId,
1120
+ firstName: 'Jane',
1121
+ lastName: 'Smith',
1122
+ phone: '+49 123 456789',
1123
+ email: gNormalUserEmail, // Email unchanged
1124
+ });
1125
+ });
1126
+
1127
+ it('should prevent user from changing email', async () => {
1128
+ const result = await testHelper.graphQl({
1129
+ arguments: {
1130
+ id: gNormalUserId,
1131
+ input: {
1132
+ firstName: 'John',
1133
+ email: 'newemail@test.com', // Attempt to change email
1134
+ },
1135
+ },
1136
+ fields: ['email'],
1137
+ name: 'updateUser',
1138
+ type: TestGraphQLType.MUTATION,
1139
+ }, { token: gNormalUserToken });
1140
+
1141
+ // Email should remain unchanged
1142
+ expect(result.email).toBe(gNormalUserEmail);
1143
+ });
1144
+ });
1145
+
1146
+ describe('Authorization', () => {
1147
+ it('should prevent user from updating other user profile', async () => {
1148
+ const result = await testHelper.graphQl({
1149
+ arguments: {
1150
+ id: gOtherUserId,
1151
+ input: {
1152
+ firstName: 'Hacker',
1153
+ },
1154
+ },
1155
+ fields: ['id', 'firstName'],
1156
+ name: 'updateUser',
1157
+ type: TestGraphQLType.MUTATION,
1158
+ }, { token: gNormalUserToken, statusCode: 200 });
1159
+
1160
+ expect(result.errors).toBeDefined();
1161
+ expect(result.errors[0].message).toContain('Forbidden');
1162
+ });
1163
+
1164
+ it('should allow admin to update any profile', async () => {
1165
+ const result = await testHelper.graphQl({
1166
+ arguments: {
1167
+ id: gNormalUserId,
1168
+ input: {
1169
+ firstName: 'AdminUpdated',
1170
+ },
1171
+ },
1172
+ fields: ['firstName'],
1173
+ name: 'updateUser',
1174
+ type: TestGraphQLType.MUTATION,
1175
+ }, { token: gAdminUserToken });
1176
+
1177
+ expect(result.firstName).toBe('AdminUpdated');
1178
+ });
1179
+ });
1180
+
1181
+ describe('Validation', () => {
1182
+ it('should reject invalid phone number format', async () => {
1183
+ const result = await testHelper.graphQl({
1184
+ arguments: {
1185
+ id: gNormalUserId,
1186
+ input: {
1187
+ phone: '123', // Invalid format
1188
+ },
1189
+ },
1190
+ fields: ['phone'],
1191
+ name: 'updateUser',
1192
+ type: TestGraphQLType.MUTATION,
1193
+ }, { token: gNormalUserToken, statusCode: 200 });
1194
+
1195
+ expect(result.errors).toBeDefined();
1196
+ expect(result.errors[0].message).toContain('phone');
1197
+ });
1198
+
1199
+ it('should accept valid German phone formats', async () => {
1200
+ const validPhones = [
1201
+ '+49 123 456789',
1202
+ '+49 (0)123 456789',
1203
+ '0123 456789',
1204
+ ];
1205
+
1206
+ for (const phone of validPhones) {
1207
+ const result = await testHelper.graphQl({
1208
+ arguments: {
1209
+ id: gNormalUserId,
1210
+ input: { phone },
1211
+ },
1212
+ fields: ['phone'],
1213
+ name: 'updateUser',
1214
+ type: TestGraphQLType.MUTATION,
1215
+ }, { token: gNormalUserToken });
1216
+
1217
+ expect(result.phone).toBe(phone);
1218
+ }
1219
+ });
1220
+ });
1221
+ });
1222
+ ```
1223
+
1224
+ ---
1225
+
1226
+ ## Debugging Test Failures
1227
+
1228
+ When your tests fail and error messages are unclear, enable debugging:
1229
+
1230
+ **🔍 TIP: For all TestHelper options, read the source file:**
1231
+
1232
+ ```
1233
+ node_modules/@lenne.tech/nest-server/src/test/test.helper.ts
1234
+ ```
1235
+
1236
+ This file documents all capabilities including `log`, `logError`, file uploads via `attachments`, and more.
1237
+
1238
+ ### TestHelper Debugging Options
1239
+
1240
+ ```typescript
1241
+ // Add to any failing test for detailed output
1242
+ const result = await testHelper.graphQl({
1243
+ arguments: { id: userId },
1244
+ fields: ['id', 'email'],
1245
+ name: 'getUser',
1246
+ type: TestGraphQLType.MUTATION,
1247
+ }, {
1248
+ token: userToken,
1249
+ log: true, // Logs request details to console
1250
+ logError: true, // Logs detailed error information
1251
+ });
1252
+
1253
+ // Or for REST calls
1254
+ const result = await testHelper.rest('/api/endpoint', {
1255
+ method: 'POST',
1256
+ payload: data,
1257
+ token: userToken,
1258
+ log: true,
1259
+ logError: true,
1260
+ });
1261
+ ```
1262
+
1263
+ ### Server-Side Debugging
1264
+
1265
+ **Enable exception logging** in `src/config.env.ts`:
1266
+ ```typescript
1267
+ export default {
1268
+ logExceptions: true, // Shows stack traces for all exceptions
1269
+ // ... other config
1270
+ };
1271
+ ```
1272
+
1273
+ **Enable validation debugging** via environment variable:
1274
+ ```bash
1275
+ # Run tests with validation debugging
1276
+ DEBUG_VALIDATION=true npm test
1277
+ ```
1278
+
1279
+ Or set in your test file:
1280
+ ```typescript
1281
+ beforeAll(async () => {
1282
+ // Enable validation debug logging
1283
+ process.env.DEBUG_VALIDATION = 'true';
1284
+
1285
+ // ... rest of setup
1286
+ });
1287
+ ```
1288
+
1289
+ This enables detailed console.debug output from MapAndValidatePipe (automatically activated via CoreModule - see `node_modules/@lenne.tech/nest-server/src/core/common/pipes/map-and-validate.pipe.ts`).
1290
+
1291
+ ### Full Debugging Setup Example
1292
+
1293
+ ```typescript
1294
+ describe('My Story Test', () => {
1295
+ beforeAll(async () => {
1296
+ // Enable validation debugging
1297
+ process.env.DEBUG_VALIDATION = 'true';
1298
+
1299
+ // ... normal setup
1300
+ });
1301
+
1302
+ it('should debug this failing test', async () => {
1303
+ const result = await testHelper.graphQl({
1304
+ // ... your test config
1305
+ }, {
1306
+ log: true, // Enable request/response logging
1307
+ logError: true, // Enable error logging
1308
+ });
1309
+ });
1310
+ });
1311
+ ```
1312
+
1313
+ **Remember to disable debugging logs before committing** to keep test output clean in CI/CD.
1314
+
1315
+ ### File Upload Testing
1316
+
1317
+ TestHelper supports file uploads via the `attachments` option:
1318
+
1319
+ ```typescript
1320
+ // Upload a single file
1321
+ const result = await testHelper.rest('/api/upload', {
1322
+ method: 'POST',
1323
+ attachments: [
1324
+ { name: 'document', path: '/path/to/document.pdf' },
1325
+ ],
1326
+ token: userToken,
1327
+ });
1328
+
1329
+ // Upload multiple files
1330
+ const result = await testHelper.rest('/api/upload-multiple', {
1331
+ method: 'POST',
1332
+ attachments: [
1333
+ { name: 'avatar', path: '/path/to/avatar.png' },
1334
+ { name: 'resume', path: '/path/to/resume.pdf' },
1335
+ ],
1336
+ token: userToken,
1337
+ });
1338
+ ```
1339
+
1340
+ **See `node_modules/@lenne.tech/nest-server/src/test/test.helper.ts` for all available options.**
1341
+
1342
+ ---
1343
+
1344
+ ## Key Takeaways from Examples
1345
+
1346
+ ### 1. Test Structure
1347
+ - Always setup test data in `beforeAll`
1348
+ - Clean up in `afterAll`
1349
+ - Group related tests in `describe` blocks
1350
+ - Test happy path, validation, authorization separately
1351
+
1352
+ ### 2. Security Testing
1353
+ - Create users with different roles
1354
+ - Test both authorized and unauthorized access
1355
+ - Never weaken security to make tests pass
1356
+ - Test permission boundaries explicitly
1357
+
1358
+ ### 3. Business Logic
1359
+ - Test calculated fields (like totalPrice)
1360
+ - Test side effects (like stock reduction)
1361
+ - Test validation rules thoroughly
1362
+ - Test edge cases and error conditions
1363
+
1364
+ ### 4. Implementation Strategy
1365
+ - Use nest-server-generator for scaffolding
1366
+ - Implement business logic in services
1367
+ - Add custom validation where needed
1368
+ - Follow existing patterns in codebase
1369
+
1370
+ ### 5. Debugging
1371
+ - Use `log: true` and `logError: true` in TestHelper for detailed output
1372
+ - Enable `logExceptions` in config.env.ts for server-side errors
1373
+ - Use `DEBUG_VALIDATION=true` for validation debugging
1374
+ - Disable the debug logs again once all tests have been completed without errors
1375
+
1376
+ ### 6. Iteration
1377
+ - First run will always fail (expected)
1378
+ - Fix failures systematically
1379
+ - Enable debugging when error messages are unclear
1380
+ - Re-run tests after each change
1381
+ - Continue until all tests pass
1382
+
1383
+ Remember: **Tests define the contract, code fulfills the contract.**