opencode-andev-rules 1.0.1 → 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.
package/README.md CHANGED
@@ -64,30 +64,3 @@ scripts/
64
64
  3. Chạy `npm run validate` trước khi push.
65
65
  4. **Không sửa version bằng tay** — version do workflow publish tự tăng (xem dưới).
66
66
 
67
- ## Release lên npm
68
-
69
- > npm **từ chối publish đè version cũ** — version được **tự tăng mỗi lần publish**, không ai cần sửa `package.json`.
70
-
71
- **Cách chính — bấm nút (khuyên dùng):**
72
-
73
- 1. Vào tab **Actions** → workflow **Publish to npm** → **Run workflow**
74
- 2. Chọn loại tăng version: `patch` (fix skill, sửa doc) / `minor` (thêm skill) / `major` (breaking)
75
- 3. Workflow tự: validate → bump version → sync `skills/registry.json` → kiểm tra version chưa có trên npm → publish → push commit + tag `vX.Y.Z` về main
76
-
77
- **Cách phụ — chạy local** (yêu cầu đã `npm login`):
78
-
79
- ```bash
80
- bash scripts/release.sh patch # hoặc: minor | major | 1.2.3
81
- ```
82
-
83
- Kiểm tra: https://www.npmjs.com/package/opencode-andev-rules
84
-
85
- ### Setup npm auth 1 lần duy nhất (chọn 1 trong 2)
86
-
87
- **Cách A — NPM_TOKEN (nhanh nhất):**
88
- 1. npmjs.com → Access Tokens → Generate New Token → **Automation**
89
- 2. GitHub repo → Settings → Secrets → Actions → New secret `NPM_TOKEN` = token vừa tạo
90
-
91
- **Cách B — Trusted Publishing (không cần token):**
92
- 1. npmjs.com → package `opencode-andev-rules` → Settings → Trusted Publisher → GitHub Actions → nhập `thaiannguyen-05/rules-for-global-agents` + workflow `publish.yml`
93
- 2. Xóa secret `NPM_TOKEN` (workflow tự fallback sang OIDC, Node 22 đã đủ npm ≥ 11.5.1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-andev-rules",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Global coding rules for TypeScript & NestJS + AI agent skills.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -252,3 +252,4 @@ export async function correlationIdPlugin(app: FastifyInstance) {
252
252
  ## References
253
253
 
254
254
  - Feature slice layout, types, guards, dto, service, controller rules: `references/nestjs-module.md`.
255
+ - Unit, integration, E2E testing strategies, TestContainers, mocking: `references/nestjs-testing.md`.
@@ -0,0 +1,395 @@
1
+ # NestJS Testing Strategies
2
+
3
+ ## Test Types
4
+
5
+ ### Test Pyramid
6
+ ```
7
+ ╱╲
8
+ ╱ E2E ╲
9
+ ╱────────╲
10
+ ╱ Integration ╲
11
+ ╱────────────────╲
12
+ ╱ Unit Tests ╲
13
+ ╱──────────────────╲
14
+ ```
15
+
16
+ | Type | Speed | Scope | Confidence |
17
+ |------|-------|-------|------------|
18
+ | Unit | Fast | Single class/module | Low |
19
+ | Integration | Medium | Module interactions | Medium |
20
+ | E2E | Slow | Full system | High |
21
+
22
+ ## Unit Testing
23
+
24
+ ### Service Test
25
+ ```typescript
26
+ import { Test } from '@nestjs/testing';
27
+ import { UserService } from './user.service';
28
+ import { UserRepository } from './user.repository';
29
+ import { EmailService } from './email.service';
30
+
31
+ describe('UserService', () => {
32
+ let service: UserService;
33
+ let userRepo: jest.Mocked<UserRepository>;
34
+ let emailService: jest.Mocked<EmailService>;
35
+
36
+ beforeEach(async () => {
37
+ const module = await Test.createTestingModule({
38
+ providers: [
39
+ UserService,
40
+ {
41
+ provide: UserRepository,
42
+ useValue: {
43
+ findById: jest.fn(),
44
+ save: jest.fn(),
45
+ delete: jest.fn(),
46
+ },
47
+ },
48
+ {
49
+ provide: EmailService,
50
+ useValue: {
51
+ sendWelcomeEmail: jest.fn(),
52
+ },
53
+ },
54
+ ],
55
+ }).compile();
56
+
57
+ service = module.get(UserService);
58
+ userRepo = module.get(UserRepository);
59
+ emailService = module.get(EmailService);
60
+ });
61
+
62
+ describe('createUser', () => {
63
+ it('should create a user and send welcome email', async () => {
64
+ const dto = { email: 'test@example.com', name: 'Test' };
65
+ const expectedUser = { id: '1', ...dto };
66
+
67
+ userRepo.save.mockResolvedValue(expectedUser);
68
+
69
+ const result = await service.createUser(dto);
70
+
71
+ expect(result).toEqual(expectedUser);
72
+ expect(userRepo.save).toHaveBeenCalledWith(expect.objectContaining(dto));
73
+ expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(expectedUser.id, dto.email);
74
+ });
75
+
76
+ it('should throw if email already exists', async () => {
77
+ userRepo.findByEmail.mockResolvedValue({ id: '1', email: 'exists@example.com' });
78
+
79
+ await expect(
80
+ service.createUser({ email: 'exists@example.com', name: 'Test' }),
81
+ ).rejects.toThrow('Email already exists');
82
+ });
83
+ });
84
+ });
85
+ ```
86
+
87
+ ### Controller Test
88
+ ```typescript
89
+ import { Test } from '@nestjs/testing';
90
+ import { UserController } from './user.controller';
91
+ import { UserService } from './user.service';
92
+
93
+ describe('UserController', () => {
94
+ let controller: UserController;
95
+ let service: jest.Mocked<UserService>;
96
+
97
+ beforeEach(async () => {
98
+ const module = await Test.createTestingModule({
99
+ controllers: [UserController],
100
+ providers: [
101
+ {
102
+ provide: UserService,
103
+ useValue: {
104
+ createUser: jest.fn(),
105
+ findById: jest.fn(),
106
+ findAll: jest.fn(),
107
+ },
108
+ },
109
+ ],
110
+ }).compile();
111
+
112
+ controller = module.get(UserController);
113
+ service = module.get(UserService);
114
+ });
115
+
116
+ it('should return created user', async () => {
117
+ const dto = { email: 'test@example.com', name: 'Test' };
118
+ const expected = { id: '1', ...dto };
119
+
120
+ service.createUser.mockResolvedValue(expected);
121
+
122
+ const result = await controller.create(dto);
123
+
124
+ expect(result).toEqual(expected);
125
+ expect(result).not.toHaveProperty('password');
126
+ });
127
+ });
128
+ ```
129
+
130
+ ## Integration Testing
131
+
132
+ ### Database Integration
133
+ ```typescript
134
+ import { Test } from '@nestjs/testing';
135
+ import { TypeOrmModule } from '@nestjs/typeorm';
136
+ import { UserService } from './user.service';
137
+ import { User } from './user.entity';
138
+ import { UserModule } from './user.module';
139
+ import { createTestDatabase } from '../test/utils/database';
140
+
141
+ describe('UserService Integration', () => {
142
+ let service: UserService;
143
+ let module: TestingModule;
144
+
145
+ beforeAll(async () => {
146
+ const database = await createTestDatabase();
147
+
148
+ module = await Test.createTestingModule({
149
+ imports: [
150
+ TypeOrmModule.forRoot(database.config),
151
+ UserModule,
152
+ ],
153
+ }).compile();
154
+
155
+ service = module.get(UserService);
156
+ });
157
+
158
+ afterAll(async () => {
159
+ await module.close();
160
+ });
161
+
162
+ beforeEach(async () => {
163
+ await cleanDatabase();
164
+ });
165
+
166
+ it('should persist user to database', async () => {
167
+ const user = await service.createUser({
168
+ email: 'test@example.com',
169
+ name: 'Test User',
170
+ });
171
+
172
+ const found = await service.findById(user.id);
173
+ expect(found).toBeDefined();
174
+ expect(found.email).toBe('test@example.com');
175
+ });
176
+
177
+ it('should enforce unique email constraint', async () => {
178
+ await service.createUser({
179
+ email: 'dupe@example.com',
180
+ name: 'First',
181
+ });
182
+
183
+ await expect(
184
+ service.createUser({
185
+ email: 'dupe@example.com',
186
+ name: 'Second',
187
+ }),
188
+ ).rejects.toThrow();
189
+ });
190
+ });
191
+ ```
192
+
193
+ ### HTTP Integration
194
+ ```typescript
195
+ import * as request from 'supertest';
196
+ import { Test } from '@nestjs/testing';
197
+ import { INestApplication } from '@nestjs/common';
198
+ import { AppModule } from '../src/app.module';
199
+
200
+ describe('User API (integration)', () => {
201
+ let app: INestApplication;
202
+
203
+ beforeAll(async () => {
204
+ const module = await Test.createTestingModule({
205
+ imports: [AppModule],
206
+ }).compile();
207
+
208
+ app = module.createNestApplication();
209
+ await app.init();
210
+ });
211
+
212
+ afterAll(async () => {
213
+ await app.close();
214
+ });
215
+
216
+ describe('POST /users', () => {
217
+ it('should create user', async () => {
218
+ const response = await request(app.getHttpServer())
219
+ .post('/users')
220
+ .send({ email: 'test@example.com', name: 'Test' })
221
+ .expect(201);
222
+
223
+ expect(response.body).toHaveProperty('id');
224
+ expect(response.body.email).toBe('test@example.com');
225
+ });
226
+
227
+ it('should reject invalid email', async () => {
228
+ await request(app.getHttpServer())
229
+ .post('/users')
230
+ .send({ email: 'invalid', name: 'Test' })
231
+ .expect(400);
232
+ });
233
+ });
234
+ });
235
+ ```
236
+
237
+ ## E2E Testing
238
+
239
+ ### Full E2E Test
240
+ ```typescript
241
+ import * as request from 'supertest';
242
+ import { Test } from '@nestjs/testing';
243
+ import { INestApplication } from '@nestjs/common';
244
+ import { AppModule } from '../src/app.module';
245
+
246
+ describe('User E2E', () => {
247
+ let app: INestApplication;
248
+ let authToken: string;
249
+
250
+ beforeAll(async () => {
251
+ const module = await Test.createTestingModule({
252
+ imports: [AppModule],
253
+ }).compile();
254
+
255
+ app = module.createNestApplication();
256
+ await app.init();
257
+
258
+ // Auth
259
+ const loginRes = await request(app.getHttpServer())
260
+ .post('/auth/login')
261
+ .send({ email: 'admin@test.com', password: 'admin123' });
262
+
263
+ authToken = loginRes.body.accessToken;
264
+ });
265
+
266
+ afterAll(async () => {
267
+ await app.close();
268
+ });
269
+
270
+ it('full user lifecycle', async () => {
271
+ // Create
272
+ const createRes = await request(app.getHttpServer())
273
+ .post('/users')
274
+ .set('Authorization', `Bearer ${authToken}`)
275
+ .send({ email: 'new@test.com', name: 'New User' })
276
+ .expect(201);
277
+
278
+ const userId = createRes.body.id;
279
+
280
+ // Read
281
+ await request(app.getHttpServer())
282
+ .get(`/users/${userId}`)
283
+ .set('Authorization', `Bearer ${authToken}`)
284
+ .expect(200);
285
+
286
+ // List
287
+ const listRes = await request(app.getHttpServer())
288
+ .get('/users')
289
+ .set('Authorization', `Bearer ${authToken}`)
290
+ .expect(200);
291
+
292
+ expect(listRes.body.data.length).toBeGreaterThan(0);
293
+
294
+ // Delete
295
+ await request(app.getHttpServer())
296
+ .delete(`/users/${userId}`)
297
+ .set('Authorization', `Bearer ${authToken}`)
298
+ .expect(204);
299
+ });
300
+ });
301
+ ```
302
+
303
+ ## Test Database Utilities
304
+
305
+ ### Test Database Setup
306
+ ```typescript
307
+ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
308
+ import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
309
+
310
+ export async function createTestDatabase(): Promise<{
311
+ container: StartedPostgreSqlContainer;
312
+ config: TypeOrmModuleOptions;
313
+ }> {
314
+ const container = await new PostgreSqlContainer()
315
+ .withDatabase('test')
316
+ .withUsername('test')
317
+ .withPassword('test')
318
+ .start();
319
+
320
+ const config: TypeOrmModuleOptions = {
321
+ type: 'postgres',
322
+ host: container.getHost(),
323
+ port: container.getMappedPort(5432),
324
+ username: container.getUsername(),
325
+ password: container.getPassword(),
326
+ database: container.getDatabase(),
327
+ entities: ['dist/**/*.entity{.ts,.js}'],
328
+ synchronize: true,
329
+ };
330
+
331
+ return { container, config };
332
+ }
333
+
334
+ export async function cleanDatabase(): Promise<void> {
335
+ const entities = getMetadataArgsStorage().tables.map((t) => t.name);
336
+ for (const entity of entities) {
337
+ await getConnection().manager.query(`TRUNCATE TABLE "${entity}" CASCADE`);
338
+ }
339
+ }
340
+ ```
341
+
342
+ ## Mocking Strategies
343
+
344
+ ### Service Mocking
345
+ ```typescript
346
+ // Mock factory
347
+ export const createMockUserService = (): jest.Mocked<UserService> => ({
348
+ createUser: jest.fn(),
349
+ findById: jest.fn(),
350
+ findAll: jest.fn(),
351
+ updateUser: jest.fn(),
352
+ deleteUser: jest.fn(),
353
+ });
354
+
355
+ // Custom provider
356
+ const mockUserService = createMockUserService();
357
+ mockUserService.findById.mockResolvedValue(testUser);
358
+
359
+ const module = await Test.createTestingModule({
360
+ providers: [
361
+ { provide: UserService, useValue: mockUserService },
362
+ ],
363
+ }).compile();
364
+ ```
365
+
366
+ ### Guard Mocking
367
+ ```typescript
368
+ import { ExecutionContext } from '@nestjs/common';
369
+
370
+ export const mockAuthGuard = {
371
+ canActivate: jest.fn((context: ExecutionContext) => {
372
+ const req = context.switchToHttp().getRequest();
373
+ req.user = { id: 'test-user-id', roles: ['admin'] };
374
+ return true;
375
+ }),
376
+ };
377
+
378
+ // Override guard in test
379
+ const module = await Test.createTestingModule({
380
+ imports: [UserModule],
381
+ })
382
+ .overrideGuard(AuthGuard('jwt'))
383
+ .useValue(mockAuthGuard)
384
+ .compile();
385
+ ```
386
+
387
+ ## Key Points
388
+ - Unit tests mock all dependencies and test single classes in isolation
389
+ - Integration tests use real database (TestContainers) and verify module interactions
390
+ - E2E tests exercise the full HTTP stack including guards, pipes, interceptors
391
+ - TestContainers provides disposable databases for integration testing
392
+ - Clean database between tests to avoid state pollution
393
+ - Guard mocking enables testing authenticated endpoints without tokens
394
+ - Service mocking with jest.Mocked<T> provides type-safe mocks
395
+ - Override providers and guards for controlled test scenarios
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "andev-skills",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "AI agent skills by thaiannguyen-05",
5
5
  "skills": [
6
6
  {
@@ -33,7 +33,8 @@
33
33
  "path": "skills/backend/nestJS",
34
34
  "files": [
35
35
  "SKILL.md",
36
- "references/nestjs-module.md"
36
+ "references/nestjs-module.md",
37
+ "references/nestjs-testing.md"
37
38
  ]
38
39
  },
39
40
  {