@lenne.tech/cli 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/claude/install-plugin.js +339 -0
- package/package.json +1 -1
- package/build/commands/claude/install-commands.js +0 -337
- package/build/commands/claude/install-mcps.js +0 -258
- package/build/commands/claude/install-skills.js +0 -693
- package/build/lib/mcp-registry.js +0 -80
- package/build/templates/claude-commands/code-cleanup.md +0 -82
- package/build/templates/claude-commands/commit-message.md +0 -21
- package/build/templates/claude-commands/create-story.md +0 -435
- package/build/templates/claude-commands/mr-description-clipboard.md +0 -48
- package/build/templates/claude-commands/mr-description.md +0 -33
- package/build/templates/claude-commands/sec-review.md +0 -62
- package/build/templates/claude-commands/skill-optimize.md +0 -481
- package/build/templates/claude-commands/test-generate.md +0 -45
- package/build/templates/claude-skills/building-stories-with-tdd/SKILL.md +0 -265
- package/build/templates/claude-skills/building-stories-with-tdd/code-quality.md +0 -276
- package/build/templates/claude-skills/building-stories-with-tdd/database-indexes.md +0 -182
- package/build/templates/claude-skills/building-stories-with-tdd/examples.md +0 -1383
- package/build/templates/claude-skills/building-stories-with-tdd/handling-existing-tests.md +0 -197
- package/build/templates/claude-skills/building-stories-with-tdd/reference.md +0 -1427
- package/build/templates/claude-skills/building-stories-with-tdd/security-review.md +0 -307
- package/build/templates/claude-skills/building-stories-with-tdd/workflow.md +0 -1004
- package/build/templates/claude-skills/generating-nest-servers/SKILL.md +0 -303
- package/build/templates/claude-skills/generating-nest-servers/configuration.md +0 -285
- package/build/templates/claude-skills/generating-nest-servers/declare-keyword-warning.md +0 -133
- package/build/templates/claude-skills/generating-nest-servers/description-management.md +0 -226
- package/build/templates/claude-skills/generating-nest-servers/examples.md +0 -893
- package/build/templates/claude-skills/generating-nest-servers/framework-guide.md +0 -259
- package/build/templates/claude-skills/generating-nest-servers/quality-review.md +0 -864
- package/build/templates/claude-skills/generating-nest-servers/reference.md +0 -487
- package/build/templates/claude-skills/generating-nest-servers/security-rules.md +0 -371
- package/build/templates/claude-skills/generating-nest-servers/verification-checklist.md +0 -262
- package/build/templates/claude-skills/generating-nest-servers/workflow-process.md +0 -1061
- package/build/templates/claude-skills/using-lt-cli/SKILL.md +0 -284
- package/build/templates/claude-skills/using-lt-cli/examples.md +0 -546
- package/build/templates/claude-skills/using-lt-cli/reference.md +0 -513
|
@@ -1,1061 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: nest-server-generator-workflow
|
|
3
|
-
version: 1.0.0
|
|
4
|
-
description: Complete 7-phase workflow for NestJS module/object generation - from analysis to testing, including SubObject creation, inheritance handling, description management, enum files, and comprehensive API testing with security validation
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Workflow Process
|
|
8
|
-
|
|
9
|
-
## Table of Contents
|
|
10
|
-
- [Phase 1: Analysis & Planning](#phase-1-analysis--planning)
|
|
11
|
-
- [Phase 2: SubObject Creation](#phase-2-subobject-creation)
|
|
12
|
-
- [Phase 3: Module Creation](#phase-3-module-creation)
|
|
13
|
-
- [Phase 4: Inheritance Handling](#phase-4-inheritance-handling)
|
|
14
|
-
- [Phase 5: Description Management](#phase-5-description-management)
|
|
15
|
-
- [Phase 6: Enum File Creation](#phase-6-enum-file-creation)
|
|
16
|
-
- [Phase 7: API Test Creation](#phase-7-api-test-creation)
|
|
17
|
-
|
|
18
|
-
### Phase 1: Analysis & Planning
|
|
19
|
-
|
|
20
|
-
1. **Parse the specification** completely
|
|
21
|
-
2. **Identify all components**:
|
|
22
|
-
- List all SubObjects
|
|
23
|
-
- List all Objects
|
|
24
|
-
- List all Modules
|
|
25
|
-
- Identify inheritance relationships
|
|
26
|
-
- Identify enum types needed
|
|
27
|
-
3. **Create comprehensive todo list** with:
|
|
28
|
-
- Create each SubObject
|
|
29
|
-
- Create each Object
|
|
30
|
-
- Create each Module
|
|
31
|
-
- Handle inheritance modifications
|
|
32
|
-
- Create enum files
|
|
33
|
-
- Create API tests for each module
|
|
34
|
-
- Run tests and verify
|
|
35
|
-
|
|
36
|
-
**Phase 1 Checklist:**
|
|
37
|
-
- [ ] Specification completely parsed
|
|
38
|
-
- [ ] All components identified (SubObjects, Objects, Modules)
|
|
39
|
-
- [ ] Inheritance relationships documented
|
|
40
|
-
- [ ] Enum types listed
|
|
41
|
-
- [ ] Comprehensive todo list created
|
|
42
|
-
- [ ] Ready for Phase 2
|
|
43
|
-
|
|
44
|
-
### Phase 2: SubObject Creation
|
|
45
|
-
|
|
46
|
-
**Create SubObjects in dependency order** (if SubObject A contains SubObject B, create B first):
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
lt server object --name <ObjectName> \
|
|
50
|
-
--prop-name-0 <name> --prop-type-0 <type> \
|
|
51
|
-
--prop-name-1 <name> --prop-type-1 <type> \
|
|
52
|
-
...
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**Apply modifiers**:
|
|
56
|
-
- Optional: `--prop-nullable-X true`
|
|
57
|
-
- Array: `--prop-array-X true`
|
|
58
|
-
- Enum: `--prop-enum-X <EnumName>`
|
|
59
|
-
- Schema: `--prop-schema-X <SchemaName>`
|
|
60
|
-
|
|
61
|
-
**Phase 2 Checklist:**
|
|
62
|
-
- [ ] All SubObjects created in correct dependency order
|
|
63
|
-
- [ ] All modifiers applied (nullable, array, enum, schema)
|
|
64
|
-
- [ ] Properties in alphabetical order
|
|
65
|
-
- [ ] No circular dependencies
|
|
66
|
-
- [ ] Ready for Phase 3
|
|
67
|
-
|
|
68
|
-
### Phase 3: Module Creation
|
|
69
|
-
|
|
70
|
-
**Create modules with all properties**:
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
lt server module --name <ModuleName> --controller <Rest|GraphQL|Both> \
|
|
74
|
-
--prop-name-0 <name> --prop-type-0 <type> \
|
|
75
|
-
--prop-name-1 <name> --prop-type-1 <type> \
|
|
76
|
-
...
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**For references to other modules**:
|
|
80
|
-
```bash
|
|
81
|
-
--prop-name-X author --prop-type-X ObjectId --prop-reference-X User
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**For embedded objects**:
|
|
85
|
-
```bash
|
|
86
|
-
--prop-name-X address --prop-schema-X Address
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**Phase 3 Checklist:**
|
|
90
|
-
- [ ] All modules created with correct properties
|
|
91
|
-
- [ ] References correctly set (ObjectId with --prop-reference-X)
|
|
92
|
-
- [ ] Embedded objects correctly referenced (--prop-schema-X)
|
|
93
|
-
- [ ] Properties in alphabetical order
|
|
94
|
-
- [ ] All required imports present
|
|
95
|
-
- [ ] Ready for Phase 4
|
|
96
|
-
|
|
97
|
-
### Phase 4: Inheritance Handling
|
|
98
|
-
|
|
99
|
-
When a model extends another model (e.g., `Extends: Profile`):
|
|
100
|
-
|
|
101
|
-
1. **Identify parent model location**:
|
|
102
|
-
- Core models (from @lenne.tech/nest-server): CoreModel, CorePersisted, etc.
|
|
103
|
-
- Custom parent models: Need to find in project
|
|
104
|
-
|
|
105
|
-
2. **For Core parent models**:
|
|
106
|
-
- Replace in model file: `extends CoreModel` → `extends ParentModel`
|
|
107
|
-
- Import: `import { ParentModel } from './path'`
|
|
108
|
-
|
|
109
|
-
3. **For custom parent models (objects/other modules)**:
|
|
110
|
-
- Model extends parent object: Import and extend
|
|
111
|
-
- Input files must include parent properties
|
|
112
|
-
|
|
113
|
-
4. **Input/Output inheritance**:
|
|
114
|
-
- **CreateInput**: Must include ALL required properties from parent AND model
|
|
115
|
-
- **UpdateInput**: Include all properties as optional
|
|
116
|
-
- Check parent's CreateInput for required fields
|
|
117
|
-
- Copy required fields to child's CreateInput
|
|
118
|
-
|
|
119
|
-
**Example**: If `BuyerProfile` extends `Profile`:
|
|
120
|
-
```typescript
|
|
121
|
-
// buyer-profile.model.ts
|
|
122
|
-
import { Profile } from '../../common/objects/profile/profile.object';
|
|
123
|
-
export class BuyerProfile extends Profile { ... }
|
|
124
|
-
|
|
125
|
-
// buyer-profile-create.input.ts
|
|
126
|
-
// Must include ALL required fields from Profile's create input + BuyerProfile fields
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**Phase 4 Checklist:**
|
|
130
|
-
- [ ] All parent models identified (Core or custom)
|
|
131
|
-
- [ ] Model extends correct parent class
|
|
132
|
-
- [ ] Imports updated correctly
|
|
133
|
-
- [ ] CreateInput includes ALL parent required fields
|
|
134
|
-
- [ ] UpdateInput includes all properties as optional
|
|
135
|
-
- [ ] No missing required fields
|
|
136
|
-
- [ ] Ready for Phase 5
|
|
137
|
-
|
|
138
|
-
### Phase 5: Description Management
|
|
139
|
-
|
|
140
|
-
**⚠️ CRITICAL PHASE - Refer to "CRITICAL: DESCRIPTION MANAGEMENT" section at the top of this document!**
|
|
141
|
-
|
|
142
|
-
This phase is often done incorrectly. Follow these steps EXACTLY:
|
|
143
|
-
|
|
144
|
-
#### Step 5.1: Extract Descriptions from User Input
|
|
145
|
-
|
|
146
|
-
**BEFORE applying any descriptions, review the original specification:**
|
|
147
|
-
|
|
148
|
-
Go back to the user's original specification and extract ALL comments that appear after `//`:
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
Module: Product
|
|
152
|
-
- name: string // Product name
|
|
153
|
-
- price: number // Produktpreis
|
|
154
|
-
- description?: string // Produktbeschreibung
|
|
155
|
-
- stock: number // Current inventory
|
|
156
|
-
|
|
157
|
-
SubObject: Address
|
|
158
|
-
- street: string // Straße
|
|
159
|
-
- city: string // City name
|
|
160
|
-
- zipCode: string // Postleitzahl
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
**Create a mapping**:
|
|
164
|
-
```
|
|
165
|
-
Product.name → "Product name" (English)
|
|
166
|
-
Product.price → "Produktpreis" (German)
|
|
167
|
-
Product.description → "Produktbeschreibung" (German)
|
|
168
|
-
Product.stock → "Current inventory" (English)
|
|
169
|
-
Address.street → "Straße" (German)
|
|
170
|
-
Address.city → "City name" (English)
|
|
171
|
-
Address.zipCode → "Postleitzahl" (German)
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
#### Step 5.2: Format Descriptions
|
|
175
|
-
|
|
176
|
-
**Rule**: `"ENGLISH_DESCRIPTION (DEUTSCHE_BESCHREIBUNG)"`
|
|
177
|
-
|
|
178
|
-
Apply formatting rules:
|
|
179
|
-
|
|
180
|
-
1. **If comment is in English**:
|
|
181
|
-
```
|
|
182
|
-
// Product name
|
|
183
|
-
```
|
|
184
|
-
→ Use as: `description: 'Product name'`
|
|
185
|
-
|
|
186
|
-
Fix typos if needed:
|
|
187
|
-
```
|
|
188
|
-
// Prodcut name (typo)
|
|
189
|
-
```
|
|
190
|
-
→ Use as: `description: 'Product name'` (typo corrected)
|
|
191
|
-
|
|
192
|
-
2. **If comment is in German**:
|
|
193
|
-
```
|
|
194
|
-
// Produktpreis
|
|
195
|
-
```
|
|
196
|
-
→ Translate and add original: `description: 'Product price (Produktpreis)'`
|
|
197
|
-
|
|
198
|
-
```
|
|
199
|
-
// Straße
|
|
200
|
-
```
|
|
201
|
-
→ Translate and add original: `description: 'Street (Straße)'`
|
|
202
|
-
|
|
203
|
-
Fix typos in original:
|
|
204
|
-
```
|
|
205
|
-
// Postleizahl (typo: missing 't')
|
|
206
|
-
```
|
|
207
|
-
→ Translate and add corrected: `description: 'Postal code (Postleitzahl)'`
|
|
208
|
-
|
|
209
|
-
3. **If no comment provided**:
|
|
210
|
-
→ Create meaningful English description: `description: 'User email address'`
|
|
211
|
-
|
|
212
|
-
**⚠️ CRITICAL - Preserve Original Wording**:
|
|
213
|
-
|
|
214
|
-
- ✅ **DO:** Fix spelling/typos only
|
|
215
|
-
- ❌ **DON'T:** Rephrase, expand, or improve wording
|
|
216
|
-
- ❌ **DON'T:** Change terms (they may be predefined/referenced by external systems)
|
|
217
|
-
|
|
218
|
-
**Examples**:
|
|
219
|
-
```
|
|
220
|
-
✅ CORRECT:
|
|
221
|
-
// Straße → 'Street (Straße)' (preserve word)
|
|
222
|
-
// Produkt → 'Product (Produkt)' (don't add "name")
|
|
223
|
-
// Status → 'Status (Status)' (same in both languages)
|
|
224
|
-
|
|
225
|
-
❌ WRONG:
|
|
226
|
-
// Straße → 'Street name (Straßenname)' (changed word!)
|
|
227
|
-
// Produkt → 'Product name (Produktname)' (added word!)
|
|
228
|
-
// Status → 'Current status (Aktueller Status)' (added word!)
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
#### Step 5.3: Apply Descriptions EVERYWHERE
|
|
232
|
-
|
|
233
|
-
**🚨 MOST IMPORTANT: Apply SAME description to ALL files!**
|
|
234
|
-
|
|
235
|
-
For **EVERY property in EVERY Module**:
|
|
236
|
-
|
|
237
|
-
1. Open `<module>.model.ts` → Add description to property
|
|
238
|
-
2. Open `inputs/<module>-create.input.ts` → Add SAME description to property
|
|
239
|
-
3. Open `inputs/<module>.input.ts` → Add SAME description to property
|
|
240
|
-
|
|
241
|
-
For **EVERY property in EVERY SubObject**:
|
|
242
|
-
|
|
243
|
-
1. Open `objects/<object>/<object>.object.ts` → Add description to property
|
|
244
|
-
2. Open `objects/<object>/<object>-create.input.ts` → Add SAME description to property
|
|
245
|
-
3. Open `objects/<object>/<object>.input.ts` → Add SAME description to property
|
|
246
|
-
|
|
247
|
-
**Example for Module "Product" with property "price"**:
|
|
248
|
-
|
|
249
|
-
```typescript
|
|
250
|
-
// File: src/server/modules/product/product.model.ts
|
|
251
|
-
@UnifiedField({ description: 'Product price (Produktpreis)' })
|
|
252
|
-
price: number;
|
|
253
|
-
|
|
254
|
-
// File: src/server/modules/product/inputs/product-create.input.ts
|
|
255
|
-
@UnifiedField({ description: 'Product price (Produktpreis)' })
|
|
256
|
-
price: number;
|
|
257
|
-
|
|
258
|
-
// File: src/server/modules/product/inputs/product.input.ts
|
|
259
|
-
@UnifiedField({ description: 'Product price (Produktpreis)' })
|
|
260
|
-
price?: number;
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
**Example for SubObject "Address" with property "street"**:
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
// File: src/server/common/objects/address/address.object.ts
|
|
267
|
-
@UnifiedField({ description: 'Street (Straße)' })
|
|
268
|
-
street: string;
|
|
269
|
-
|
|
270
|
-
// File: src/server/common/objects/address/address-create.input.ts
|
|
271
|
-
@UnifiedField({ description: 'Street (Straße)' })
|
|
272
|
-
street: string;
|
|
273
|
-
|
|
274
|
-
// File: src/server/common/objects/address/address.input.ts
|
|
275
|
-
@UnifiedField({ description: 'Street (Straße)' })
|
|
276
|
-
street?: string;
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
#### Step 5.4: Add Class-Level Descriptions
|
|
280
|
-
|
|
281
|
-
Also add descriptions to the `@ObjectType()` and `@InputType()` decorators:
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
@ObjectType({ description: 'Product entity (Produkt-Entität)' })
|
|
285
|
-
export class Product extends CoreModel { ... }
|
|
286
|
-
|
|
287
|
-
@InputType({ description: 'Product creation data (Produkt-Erstellungsdaten)' })
|
|
288
|
-
export class ProductCreateInput { ... }
|
|
289
|
-
|
|
290
|
-
@InputType({ description: 'Product update data (Produkt-Aktualisierungsdaten)' })
|
|
291
|
-
export class ProductInput { ... }
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
#### Step 5.5: Verify Consistency
|
|
295
|
-
|
|
296
|
-
After applying all descriptions, verify:
|
|
297
|
-
|
|
298
|
-
- [ ] All user-provided comments extracted and processed
|
|
299
|
-
- [ ] All German descriptions translated to format: `ENGLISH (DEUTSCH)`
|
|
300
|
-
- [ ] All English descriptions kept as-is
|
|
301
|
-
- [ ] Module Model has descriptions on all properties
|
|
302
|
-
- [ ] Module CreateInput has SAME descriptions on all properties
|
|
303
|
-
- [ ] Module UpdateInput has SAME descriptions on all properties
|
|
304
|
-
- [ ] SubObject has descriptions on all properties
|
|
305
|
-
- [ ] SubObject CreateInput has SAME descriptions on all properties
|
|
306
|
-
- [ ] SubObject UpdateInput has SAME descriptions on all properties
|
|
307
|
-
- [ ] Class-level decorators have descriptions
|
|
308
|
-
- [ ] NO inconsistencies (same property, different descriptions)
|
|
309
|
-
|
|
310
|
-
**If ANY checkbox is unchecked, STOP and fix before continuing to Phase 6!**
|
|
311
|
-
|
|
312
|
-
**Phase 5 Checklist:**
|
|
313
|
-
- [ ] All user-provided comments extracted and processed
|
|
314
|
-
- [ ] All German descriptions translated to format: ENGLISH (DEUTSCH)
|
|
315
|
-
- [ ] All English descriptions kept as-is (typos fixed only)
|
|
316
|
-
- [ ] Descriptions applied to ALL Model properties
|
|
317
|
-
- [ ] Descriptions applied to ALL CreateInput properties
|
|
318
|
-
- [ ] Descriptions applied to ALL UpdateInput properties
|
|
319
|
-
- [ ] Descriptions applied to ALL SubObject properties
|
|
320
|
-
- [ ] Class-level decorators have descriptions
|
|
321
|
-
- [ ] NO inconsistencies (same property different descriptions)
|
|
322
|
-
- [ ] Ready for Phase 6
|
|
323
|
-
|
|
324
|
-
### Phase 6: Enum File Creation
|
|
325
|
-
|
|
326
|
-
For each enum used, create enum file manually:
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
// src/server/common/enums/status.enum.ts
|
|
330
|
-
export enum StatusEnum {
|
|
331
|
-
PENDING = 'PENDING',
|
|
332
|
-
ACTIVE = 'ACTIVE',
|
|
333
|
-
COMPLETED = 'COMPLETED',
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
**Naming convention**:
|
|
338
|
-
- File: `kebab-case.enum.ts`
|
|
339
|
-
- Enum: `PascalCaseEnum`
|
|
340
|
-
- Values: `UPPER_SNAKE_CASE`
|
|
341
|
-
|
|
342
|
-
**Phase 6 Checklist:**
|
|
343
|
-
- [ ] All enum files created in src/server/common/enums/
|
|
344
|
-
- [ ] File naming follows kebab-case.enum.ts
|
|
345
|
-
- [ ] Enum naming follows PascalCaseEnum
|
|
346
|
-
- [ ] Values follow UPPER_SNAKE_CASE
|
|
347
|
-
- [ ] All enums properly imported where used
|
|
348
|
-
- [ ] Ready for Phase 7
|
|
349
|
-
|
|
350
|
-
### Phase 7: API Test Creation
|
|
351
|
-
|
|
352
|
-
**⚠️ CRITICAL: Test Type Requirement**
|
|
353
|
-
|
|
354
|
-
**ONLY create API tests using TestHelper - NEVER create direct Service tests!**
|
|
355
|
-
|
|
356
|
-
- ✅ **DO:** Create tests that call REST endpoints or GraphQL queries/mutations using `TestHelper`
|
|
357
|
-
- ✅ **DO:** Test through the API layer (Controller/Resolver → Service → Database)
|
|
358
|
-
- ❌ **DON'T:** Create tests that directly instantiate or call Service methods
|
|
359
|
-
- ❌ **DON'T:** Create unit tests for Services (e.g., `user.service.spec.ts`)
|
|
360
|
-
- ❌ **DON'T:** Mock dependencies or bypass the API layer
|
|
361
|
-
|
|
362
|
-
**Why API tests only?**
|
|
363
|
-
- API tests validate the complete security model (decorators, guards, permissions)
|
|
364
|
-
- Direct Service tests bypass authentication and authorization checks
|
|
365
|
-
- TestHelper provides all necessary tools for comprehensive API testing
|
|
366
|
-
|
|
367
|
-
**Exception: Direct database/service access for test setup/cleanup ONLY**
|
|
368
|
-
|
|
369
|
-
Direct database or service access is ONLY allowed for:
|
|
370
|
-
|
|
371
|
-
- ✅ **Test Setup (beforeAll/beforeEach)**:
|
|
372
|
-
- Setting user roles in database: `await db.collection('users').updateOne({ _id: userId }, { $set: { roles: ['admin'] } })`
|
|
373
|
-
- Setting verified flag: `await db.collection('users').updateOne({ _id: userId }, { $set: { verified: true } })`
|
|
374
|
-
- Creating prerequisite test data that can't be created via API
|
|
375
|
-
|
|
376
|
-
- ✅ **Test Cleanup (afterAll/afterEach)**:
|
|
377
|
-
- Deleting test objects: `await db.collection('products').deleteMany({ createdBy: testUserId })`
|
|
378
|
-
- Cleaning up test data: `await db.collection('users').deleteOne({ email: 'test@example.com' })`
|
|
379
|
-
|
|
380
|
-
- ❌ **NEVER for testing functionality**:
|
|
381
|
-
- Don't call `userService.create()` to test user creation - use API endpoint!
|
|
382
|
-
- Don't call `productService.update()` to test updates - use API endpoint!
|
|
383
|
-
- Don't access database to verify results - query via API instead!
|
|
384
|
-
|
|
385
|
-
**Example of correct usage:**
|
|
386
|
-
|
|
387
|
-
```typescript
|
|
388
|
-
describe('Product Tests', () => {
|
|
389
|
-
let adminToken: string;
|
|
390
|
-
let userId: string;
|
|
391
|
-
|
|
392
|
-
beforeAll(async () => {
|
|
393
|
-
// ✅ ALLOWED: Direct DB access for setup
|
|
394
|
-
const user = await testHelper.rest('/auth/signup', {
|
|
395
|
-
method: 'POST',
|
|
396
|
-
payload: { email: 'admin@test.com', password: 'password' }
|
|
397
|
-
});
|
|
398
|
-
userId = user.id;
|
|
399
|
-
|
|
400
|
-
// ✅ ALLOWED: Direct DB manipulation for test setup
|
|
401
|
-
await db.collection('users').updateOne(
|
|
402
|
-
{ _id: new ObjectId(userId) },
|
|
403
|
-
{ $set: { roles: ['admin'], verified: true } }
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
// Get token via API
|
|
407
|
-
const auth = await testHelper.rest('/auth/signin', {
|
|
408
|
-
method: 'POST',
|
|
409
|
-
payload: { email: 'admin@test.com', password: 'password' }
|
|
410
|
-
});
|
|
411
|
-
adminToken = auth.token;
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it('should create product', async () => {
|
|
415
|
-
// ✅ CORRECT: Test via API
|
|
416
|
-
const result = await testHelper.rest('/api/products', {
|
|
417
|
-
method: 'POST',
|
|
418
|
-
payload: { name: 'Test Product' },
|
|
419
|
-
token: adminToken
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
expect(result.name).toBe('Test Product');
|
|
423
|
-
|
|
424
|
-
// ❌ WRONG: Don't verify via DB
|
|
425
|
-
// const dbProduct = await db.collection('products').findOne({ _id: result.id });
|
|
426
|
-
|
|
427
|
-
// ✅ CORRECT: Verify via API
|
|
428
|
-
const fetched = await testHelper.rest(`/api/products/${result.id}`, {
|
|
429
|
-
method: 'GET',
|
|
430
|
-
token: adminToken
|
|
431
|
-
});
|
|
432
|
-
expect(fetched.name).toBe('Test Product');
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
afterAll(async () => {
|
|
436
|
-
// ✅ ALLOWED: Direct DB access for cleanup
|
|
437
|
-
await db.collection('products').deleteMany({ createdBy: userId });
|
|
438
|
-
await db.collection('users').deleteOne({ _id: new ObjectId(userId) });
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
**⚠️ CRITICAL: Test Creation Process**
|
|
446
|
-
|
|
447
|
-
Creating API tests is NOT just about testing functionality - it's about **validating the security model**. You MUST follow this exact process:
|
|
448
|
-
|
|
449
|
-
---
|
|
450
|
-
|
|
451
|
-
#### Step 1: 🔍 MANDATORY Permission Analysis (BEFORE writing ANY test)
|
|
452
|
-
|
|
453
|
-
**YOU MUST analyze these THREE layers BEFORE writing a single test:**
|
|
454
|
-
|
|
455
|
-
1. **Controller/Resolver Layer** - Check `@Roles()` decorator:
|
|
456
|
-
```typescript
|
|
457
|
-
// In product.resolver.ts
|
|
458
|
-
@Roles(RoleEnum.S_EVERYONE) // ← WHO can call this?
|
|
459
|
-
@Query(() => [Product])
|
|
460
|
-
async getProducts() { ... }
|
|
461
|
-
|
|
462
|
-
@Roles(RoleEnum.S_USER) // ← All signed-in users
|
|
463
|
-
@Mutation(() => Product)
|
|
464
|
-
async createProduct(@Args('input') input: ProductCreateInput) { ... }
|
|
465
|
-
|
|
466
|
-
@Roles(RoleEnum.ADMIN, RoleEnum.S_CREATOR) // ← Only admin or creator
|
|
467
|
-
@Mutation(() => Product)
|
|
468
|
-
async updateProduct(@Args('id') id: string, @Args('input') input: ProductInput) { ... }
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
2. **Model Layer** - Check `@Restricted()` and `securityCheck()`:
|
|
472
|
-
```typescript
|
|
473
|
-
// In product.model.ts
|
|
474
|
-
export class Product extends CoreModel {
|
|
475
|
-
securityCheck(user: User, force?: boolean) {
|
|
476
|
-
if (force || user?.hasRole(RoleEnum.ADMIN)) {
|
|
477
|
-
return this; // Admin sees all
|
|
478
|
-
}
|
|
479
|
-
if (this.isPublic) {
|
|
480
|
-
return this; // Everyone sees public products
|
|
481
|
-
}
|
|
482
|
-
if (!equalIds(user, this.createdBy)) {
|
|
483
|
-
return undefined; // Non-creator gets nothing
|
|
484
|
-
}
|
|
485
|
-
return this; // Creator sees own products
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
3. **Service Layer** - Check `serviceOptions.roles` usage:
|
|
491
|
-
```typescript
|
|
492
|
-
// In product.service.ts
|
|
493
|
-
async update(id: string, input: ProductInput, serviceOptions?: ServiceOptions) {
|
|
494
|
-
// Check if user has ADMIN or S_CREATOR role
|
|
495
|
-
// ...
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
**Permission Analysis Checklist:**
|
|
500
|
-
- [ ] I have checked ALL `@Roles()` decorators in controller/resolver
|
|
501
|
-
- [ ] I have read the complete `securityCheck()` method in the model
|
|
502
|
-
- [ ] I have checked ALL `@Restricted()` decorators
|
|
503
|
-
- [ ] I understand WHO can CREATE (usually S_USER or ADMIN)
|
|
504
|
-
- [ ] I understand WHO can READ (S_USER + securityCheck filtering)
|
|
505
|
-
- [ ] I understand WHO can UPDATE (usually ADMIN + S_CREATOR)
|
|
506
|
-
- [ ] I understand WHO can DELETE (usually ADMIN + S_CREATOR)
|
|
507
|
-
|
|
508
|
-
**Common Permission Patterns:**
|
|
509
|
-
- `S_EVERYONE` → No authentication required
|
|
510
|
-
- `S_USER` → Any signed-in user
|
|
511
|
-
- `ADMIN` → User with 'admin' role
|
|
512
|
-
- `S_CREATOR` → User who created the resource (user.id === object.createdBy)
|
|
513
|
-
|
|
514
|
-
---
|
|
515
|
-
|
|
516
|
-
#### Step 2: 🎯 Apply Principle of Least Privilege
|
|
517
|
-
|
|
518
|
-
**GOLDEN RULE**: Always test with the **LEAST privileged user** who is still authorized.
|
|
519
|
-
|
|
520
|
-
**Decision Tree:**
|
|
521
|
-
|
|
522
|
-
```
|
|
523
|
-
Is endpoint marked with @Roles(RoleEnum.S_EVERYONE)?
|
|
524
|
-
├─ YES → Test WITHOUT token (unauthenticated)
|
|
525
|
-
└─ NO → Is endpoint marked with @Roles(RoleEnum.S_USER)?
|
|
526
|
-
├─ YES → Test WITH regular user token (NOT admin, NOT creator)
|
|
527
|
-
└─ NO → Is endpoint marked with @Roles(RoleEnum.ADMIN, RoleEnum.S_CREATOR)?
|
|
528
|
-
├─ For UPDATE/DELETE → Test WITH creator token (user who created it)
|
|
529
|
-
└─ For ADMIN-only → Test WITH admin token
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
**❌ WRONG Approach:**
|
|
533
|
-
```typescript
|
|
534
|
-
// BAD: Using admin for everything
|
|
535
|
-
it('should create product', async () => {
|
|
536
|
-
const result = await testHelper.graphQl({
|
|
537
|
-
name: 'createProduct',
|
|
538
|
-
type: TestGraphQLType.MUTATION,
|
|
539
|
-
arguments: { input: { name: 'Test' } },
|
|
540
|
-
fields: ['id']
|
|
541
|
-
}, { token: adminToken }); // ❌ WRONG - Over-privileged!
|
|
542
|
-
});
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
**✅ CORRECT Approach:**
|
|
546
|
-
```typescript
|
|
547
|
-
// GOOD: Using least privileged user
|
|
548
|
-
it('should create product as regular user', async () => {
|
|
549
|
-
const result = await testHelper.graphQl({
|
|
550
|
-
name: 'createProduct',
|
|
551
|
-
type: TestGraphQLType.MUTATION,
|
|
552
|
-
arguments: { input: { name: 'Test' } },
|
|
553
|
-
fields: ['id']
|
|
554
|
-
}, { token: userToken }); // ✅ CORRECT - S_USER is enough!
|
|
555
|
-
});
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
---
|
|
559
|
-
|
|
560
|
-
#### Step 3: 📋 Create Test User Matrix
|
|
561
|
-
|
|
562
|
-
Based on your permission analysis, create test users:
|
|
563
|
-
|
|
564
|
-
```typescript
|
|
565
|
-
describe('Product API', () => {
|
|
566
|
-
let testHelper: TestHelper;
|
|
567
|
-
|
|
568
|
-
// Create users based on ACTUAL needs (not all of them!)
|
|
569
|
-
let noToken: undefined; // For S_EVERYONE endpoints
|
|
570
|
-
let userToken: string; // For S_USER endpoints
|
|
571
|
-
let creatorToken: string; // For S_CREATOR (will create test data)
|
|
572
|
-
let otherUserToken: string; // For testing "not creator" scenarios
|
|
573
|
-
let adminToken: string; // Only if ADMIN-specific endpoints exist
|
|
574
|
-
|
|
575
|
-
let createdProductId: string;
|
|
576
|
-
|
|
577
|
-
beforeAll(async () => {
|
|
578
|
-
testHelper = new TestHelper(app);
|
|
579
|
-
|
|
580
|
-
// Only create users you ACTUALLY need based on @Roles() analysis!
|
|
581
|
-
|
|
582
|
-
// Regular user (for S_USER endpoints)
|
|
583
|
-
const userAuth = await testHelper.graphQl({
|
|
584
|
-
name: 'signUp',
|
|
585
|
-
type: TestGraphQLType.MUTATION,
|
|
586
|
-
arguments: {
|
|
587
|
-
input: {
|
|
588
|
-
email: 'user@test.com',
|
|
589
|
-
password: 'password',
|
|
590
|
-
roles: ['user'] // Regular user, no special privileges
|
|
591
|
-
}
|
|
592
|
-
},
|
|
593
|
-
fields: ['token', 'user { id }']
|
|
594
|
-
});
|
|
595
|
-
userToken = userAuth.token;
|
|
596
|
-
|
|
597
|
-
// Creator user (will create test objects)
|
|
598
|
-
const creatorAuth = await testHelper.graphQl({
|
|
599
|
-
name: 'signUp',
|
|
600
|
-
type: TestGraphQLType.MUTATION,
|
|
601
|
-
arguments: {
|
|
602
|
-
input: {
|
|
603
|
-
email: 'creator@test.com',
|
|
604
|
-
password: 'password',
|
|
605
|
-
roles: ['user']
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
fields: ['token', 'user { id }']
|
|
609
|
-
});
|
|
610
|
-
creatorToken = creatorAuth.token;
|
|
611
|
-
|
|
612
|
-
// Other user (to test "not creator" scenarios)
|
|
613
|
-
const otherUserAuth = await testHelper.graphQl({
|
|
614
|
-
name: 'signUp',
|
|
615
|
-
type: TestGraphQLType.MUTATION,
|
|
616
|
-
arguments: {
|
|
617
|
-
input: {
|
|
618
|
-
email: 'other@test.com',
|
|
619
|
-
password: 'password',
|
|
620
|
-
roles: ['user']
|
|
621
|
-
}
|
|
622
|
-
},
|
|
623
|
-
fields: ['token', 'user { id }']
|
|
624
|
-
});
|
|
625
|
-
otherUserToken = otherUserAuth.token;
|
|
626
|
-
|
|
627
|
-
// Admin user (ONLY if truly needed!)
|
|
628
|
-
const adminAuth = await testHelper.graphQl({
|
|
629
|
-
name: 'signUp',
|
|
630
|
-
type: TestGraphQLType.MUTATION,
|
|
631
|
-
arguments: {
|
|
632
|
-
input: {
|
|
633
|
-
email: 'admin@test.com',
|
|
634
|
-
password: 'password',
|
|
635
|
-
roles: ['admin', 'user'] // ← 'admin' role!
|
|
636
|
-
}
|
|
637
|
-
},
|
|
638
|
-
fields: ['token']
|
|
639
|
-
});
|
|
640
|
-
adminToken = adminAuth.token;
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
afterAll(async () => {
|
|
644
|
-
// Clean up with appropriate privileged user
|
|
645
|
-
if (createdProductId) {
|
|
646
|
-
// Use creator or admin token for cleanup
|
|
647
|
-
await testHelper.graphQl({
|
|
648
|
-
name: 'deleteProduct',
|
|
649
|
-
type: TestGraphQLType.MUTATION,
|
|
650
|
-
arguments: { id: createdProductId },
|
|
651
|
-
fields: ['id']
|
|
652
|
-
}, { token: creatorToken });
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
});
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
---
|
|
659
|
-
|
|
660
|
-
#### Step 4: ✅ Write Tests with Correct Privileges
|
|
661
|
-
|
|
662
|
-
**Example 1: S_EVERYONE endpoint (public access)**
|
|
663
|
-
|
|
664
|
-
```typescript
|
|
665
|
-
// Endpoint: @Roles(RoleEnum.S_EVERYONE)
|
|
666
|
-
describe('Public Endpoints', () => {
|
|
667
|
-
it('should get public products WITHOUT token', async () => {
|
|
668
|
-
const result = await testHelper.graphQl({
|
|
669
|
-
name: 'getPublicProducts',
|
|
670
|
-
type: TestGraphQLType.QUERY,
|
|
671
|
-
fields: ['id', 'name', 'price']
|
|
672
|
-
}); // ← NO TOKEN! S_EVERYONE means unauthenticated is OK
|
|
673
|
-
|
|
674
|
-
expect(result).toBeDefined();
|
|
675
|
-
expect(Array.isArray(result)).toBe(true);
|
|
676
|
-
});
|
|
677
|
-
});
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
**Example 2: S_USER endpoint (any authenticated user)**
|
|
681
|
-
|
|
682
|
-
```typescript
|
|
683
|
-
// Endpoint: @Roles(RoleEnum.S_USER)
|
|
684
|
-
describe('Create Product', () => {
|
|
685
|
-
it('should create product as regular user', async () => {
|
|
686
|
-
const result = await testHelper.graphQl({
|
|
687
|
-
name: 'createProduct',
|
|
688
|
-
type: TestGraphQLType.MUTATION,
|
|
689
|
-
arguments: { input: { name: 'Test Product', price: 10 } },
|
|
690
|
-
fields: ['id', 'name', 'price', 'createdBy']
|
|
691
|
-
}, { token: userToken }); // ← Regular user, NOT admin!
|
|
692
|
-
|
|
693
|
-
expect(result).toBeDefined();
|
|
694
|
-
expect(result.name).toBe('Test Product');
|
|
695
|
-
createdProductId = result.id;
|
|
696
|
-
|
|
697
|
-
// Verify creator is set
|
|
698
|
-
expect(result.createdBy).toBe(userAuth.user.id);
|
|
699
|
-
});
|
|
700
|
-
});
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
**Example 3: UPDATE - S_CREATOR or ADMIN**
|
|
704
|
-
|
|
705
|
-
```typescript
|
|
706
|
-
// Endpoint: @Roles(RoleEnum.ADMIN, RoleEnum.S_CREATOR)
|
|
707
|
-
describe('Update Product', () => {
|
|
708
|
-
it('should update product as creator', async () => {
|
|
709
|
-
// First, creator creates a product
|
|
710
|
-
const created = await testHelper.graphQl({
|
|
711
|
-
name: 'createProduct',
|
|
712
|
-
type: TestGraphQLType.MUTATION,
|
|
713
|
-
arguments: { input: { name: 'Original', price: 10 } },
|
|
714
|
-
fields: ['id', 'name']
|
|
715
|
-
}, { token: creatorToken });
|
|
716
|
-
|
|
717
|
-
// Then, same creator updates it
|
|
718
|
-
const result = await testHelper.graphQl({
|
|
719
|
-
name: 'updateProduct',
|
|
720
|
-
type: TestGraphQLType.MUTATION,
|
|
721
|
-
arguments: {
|
|
722
|
-
id: created.id,
|
|
723
|
-
input: { name: 'Updated' }
|
|
724
|
-
},
|
|
725
|
-
fields: ['id', 'name']
|
|
726
|
-
}, { token: creatorToken }); // ← Use CREATOR token (least privilege!)
|
|
727
|
-
|
|
728
|
-
expect(result.name).toBe('Updated');
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
it('should update any product as admin', async () => {
|
|
732
|
-
// Admin can update products they did NOT create
|
|
733
|
-
const result = await testHelper.graphQl({
|
|
734
|
-
name: 'updateProduct',
|
|
735
|
-
type: TestGraphQLType.MUTATION,
|
|
736
|
-
arguments: {
|
|
737
|
-
id: createdProductId, // Created by different user
|
|
738
|
-
input: { name: 'Admin Updated' }
|
|
739
|
-
},
|
|
740
|
-
fields: ['id', 'name']
|
|
741
|
-
}, { token: adminToken }); // ← Admin needed for other's products
|
|
742
|
-
|
|
743
|
-
expect(result.name).toBe('Admin Updated');
|
|
744
|
-
});
|
|
745
|
-
});
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
---
|
|
749
|
-
|
|
750
|
-
#### Step 5: 🛡️ MANDATORY: Test Permission Failures
|
|
751
|
-
|
|
752
|
-
**CRITICAL**: You MUST test that unauthorized users are BLOCKED. This validates the security model.
|
|
753
|
-
|
|
754
|
-
```typescript
|
|
755
|
-
describe('Security Validation', () => {
|
|
756
|
-
describe('Unauthorized Access', () => {
|
|
757
|
-
it('should FAIL to create product without authentication', async () => {
|
|
758
|
-
// @Roles(RoleEnum.S_USER) requires authentication
|
|
759
|
-
const result = await testHelper.graphQl({
|
|
760
|
-
name: 'createProduct',
|
|
761
|
-
type: TestGraphQLType.MUTATION,
|
|
762
|
-
arguments: { input: { name: 'Hack', price: 1 } },
|
|
763
|
-
fields: ['id']
|
|
764
|
-
}, { statusCode: 401 }); // ← NO TOKEN = should fail with 401
|
|
765
|
-
|
|
766
|
-
expect(result.errors).toBeDefined();
|
|
767
|
-
expect(result.errors[0].message).toContain('Unauthorized');
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
it('should FAIL to update product as non-creator', async () => {
|
|
771
|
-
// @Roles(RoleEnum.ADMIN, RoleEnum.S_CREATOR)
|
|
772
|
-
const result = await testHelper.graphQl({
|
|
773
|
-
name: 'updateProduct',
|
|
774
|
-
type: TestGraphQLType.MUTATION,
|
|
775
|
-
arguments: {
|
|
776
|
-
id: createdProductId, // Created by creatorUser
|
|
777
|
-
input: { name: 'Hacked' }
|
|
778
|
-
},
|
|
779
|
-
fields: ['id']
|
|
780
|
-
}, { token: otherUserToken, statusCode: 403 }); // ← Different user = should fail with 403
|
|
781
|
-
|
|
782
|
-
expect(result.errors).toBeDefined();
|
|
783
|
-
expect(result.errors[0].message).toContain('Forbidden');
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
it('should FAIL to delete product as non-creator', async () => {
|
|
787
|
-
const result = await testHelper.graphQl({
|
|
788
|
-
name: 'deleteProduct',
|
|
789
|
-
type: TestGraphQLType.MUTATION,
|
|
790
|
-
arguments: { id: createdProductId },
|
|
791
|
-
fields: ['id']
|
|
792
|
-
}, { token: otherUserToken, statusCode: 403 });
|
|
793
|
-
|
|
794
|
-
expect(result.errors).toBeDefined();
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
it('should FAIL to read private product as different user', async () => {
|
|
798
|
-
// If securityCheck() blocks non-creators
|
|
799
|
-
const result = await testHelper.graphQl({
|
|
800
|
-
name: 'getProduct',
|
|
801
|
-
type: TestGraphQLType.QUERY,
|
|
802
|
-
arguments: { id: privateProductId },
|
|
803
|
-
fields: ['id', 'name']
|
|
804
|
-
}, { token: otherUserToken });
|
|
805
|
-
|
|
806
|
-
// securityCheck returns undefined for non-creator
|
|
807
|
-
expect(result).toBeUndefined();
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
});
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
---
|
|
814
|
-
|
|
815
|
-
#### Step 6: 📝 Complete Test Structure
|
|
816
|
-
|
|
817
|
-
**Test file location**:
|
|
818
|
-
```
|
|
819
|
-
tests/modules/<module-name>.e2e-spec.ts
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
**Complete test template with proper privileges**:
|
|
823
|
-
|
|
824
|
-
```typescript
|
|
825
|
-
import { TestGraphQLType, TestHelper } from '@lenne.tech/nest-server';
|
|
826
|
-
|
|
827
|
-
describe('Product Module E2E', () => {
|
|
828
|
-
let testHelper: TestHelper;
|
|
829
|
-
let userToken: string;
|
|
830
|
-
let creatorToken: string;
|
|
831
|
-
let otherUserToken: string;
|
|
832
|
-
let adminToken: string;
|
|
833
|
-
let createdProductId: string;
|
|
834
|
-
let userAuth: any;
|
|
835
|
-
let creatorAuth: any;
|
|
836
|
-
|
|
837
|
-
beforeAll(async () => {
|
|
838
|
-
testHelper = new TestHelper(app);
|
|
839
|
-
|
|
840
|
-
// Create test users (based on permission analysis)
|
|
841
|
-
userAuth = await testHelper.graphQl({
|
|
842
|
-
name: 'signUp',
|
|
843
|
-
type: TestGraphQLType.MUTATION,
|
|
844
|
-
arguments: { input: { email: 'user@test.com', password: 'password', roles: ['user'] } },
|
|
845
|
-
fields: ['token', 'user { id }']
|
|
846
|
-
});
|
|
847
|
-
userToken = userAuth.token;
|
|
848
|
-
|
|
849
|
-
creatorAuth = await testHelper.graphQl({
|
|
850
|
-
name: 'signUp',
|
|
851
|
-
type: TestGraphQLType.MUTATION,
|
|
852
|
-
arguments: { input: { email: 'creator@test.com', password: 'password', roles: ['user'] } },
|
|
853
|
-
fields: ['token', 'user { id }']
|
|
854
|
-
});
|
|
855
|
-
creatorToken = creatorAuth.token;
|
|
856
|
-
|
|
857
|
-
const otherUserAuth = await testHelper.graphQl({
|
|
858
|
-
name: 'signUp',
|
|
859
|
-
type: TestGraphQLType.MUTATION,
|
|
860
|
-
arguments: { input: { email: 'other@test.com', password: 'password', roles: ['user'] } },
|
|
861
|
-
fields: ['token']
|
|
862
|
-
});
|
|
863
|
-
otherUserToken = otherUserAuth.token;
|
|
864
|
-
|
|
865
|
-
const adminAuth = await testHelper.graphQl({
|
|
866
|
-
name: 'signUp',
|
|
867
|
-
type: TestGraphQLType.MUTATION,
|
|
868
|
-
arguments: { input: { email: 'admin@test.com', password: 'password', roles: ['admin', 'user'] } },
|
|
869
|
-
fields: ['token']
|
|
870
|
-
});
|
|
871
|
-
adminToken = adminAuth.token;
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
afterAll(async () => {
|
|
875
|
-
// Cleanup with appropriate privileges
|
|
876
|
-
if (createdProductId) {
|
|
877
|
-
await testHelper.graphQl({
|
|
878
|
-
name: 'deleteProduct',
|
|
879
|
-
type: TestGraphQLType.MUTATION,
|
|
880
|
-
arguments: { id: createdProductId },
|
|
881
|
-
fields: ['id']
|
|
882
|
-
}, { token: creatorToken });
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
// 1. CREATE Tests (with least privileged user)
|
|
887
|
-
describe('Create Product', () => {
|
|
888
|
-
it('should create product as regular user', async () => {
|
|
889
|
-
const result = await testHelper.graphQl({
|
|
890
|
-
name: 'createProduct',
|
|
891
|
-
type: TestGraphQLType.MUTATION,
|
|
892
|
-
arguments: { input: { name: 'Test', price: 10 } },
|
|
893
|
-
fields: ['id', 'name', 'price', 'createdBy']
|
|
894
|
-
}, { token: userToken }); // ← S_USER = regular user
|
|
895
|
-
|
|
896
|
-
expect(result.name).toBe('Test');
|
|
897
|
-
createdProductId = result.id;
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
it('should FAIL to create without authentication', async () => {
|
|
901
|
-
const result = await testHelper.graphQl({
|
|
902
|
-
name: 'createProduct',
|
|
903
|
-
type: TestGraphQLType.MUTATION,
|
|
904
|
-
arguments: { input: { name: 'Fail', price: 10 } },
|
|
905
|
-
fields: ['id']
|
|
906
|
-
}, { statusCode: 401 }); // ← No token = should fail
|
|
907
|
-
|
|
908
|
-
expect(result.errors).toBeDefined();
|
|
909
|
-
});
|
|
910
|
-
|
|
911
|
-
it('should FAIL to create without required fields', async () => {
|
|
912
|
-
const result = await testHelper.graphQl({
|
|
913
|
-
name: 'createProduct',
|
|
914
|
-
type: TestGraphQLType.MUTATION,
|
|
915
|
-
arguments: { input: {} },
|
|
916
|
-
fields: ['id']
|
|
917
|
-
}, { token: userToken, statusCode: 400 });
|
|
918
|
-
|
|
919
|
-
expect(result.errors).toBeDefined();
|
|
920
|
-
});
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
// 2. READ Tests
|
|
924
|
-
describe('Get Products', () => {
|
|
925
|
-
it('should get all products as regular user', async () => {
|
|
926
|
-
const result = await testHelper.graphQl({
|
|
927
|
-
name: 'getProducts',
|
|
928
|
-
type: TestGraphQLType.QUERY,
|
|
929
|
-
fields: ['id', 'name', 'price']
|
|
930
|
-
}, { token: userToken });
|
|
931
|
-
|
|
932
|
-
expect(Array.isArray(result)).toBe(true);
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
it('should get product by ID as regular user', async () => {
|
|
936
|
-
const result = await testHelper.graphQl({
|
|
937
|
-
name: 'getProduct',
|
|
938
|
-
type: TestGraphQLType.QUERY,
|
|
939
|
-
arguments: { id: createdProductId },
|
|
940
|
-
fields: ['id', 'name', 'price']
|
|
941
|
-
}, { token: userToken });
|
|
942
|
-
|
|
943
|
-
expect(result.id).toBe(createdProductId);
|
|
944
|
-
});
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
// 3. UPDATE Tests (with creator, not admin!)
|
|
948
|
-
describe('Update Product', () => {
|
|
949
|
-
let creatorProductId: string;
|
|
950
|
-
|
|
951
|
-
beforeAll(async () => {
|
|
952
|
-
// Creator creates a product to test updates
|
|
953
|
-
const created = await testHelper.graphQl({
|
|
954
|
-
name: 'createProduct',
|
|
955
|
-
type: TestGraphQLType.MUTATION,
|
|
956
|
-
arguments: { input: { name: 'Creator Product', price: 20 } },
|
|
957
|
-
fields: ['id']
|
|
958
|
-
}, { token: creatorToken });
|
|
959
|
-
creatorProductId = created.id;
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
it('should update product as creator', async () => {
|
|
963
|
-
const result = await testHelper.graphQl({
|
|
964
|
-
name: 'updateProduct',
|
|
965
|
-
type: TestGraphQLType.MUTATION,
|
|
966
|
-
arguments: { id: creatorProductId, input: { name: 'Updated' } },
|
|
967
|
-
fields: ['id', 'name']
|
|
968
|
-
}, { token: creatorToken }); // ← CREATOR token (least privilege!)
|
|
969
|
-
|
|
970
|
-
expect(result.name).toBe('Updated');
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
it('should FAIL to update product as non-creator', async () => {
|
|
974
|
-
const result = await testHelper.graphQl({
|
|
975
|
-
name: 'updateProduct',
|
|
976
|
-
type: TestGraphQLType.MUTATION,
|
|
977
|
-
arguments: { id: creatorProductId, input: { name: 'Hacked' } },
|
|
978
|
-
fields: ['id']
|
|
979
|
-
}, { token: otherUserToken, statusCode: 403 });
|
|
980
|
-
|
|
981
|
-
expect(result.errors).toBeDefined();
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
it('should update any product as admin', async () => {
|
|
985
|
-
const result = await testHelper.graphQl({
|
|
986
|
-
name: 'updateProduct',
|
|
987
|
-
type: TestGraphQLType.MUTATION,
|
|
988
|
-
arguments: { id: creatorProductId, input: { name: 'Admin Update' } },
|
|
989
|
-
fields: ['id', 'name']
|
|
990
|
-
}, { token: adminToken });
|
|
991
|
-
|
|
992
|
-
expect(result.name).toBe('Admin Update');
|
|
993
|
-
});
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
// 4. DELETE Tests (with creator, not admin!)
|
|
997
|
-
describe('Delete Product', () => {
|
|
998
|
-
it('should delete product as creator', async () => {
|
|
999
|
-
// Creator creates and deletes
|
|
1000
|
-
const created = await testHelper.graphQl({
|
|
1001
|
-
name: 'createProduct',
|
|
1002
|
-
type: TestGraphQLType.MUTATION,
|
|
1003
|
-
arguments: { input: { name: 'To Delete', price: 5 } },
|
|
1004
|
-
fields: ['id']
|
|
1005
|
-
}, { token: creatorToken });
|
|
1006
|
-
|
|
1007
|
-
const result = await testHelper.graphQl({
|
|
1008
|
-
name: 'deleteProduct',
|
|
1009
|
-
type: TestGraphQLType.MUTATION,
|
|
1010
|
-
arguments: { id: created.id },
|
|
1011
|
-
fields: ['id']
|
|
1012
|
-
}, { token: creatorToken }); // ← CREATOR token!
|
|
1013
|
-
|
|
1014
|
-
expect(result.id).toBe(created.id);
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
it('should FAIL to delete product as non-creator', async () => {
|
|
1018
|
-
const result = await testHelper.graphQl({
|
|
1019
|
-
name: 'deleteProduct',
|
|
1020
|
-
type: TestGraphQLType.MUTATION,
|
|
1021
|
-
arguments: { id: createdProductId },
|
|
1022
|
-
fields: ['id']
|
|
1023
|
-
}, { token: otherUserToken, statusCode: 403 });
|
|
1024
|
-
|
|
1025
|
-
expect(result.errors).toBeDefined();
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
it('should delete any product as admin', async () => {
|
|
1029
|
-
const result = await testHelper.graphQl({
|
|
1030
|
-
name: 'deleteProduct',
|
|
1031
|
-
type: TestGraphQLType.MUTATION,
|
|
1032
|
-
arguments: { id: createdProductId },
|
|
1033
|
-
fields: ['id']
|
|
1034
|
-
}, { token: adminToken });
|
|
1035
|
-
|
|
1036
|
-
expect(result.id).toBe(createdProductId);
|
|
1037
|
-
});
|
|
1038
|
-
});
|
|
1039
|
-
});
|
|
1040
|
-
```
|
|
1041
|
-
|
|
1042
|
-
---
|
|
1043
|
-
|
|
1044
|
-
#### Test Creation Checklist
|
|
1045
|
-
|
|
1046
|
-
Before finalizing tests, verify:
|
|
1047
|
-
|
|
1048
|
-
- [ ] ✅ I have analyzed ALL `@Roles()` decorators
|
|
1049
|
-
- [ ] ✅ I have read the complete `securityCheck()` method
|
|
1050
|
-
- [ ] ✅ I use the LEAST privileged user for each test
|
|
1051
|
-
- [ ] ✅ S_EVERYONE endpoints tested WITHOUT token
|
|
1052
|
-
- [ ] ✅ S_USER endpoints tested with REGULAR user (not admin)
|
|
1053
|
-
- [ ] ✅ UPDATE/DELETE tested with CREATOR token (not admin)
|
|
1054
|
-
- [ ] ✅ I have tests that verify unauthorized access FAILS (401/403)
|
|
1055
|
-
- [ ] ✅ I have tests that verify non-creators CANNOT update/delete
|
|
1056
|
-
- [ ] ✅ I have tests for missing required fields
|
|
1057
|
-
- [ ] ✅ All tests follow the security model
|
|
1058
|
-
- [ ] ✅ Tests validate protection mechanisms work
|
|
1059
|
-
|
|
1060
|
-
**⚠️ NEVER use admin token when a less privileged user would work!**
|
|
1061
|
-
|