create-tigra 1.1.0 → 2.0.1
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/LICENSE +21 -21
- package/README.md +80 -87
- package/bin/create-tigra.js +259 -308
- package/package.json +49 -41
- package/template/_claude/QUICK_REFERENCE.md +193 -0
- package/template/_claude/README.md +53 -0
- package/template/_claude/commands/create-client.md +881 -0
- package/template/_claude/commands/create-server.md +383 -0
- package/template/_claude/rules/client/01-project-structure.md +133 -0
- package/template/_claude/rules/client/02-components-and-types.md +146 -0
- package/template/_claude/rules/client/03-data-and-state.md +156 -0
- package/template/_claude/rules/client/04-design-system.md +185 -0
- package/template/_claude/rules/client/05-security.md +55 -0
- package/template/_claude/rules/client/06-ux-checklist.md +81 -0
- package/template/_claude/rules/client/core.md +42 -0
- package/template/_claude/rules/global/core.md +77 -0
- package/template/_claude/rules/server/core.md +50 -0
- package/template/_claude/rules/server/database.md +124 -0
- package/template/_claude/rules/server/project-conventions.md +150 -0
- package/template/_claude/rules/server/response-handling.md +144 -0
- package/template/client/.env.example +5 -0
- package/template/client/README.md +36 -0
- package/template/client/components.json +23 -0
- package/template/client/eslint.config.mjs +18 -0
- package/template/client/next.config.ts +34 -0
- package/template/client/package.json +44 -0
- package/template/client/postcss.config.mjs +7 -0
- package/template/client/src/app/(auth)/layout.tsx +18 -0
- package/template/client/src/app/(auth)/login/page.tsx +13 -0
- package/template/client/src/app/(auth)/register/page.tsx +13 -0
- package/template/client/src/app/(main)/dashboard/page.tsx +22 -0
- package/template/client/src/app/(main)/layout.tsx +11 -0
- package/template/client/src/app/error.tsx +27 -0
- package/template/client/src/app/favicon.ico +0 -0
- package/template/client/src/app/globals.css +145 -0
- package/template/client/src/app/layout.tsx +36 -0
- package/template/client/src/app/loading.tsx +11 -0
- package/template/client/src/app/not-found.tsx +23 -0
- package/template/client/src/app/page.tsx +45 -0
- package/template/client/src/app/providers.tsx +43 -0
- package/template/client/src/components/common/ConfirmDialog.tsx +56 -0
- package/template/client/src/components/common/EmptyState.tsx +31 -0
- package/template/client/src/components/common/LoadingSpinner.tsx +30 -0
- package/template/client/src/components/common/Pagination.tsx +55 -0
- package/template/client/src/components/layout/Footer.tsx +17 -0
- package/template/client/src/components/layout/Header.tsx +173 -0
- package/template/client/src/components/layout/MainLayout.tsx +18 -0
- package/template/client/src/components/ui/alert-dialog.tsx +196 -0
- package/template/client/src/components/ui/badge.tsx +48 -0
- package/template/client/src/components/ui/button.tsx +64 -0
- package/template/client/src/components/ui/card.tsx +92 -0
- package/template/client/src/components/ui/input.tsx +21 -0
- package/template/client/src/components/ui/label.tsx +24 -0
- package/template/client/src/components/ui/select.tsx +190 -0
- package/template/client/src/components/ui/skeleton.tsx +13 -0
- package/template/client/src/components/ui/table.tsx +116 -0
- package/template/client/src/features/auth/components/AuthInitializer.tsx +55 -0
- package/template/client/src/features/auth/components/LoginForm.tsx +107 -0
- package/template/client/src/features/auth/components/RegisterForm.tsx +178 -0
- package/template/client/src/features/auth/hooks/useAuth.ts +84 -0
- package/template/client/src/features/auth/services/auth.service.ts +52 -0
- package/template/client/src/features/auth/store/authSlice.ts +38 -0
- package/template/client/src/features/auth/types/auth.types.ts +32 -0
- package/template/client/src/hooks/useDebounce.ts +14 -0
- package/template/client/src/hooks/useLocalStorage.ts +55 -0
- package/template/client/src/hooks/useMediaQuery.ts +27 -0
- package/template/client/src/lib/api/api.types.ts +34 -0
- package/template/client/src/lib/api/axios.config.ts +98 -0
- package/template/client/src/lib/constants/api-endpoints.ts +18 -0
- package/template/client/src/lib/constants/app.constants.ts +12 -0
- package/template/client/src/lib/constants/routes.ts +9 -0
- package/template/client/src/lib/utils/error.ts +32 -0
- package/template/client/src/lib/utils/format.ts +37 -0
- package/template/client/src/lib/utils/security.ts +34 -0
- package/template/client/src/lib/utils.ts +6 -0
- package/template/client/src/middleware.ts +57 -0
- package/template/client/src/store/hooks.ts +7 -0
- package/template/client/src/store/index.ts +12 -0
- package/template/client/src/types/index.ts +3 -0
- package/template/client/tsconfig.json +34 -0
- package/template/gitignore +34 -0
- package/template/server/.dockerignore +66 -0
- package/template/server/.env.example +96 -69
- package/template/server/.env.production.example +90 -0
- package/template/server/Dockerfile +94 -0
- package/template/server/docker-compose.yml +82 -111
- package/template/server/docs/logging.md +62 -0
- package/template/server/eslint.config.mjs +17 -0
- package/template/server/package.json +68 -81
- package/template/server/phpmyadmin-config.php +26 -0
- package/template/server/postman_collection.json +666 -0
- package/template/server/prisma/schema.prisma +77 -93
- package/template/server/prisma/seed.ts +46 -142
- package/template/server/scripts/flush-redis.ts +41 -0
- package/template/server/src/app.ts +243 -71
- package/template/server/src/config/env.ts +67 -94
- package/template/server/src/libs/auth.ts +88 -0
- package/template/server/src/libs/cleanup.ts +35 -0
- package/template/server/src/libs/cookies.ts +46 -0
- package/template/server/src/libs/logger.ts +33 -60
- package/template/server/src/libs/monitoring.ts +205 -0
- package/template/server/src/libs/password.ts +38 -0
- package/template/server/src/libs/prisma.ts +68 -0
- package/template/server/src/libs/redis.ts +60 -79
- package/template/server/src/libs/requestLogger.ts +66 -0
- package/template/server/src/libs/storage/file-storage.service.ts +211 -0
- package/template/server/src/libs/storage/file-validator.ts +97 -0
- package/template/server/src/libs/storage/filename-sanitizer.ts +71 -0
- package/template/server/src/libs/storage/image-optimizer.service.ts +144 -0
- package/template/server/src/modules/auth/__tests__/auth.service.test.ts +365 -0
- package/template/server/src/modules/auth/auth.controller.ts +90 -141
- package/template/server/src/modules/auth/auth.repo.ts +120 -218
- package/template/server/src/modules/auth/auth.routes.ts +96 -83
- package/template/server/src/modules/auth/auth.schemas.ts +35 -137
- package/template/server/src/modules/auth/auth.service.ts +286 -329
- package/template/server/src/modules/auth/session.repo.ts +110 -0
- package/template/server/src/modules/users/users.controller.ts +120 -0
- package/template/server/src/modules/users/users.repo.ts +77 -0
- package/template/server/src/modules/users/users.routes.ts +89 -0
- package/template/server/src/modules/users/users.schemas.ts +21 -0
- package/template/server/src/modules/users/users.service.ts +169 -0
- package/template/server/src/server.ts +58 -139
- package/template/server/src/shared/errors/AppError.ts +21 -0
- package/template/server/src/shared/errors/errors.ts +43 -0
- package/template/server/src/shared/responses/paginatedResponse.ts +38 -0
- package/template/server/src/shared/responses/successResponse.ts +17 -0
- package/template/server/src/shared/schemas/pagination.schema.ts +12 -0
- package/template/server/src/shared/types/index.ts +26 -0
- package/template/server/src/test/setup.ts +74 -38
- package/template/server/tsconfig.json +27 -89
- package/template/server/uploads/avatars/.gitkeep +1 -0
- package/template/server/vitest.config.ts +43 -98
- package/template/.agent/rules/client/01-project-structure.md +0 -326
- package/template/.agent/rules/client/02-component-patterns.md +0 -249
- package/template/.agent/rules/client/03-typescript-rules.md +0 -226
- package/template/.agent/rules/client/04-state-management.md +0 -474
- package/template/.agent/rules/client/05-api-integration.md +0 -129
- package/template/.agent/rules/client/06-forms-validation.md +0 -129
- package/template/.agent/rules/client/07-common-patterns.md +0 -150
- package/template/.agent/rules/client/08-color-system.md +0 -93
- package/template/.agent/rules/client/09-security-rules.md +0 -97
- package/template/.agent/rules/client/10-testing-strategy.md +0 -370
- package/template/.agent/rules/global/ai-edit-safety.md +0 -38
- package/template/.agent/rules/server/01-db-and-migrations.md +0 -242
- package/template/.agent/rules/server/02-general-rules.md +0 -111
- package/template/.agent/rules/server/03-migrations.md +0 -20
- package/template/.agent/rules/server/04-pagination.md +0 -130
- package/template/.agent/rules/server/05-project-conventions.md +0 -71
- package/template/.agent/rules/server/06-response-handling.md +0 -173
- package/template/.agent/rules/server/07-testing-strategy.md +0 -506
- package/template/.agent/rules/server/08-observability.md +0 -180
- package/template/.agent/rules/server/10-background-jobs-v2.md +0 -185
- package/template/.agent/rules/server/11-rate-limiting-v2.md +0 -210
- package/template/.agent/rules/server/12-performance-optimization.md +0 -567
- package/template/.claude/rules/client-01-project-structure.md +0 -327
- package/template/.claude/rules/client-02-component-patterns.md +0 -250
- package/template/.claude/rules/client-03-typescript-rules.md +0 -227
- package/template/.claude/rules/client-04-state-management.md +0 -475
- package/template/.claude/rules/client-05-api-integration.md +0 -130
- package/template/.claude/rules/client-06-forms-validation.md +0 -130
- package/template/.claude/rules/client-07-common-patterns.md +0 -151
- package/template/.claude/rules/client-08-color-system.md +0 -94
- package/template/.claude/rules/client-09-security-rules.md +0 -98
- package/template/.claude/rules/client-10-testing-strategy.md +0 -371
- package/template/.claude/rules/global-ai-edit-safety.md +0 -39
- package/template/.claude/rules/server-01-db-and-migrations.md +0 -243
- package/template/.claude/rules/server-02-general-rules.md +0 -112
- package/template/.claude/rules/server-03-migrations.md +0 -21
- package/template/.claude/rules/server-04-pagination.md +0 -131
- package/template/.claude/rules/server-05-project-conventions.md +0 -72
- package/template/.claude/rules/server-06-response-handling.md +0 -174
- package/template/.claude/rules/server-07-testing-strategy.md +0 -507
- package/template/.claude/rules/server-08-observability.md +0 -181
- package/template/.claude/rules/server-10-background-jobs-v2.md +0 -186
- package/template/.claude/rules/server-11-rate-limiting-v2.md +0 -211
- package/template/.claude/rules/server-12-performance-optimization.md +0 -568
- package/template/.cursor/rules/client-01-project-structure.mdc +0 -327
- package/template/.cursor/rules/client-02-component-patterns.mdc +0 -250
- package/template/.cursor/rules/client-03-typescript-rules.mdc +0 -227
- package/template/.cursor/rules/client-04-state-management.mdc +0 -475
- package/template/.cursor/rules/client-05-api-integration.mdc +0 -130
- package/template/.cursor/rules/client-06-forms-validation.mdc +0 -130
- package/template/.cursor/rules/client-07-common-patterns.mdc +0 -151
- package/template/.cursor/rules/client-08-color-system.mdc +0 -94
- package/template/.cursor/rules/client-09-security-rules.mdc +0 -98
- package/template/.cursor/rules/client-10-testing-strategy.mdc +0 -371
- package/template/.cursor/rules/global-ai-edit-safety.mdc +0 -39
- package/template/.cursor/rules/server-01-db-and-migrations.mdc +0 -243
- package/template/.cursor/rules/server-02-general-rules.mdc +0 -112
- package/template/.cursor/rules/server-03-migrations.mdc +0 -21
- package/template/.cursor/rules/server-04-pagination.mdc +0 -131
- package/template/.cursor/rules/server-05-project-conventions.mdc +0 -72
- package/template/.cursor/rules/server-06-response-handling.mdc +0 -174
- package/template/.cursor/rules/server-07-testing-strategy.mdc +0 -507
- package/template/.cursor/rules/server-08-observability.mdc +0 -181
- package/template/.cursor/rules/server-09-api-documentation-v2.mdc +0 -169
- package/template/.cursor/rules/server-10-background-jobs-v2.mdc +0 -186
- package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +0 -211
- package/template/.cursor/rules/server-12-performance-optimization.mdc +0 -568
- package/template/CLAUDE.md +0 -207
- package/template/server/.tsc-aliasrc.json +0 -13
- package/template/server/IMPORT_FIX_CHECKLIST.md +0 -98
- package/template/server/IMPORT_FIX_COMPLETE.md +0 -89
- package/template/server/README.md +0 -183
- package/template/server/REMAINING_IMPORT_FIXES.md +0 -150
- package/template/server/SECURITY.md +0 -190
- package/template/server/Tigra-API.postman_collection.json +0 -733
- package/template/server/biome.json +0 -42
- package/template/server/scripts/fix-all-imports.ps1 +0 -52
- package/template/server/scripts/fix-imports-reference.ps1 +0 -16
- package/template/server/scripts/fix-imports.mjs +0 -55
- package/template/server/scripts/setup-env.js +0 -50
- package/template/server/scripts/wait-for-db.js +0 -60
- package/template/server/src/hooks/request-timing.hook.ts +0 -26
- package/template/server/src/libs/auth/authenticate.middleware.ts +0 -22
- package/template/server/src/libs/auth/rbac.middleware.test.ts +0 -134
- package/template/server/src/libs/auth/rbac.middleware.ts +0 -147
- package/template/server/src/libs/db.ts +0 -76
- package/template/server/src/libs/error-handler.ts +0 -89
- package/template/server/src/libs/queue.ts +0 -79
- package/template/server/src/modules/admin/admin.controller.ts +0 -122
- package/template/server/src/modules/admin/admin.routes.ts +0 -62
- package/template/server/src/modules/admin/admin.schemas.ts +0 -35
- package/template/server/src/modules/admin/admin.service.ts +0 -167
- package/template/server/src/modules/auth/auth.integration.test.ts +0 -150
- package/template/server/src/modules/auth/auth.service.test.ts +0 -119
- package/template/server/src/modules/auth/auth.types.ts +0 -97
- package/template/server/src/modules/resources/resources.controller.ts +0 -218
- package/template/server/src/modules/resources/resources.repo.ts +0 -253
- package/template/server/src/modules/resources/resources.routes.ts +0 -116
- package/template/server/src/modules/resources/resources.schemas.ts +0 -146
- package/template/server/src/modules/resources/resources.service.ts +0 -218
- package/template/server/src/modules/resources/resources.types.ts +0 -73
- package/template/server/src/plugins/rate-limit.plugin.ts +0 -21
- package/template/server/src/plugins/security.plugin.ts +0 -21
- package/template/server/src/routes/health.routes.ts +0 -31
- package/template/server/src/types/fastify.d.ts +0 -36
- package/template/server/src/utils/errors.ts +0 -108
- package/template/server/src/utils/pagination.ts +0 -120
- package/template/server/src/utils/response.ts +0 -110
- package/template/server/src/workers/file.worker.ts +0 -106
- package/template/server/tsconfig.build.json +0 -30
- package/template/server/tsconfig.test.json +0 -22
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
trigger: always_on
|
|
3
|
-
globs: "server/**/*"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
> **SCOPE**: These rules apply specifically to the **server** directory.
|
|
7
|
-
|
|
8
|
-
# Testing Strategy & Best Practices
|
|
9
|
-
|
|
10
|
-
## Test Philosophy
|
|
11
|
-
|
|
12
|
-
- **Test the behavior, not the implementation**
|
|
13
|
-
- **Aim for 70%+ coverage** on services and critical utilities
|
|
14
|
-
- **Fast tests** - Unit tests < 50ms, Integration < 500ms each
|
|
15
|
-
- **Deterministic** - Tests must pass/fail consistently
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Test Stack
|
|
20
|
-
|
|
21
|
-
```json
|
|
22
|
-
{
|
|
23
|
-
"devDependencies": {
|
|
24
|
-
"vitest": "^1.0.0",
|
|
25
|
-
"supertest": "^6.3.0",
|
|
26
|
-
"@faker-js/faker": "^8.0.0",
|
|
27
|
-
"@testcontainers/mysql": "^10.0.0"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**Tools:**
|
|
33
|
-
- **Vitest** - Test runner (faster than Jest)
|
|
34
|
-
- **Supertest** - HTTP endpoint testing
|
|
35
|
-
- **Faker** - Generate realistic test data
|
|
36
|
-
- **Testcontainers** - Isolated MySQL for integration tests
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Folder Structure
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
src/
|
|
44
|
-
├── modules/
|
|
45
|
-
│ └── users/
|
|
46
|
-
│ ├── users.service.ts
|
|
47
|
-
│ ├── users.service.test.ts # Unit tests
|
|
48
|
-
│ ├── users.controller.ts
|
|
49
|
-
│ └── users.integration.test.ts # Integration tests
|
|
50
|
-
├── libs/
|
|
51
|
-
│ └── auth/
|
|
52
|
-
│ ├── jwt.ts
|
|
53
|
-
│ └── jwt.test.ts
|
|
54
|
-
└── __tests__/
|
|
55
|
-
├── setup.ts # Global test setup
|
|
56
|
-
├── helpers/ # Test utilities
|
|
57
|
-
│ ├── factory.ts # Data factories
|
|
58
|
-
│ └── db.ts # Test DB helpers
|
|
59
|
-
└── e2e/ # End-to-end tests
|
|
60
|
-
└── auth-flow.test.ts
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Test Types & When to Use
|
|
66
|
-
|
|
67
|
-
### 1. Unit Tests (70% of tests)
|
|
68
|
-
**Test:** Services, utilities, helpers
|
|
69
|
-
**Mock:** Database, external APIs
|
|
70
|
-
**Speed:** < 50ms per test
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
// users.service.test.ts
|
|
74
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
75
|
-
import { UserService } from './users.service';
|
|
76
|
-
import { UserRepository } from './users.repo';
|
|
77
|
-
|
|
78
|
-
vi.mock('./users.repo');
|
|
79
|
-
|
|
80
|
-
describe('UserService', () => {
|
|
81
|
-
let service: UserService;
|
|
82
|
-
let mockRepo: any;
|
|
83
|
-
|
|
84
|
-
beforeEach(() => {
|
|
85
|
-
mockRepo = {
|
|
86
|
-
findById: vi.fn(),
|
|
87
|
-
create: vi.fn(),
|
|
88
|
-
};
|
|
89
|
-
service = new UserService(mockRepo);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('getUserById', () => {
|
|
93
|
-
it('should return user when found', async () => {
|
|
94
|
-
const mockUser = { id: '1', email: 'test@example.com' };
|
|
95
|
-
mockRepo.findById.mockResolvedValue(mockUser);
|
|
96
|
-
|
|
97
|
-
const result = await service.getUserById('1');
|
|
98
|
-
|
|
99
|
-
expect(result).toEqual(mockUser);
|
|
100
|
-
expect(mockRepo.findById).toHaveBeenCalledWith('1');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should throw NotFoundError when user does not exist', async () => {
|
|
104
|
-
mockRepo.findById.mockResolvedValue(null);
|
|
105
|
-
|
|
106
|
-
await expect(service.getUserById('999'))
|
|
107
|
-
.rejects.toThrow('User not found');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### 2. Integration Tests (20% of tests)
|
|
114
|
-
**Test:** API endpoints with real DB
|
|
115
|
-
**Mock:** External APIs only
|
|
116
|
-
**Speed:** < 500ms per test
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
// users.integration.test.ts
|
|
120
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
121
|
-
import { build } from '../app';
|
|
122
|
-
import { setupTestDB, cleanupTestDB } from '../__tests__/helpers/db';
|
|
123
|
-
|
|
124
|
-
describe('Users API', () => {
|
|
125
|
-
let app: any;
|
|
126
|
-
|
|
127
|
-
beforeAll(async () => {
|
|
128
|
-
await setupTestDB();
|
|
129
|
-
app = await build();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
afterAll(async () => {
|
|
133
|
-
await app.close();
|
|
134
|
-
await cleanupTestDB();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('POST /api/v1/users', () => {
|
|
138
|
-
it('should create user with valid data', async () => {
|
|
139
|
-
const response = await app.inject({
|
|
140
|
-
method: 'POST',
|
|
141
|
-
url: '/api/v1/users',
|
|
142
|
-
payload: {
|
|
143
|
-
email: 'test@example.com',
|
|
144
|
-
password: 'SecurePass123!',
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
expect(response.statusCode).toBe(201);
|
|
149
|
-
expect(response.json()).toMatchObject({
|
|
150
|
-
success: true,
|
|
151
|
-
message: 'User created successfully',
|
|
152
|
-
data: {
|
|
153
|
-
email: 'test@example.com',
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should reject invalid email', async () => {
|
|
159
|
-
const response = await app.inject({
|
|
160
|
-
method: 'POST',
|
|
161
|
-
url: '/api/v1/users',
|
|
162
|
-
payload: {
|
|
163
|
-
email: 'invalid-email',
|
|
164
|
-
password: 'SecurePass123!',
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
expect(response.statusCode).toBe(400);
|
|
169
|
-
expect(response.json().success).toBe(false);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### 3. E2E Tests (10% of tests)
|
|
176
|
-
**Test:** Complete user flows
|
|
177
|
-
**Mock:** Nothing (real environment)
|
|
178
|
-
**Speed:** < 2s per flow
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
// __tests__/e2e/auth-flow.test.ts
|
|
182
|
-
describe('Authentication Flow', () => {
|
|
183
|
-
it('should complete full auth lifecycle', async () => {
|
|
184
|
-
// 1. Register
|
|
185
|
-
const registerRes = await app.inject({
|
|
186
|
-
method: 'POST',
|
|
187
|
-
url: '/api/v1/auth/register',
|
|
188
|
-
payload: { email: 'user@test.com', password: 'Pass123!' },
|
|
189
|
-
});
|
|
190
|
-
expect(registerRes.statusCode).toBe(201);
|
|
191
|
-
|
|
192
|
-
// 2. Login
|
|
193
|
-
const loginRes = await app.inject({
|
|
194
|
-
method: 'POST',
|
|
195
|
-
url: '/api/v1/auth/login',
|
|
196
|
-
payload: { email: 'user@test.com', password: 'Pass123!' },
|
|
197
|
-
});
|
|
198
|
-
const { accessToken } = loginRes.json().data;
|
|
199
|
-
|
|
200
|
-
// 3. Access protected route
|
|
201
|
-
const profileRes = await app.inject({
|
|
202
|
-
method: 'GET',
|
|
203
|
-
url: '/api/v1/users/me',
|
|
204
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
205
|
-
});
|
|
206
|
-
expect(profileRes.statusCode).toBe(200);
|
|
207
|
-
|
|
208
|
-
// 4. Logout
|
|
209
|
-
const logoutRes = await app.inject({
|
|
210
|
-
method: 'POST',
|
|
211
|
-
url: '/api/v1/auth/logout',
|
|
212
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
213
|
-
});
|
|
214
|
-
expect(logoutRes.statusCode).toBe(200);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
## Test Database Setup
|
|
222
|
-
|
|
223
|
-
### Option 1: Testcontainers (Recommended)
|
|
224
|
-
Real MySQL in Docker, isolated per test suite.
|
|
225
|
-
|
|
226
|
-
```typescript
|
|
227
|
-
// __tests__/helpers/db.ts
|
|
228
|
-
import { MySqlContainer } from '@testcontainers/mysql';
|
|
229
|
-
import { PrismaClient } from '@prisma/client';
|
|
230
|
-
|
|
231
|
-
let container: MySqlContainer;
|
|
232
|
-
let prisma: PrismaClient;
|
|
233
|
-
|
|
234
|
-
export async function setupTestDB() {
|
|
235
|
-
container = await new MySqlContainer('mysql:8.0')
|
|
236
|
-
.withDatabase('test_db')
|
|
237
|
-
.start();
|
|
238
|
-
|
|
239
|
-
const DATABASE_URL = container.getConnectionUri();
|
|
240
|
-
process.env.DATABASE_URL = DATABASE_URL;
|
|
241
|
-
|
|
242
|
-
prisma = new PrismaClient();
|
|
243
|
-
await prisma.$executeRaw`SET FOREIGN_KEY_CHECKS = 0`;
|
|
244
|
-
// Run migrations
|
|
245
|
-
await prisma.$executeRaw`/* Your schema setup */`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export async function cleanupTestDB() {
|
|
249
|
-
await prisma.$disconnect();
|
|
250
|
-
await container.stop();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export async function clearDB() {
|
|
254
|
-
const tables = await prisma.$queryRaw`SHOW TABLES`;
|
|
255
|
-
for (const table of tables) {
|
|
256
|
-
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${Object.values(table)[0]}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Option 2: Separate Test Database
|
|
262
|
-
Simpler but requires manual setup.
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// __tests__/helpers/db.ts
|
|
266
|
-
export async function setupTestDB() {
|
|
267
|
-
process.env.DATABASE_URL = 'mysql://root:pass@localhost:3306/test_db';
|
|
268
|
-
await prisma.$executeRaw`DROP DATABASE IF EXISTS test_db`;
|
|
269
|
-
await prisma.$executeRaw`CREATE DATABASE test_db`;
|
|
270
|
-
// Run migrations
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
---
|
|
275
|
-
|
|
276
|
-
## Data Factories
|
|
277
|
-
|
|
278
|
-
Use factories to create consistent test data:
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
// __tests__/helpers/factory.ts
|
|
282
|
-
import { faker } from '@faker-js/faker';
|
|
283
|
-
|
|
284
|
-
export const UserFactory = {
|
|
285
|
-
build: (overrides = {}) => ({
|
|
286
|
-
id: faker.string.uuid(),
|
|
287
|
-
email: faker.internet.email(),
|
|
288
|
-
password: 'SecurePass123!',
|
|
289
|
-
createdAt: new Date(),
|
|
290
|
-
...overrides,
|
|
291
|
-
}),
|
|
292
|
-
|
|
293
|
-
create: async (overrides = {}) => {
|
|
294
|
-
const data = UserFactory.build(overrides);
|
|
295
|
-
return await prisma.user.create({ data });
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Usage in tests
|
|
300
|
-
it('should update user', async () => {
|
|
301
|
-
const user = await UserFactory.create();
|
|
302
|
-
// Test with real DB entity
|
|
303
|
-
});
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## Mocking Patterns
|
|
309
|
-
|
|
310
|
-
### Mock External APIs
|
|
311
|
-
```typescript
|
|
312
|
-
import { vi } from 'vitest';
|
|
313
|
-
|
|
314
|
-
vi.mock('../libs/stripe', () => ({
|
|
315
|
-
stripeClient: {
|
|
316
|
-
charges: {
|
|
317
|
-
create: vi.fn().mockResolvedValue({ id: 'ch_123', status: 'succeeded' }),
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
}));
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Mock Redis
|
|
324
|
-
```typescript
|
|
325
|
-
vi.mock('../libs/redis', () => ({
|
|
326
|
-
redis: {
|
|
327
|
-
get: vi.fn(),
|
|
328
|
-
set: vi.fn(),
|
|
329
|
-
del: vi.fn(),
|
|
330
|
-
},
|
|
331
|
-
}));
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
### Mock Environment Variables
|
|
335
|
-
```typescript
|
|
336
|
-
beforeEach(() => {
|
|
337
|
-
process.env.JWT_SECRET = 'test-secret';
|
|
338
|
-
process.env.NODE_ENV = 'test';
|
|
339
|
-
});
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
---
|
|
343
|
-
|
|
344
|
-
## Testing Best Practices
|
|
345
|
-
|
|
346
|
-
### ✅ DO:
|
|
347
|
-
- **Test edge cases**: Empty arrays, null values, boundary conditions
|
|
348
|
-
- **Use descriptive test names**: `it('should throw error when email is already taken')`
|
|
349
|
-
- **One assertion per test** (or closely related assertions)
|
|
350
|
-
- **Clean up after tests**: Reset mocks, clear DB
|
|
351
|
-
- **Use factories** for consistent test data
|
|
352
|
-
|
|
353
|
-
### ❌ DON'T:
|
|
354
|
-
- Test Prisma/Fastify internals (trust the library)
|
|
355
|
-
- Write tests that depend on execution order
|
|
356
|
-
- Mock everything (integration tests need real DB)
|
|
357
|
-
- Use production database for tests
|
|
358
|
-
- Commit `.env.test` with real credentials
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
|
|
362
|
-
## Coverage Requirements
|
|
363
|
-
|
|
364
|
-
```json
|
|
365
|
-
// package.json
|
|
366
|
-
{
|
|
367
|
-
"scripts": {
|
|
368
|
-
"test": "vitest run",
|
|
369
|
-
"test:watch": "vitest",
|
|
370
|
-
"test:coverage": "vitest run --coverage"
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
**Minimum Coverage:**
|
|
376
|
-
- **Services**: 80%
|
|
377
|
-
- **Controllers**: 60% (mostly happy paths)
|
|
378
|
-
- **Utilities**: 90%
|
|
379
|
-
- **Overall**: 70%
|
|
380
|
-
|
|
381
|
-
**Ignore from coverage:**
|
|
382
|
-
- Migration files
|
|
383
|
-
- Type definitions
|
|
384
|
-
- Config files
|
|
385
|
-
|
|
386
|
-
---
|
|
387
|
-
|
|
388
|
-
## CI Integration
|
|
389
|
-
|
|
390
|
-
```yaml
|
|
391
|
-
# .github/workflows/test.yml
|
|
392
|
-
name: Tests
|
|
393
|
-
on: [push, pull_request]
|
|
394
|
-
jobs:
|
|
395
|
-
test:
|
|
396
|
-
runs-on: ubuntu-latest
|
|
397
|
-
services:
|
|
398
|
-
mysql:
|
|
399
|
-
image: mysql:8.0
|
|
400
|
-
env:
|
|
401
|
-
MYSQL_ROOT_PASSWORD: test
|
|
402
|
-
MYSQL_DATABASE: test_db
|
|
403
|
-
ports:
|
|
404
|
-
- 3306:3306
|
|
405
|
-
steps:
|
|
406
|
-
- uses: actions/checkout@v3
|
|
407
|
-
- uses: actions/setup-node@v3
|
|
408
|
-
- run: npm ci
|
|
409
|
-
- run: npm run test:coverage
|
|
410
|
-
- run: npm run test:e2e
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
---
|
|
414
|
-
|
|
415
|
-
## Common Test Scenarios
|
|
416
|
-
|
|
417
|
-
### Testing Authentication
|
|
418
|
-
```typescript
|
|
419
|
-
describe('Protected Routes', () => {
|
|
420
|
-
it('should reject requests without token', async () => {
|
|
421
|
-
const res = await app.inject({
|
|
422
|
-
method: 'GET',
|
|
423
|
-
url: '/api/v1/users/me',
|
|
424
|
-
});
|
|
425
|
-
expect(res.statusCode).toBe(401);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('should accept valid token', async () => {
|
|
429
|
-
const token = generateTestToken({ userId: '123' });
|
|
430
|
-
const res = await app.inject({
|
|
431
|
-
method: 'GET',
|
|
432
|
-
url: '/api/v1/users/me',
|
|
433
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
434
|
-
});
|
|
435
|
-
expect(res.statusCode).toBe(200);
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
### Testing Pagination
|
|
441
|
-
```typescript
|
|
442
|
-
it('should paginate results correctly', async () => {
|
|
443
|
-
await UserFactory.create({ count: 25 }); // Create 25 users
|
|
444
|
-
|
|
445
|
-
const res = await app.inject({
|
|
446
|
-
method: 'GET',
|
|
447
|
-
url: '/api/v1/users?page=2&limit=10',
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
const { data } = res.json();
|
|
451
|
-
expect(data.items).toHaveLength(10);
|
|
452
|
-
expect(data.pagination).toMatchObject({
|
|
453
|
-
page: 2,
|
|
454
|
-
limit: 10,
|
|
455
|
-
totalItems: 25,
|
|
456
|
-
totalPages: 3,
|
|
457
|
-
hasNextPage: true,
|
|
458
|
-
hasPreviousPage: true,
|
|
459
|
-
});
|
|
460
|
-
});
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
### Testing Transactions
|
|
464
|
-
```typescript
|
|
465
|
-
it('should rollback on error', async () => {
|
|
466
|
-
await expect(
|
|
467
|
-
prisma.$transaction(async (tx) => {
|
|
468
|
-
await tx.user.create({ data: { email: 'test@example.com' } });
|
|
469
|
-
throw new Error('Simulated error');
|
|
470
|
-
})
|
|
471
|
-
).rejects.toThrow();
|
|
472
|
-
|
|
473
|
-
const users = await prisma.user.findMany();
|
|
474
|
-
expect(users).toHaveLength(0); // Transaction rolled back
|
|
475
|
-
});
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
|
-
## Debugging Tests
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
// Add logging to failing tests
|
|
484
|
-
it.only('should debug this specific test', async () => {
|
|
485
|
-
console.log('Request payload:', payload);
|
|
486
|
-
const res = await app.inject({ ... });
|
|
487
|
-
console.log('Response:', res.json());
|
|
488
|
-
expect(res.statusCode).toBe(200);
|
|
489
|
-
});
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
**Vitest UI** for visual debugging:
|
|
493
|
-
```bash
|
|
494
|
-
npm run test -- --ui
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
---
|
|
498
|
-
|
|
499
|
-
## Checklist
|
|
500
|
-
|
|
501
|
-
- [ ] Unit tests for all services
|
|
502
|
-
- [ ] Integration tests for all API endpoints
|
|
503
|
-
- [ ] E2E test for critical user flows
|
|
504
|
-
- [ ] 70%+ code coverage
|
|
505
|
-
- [ ] Tests pass in CI
|
|
506
|
-
- [ ] No hardcoded credentials in tests
|
|
507
|
-
- [ ] Test DB isolated from development DB
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
trigger: always_on
|
|
3
|
-
globs: "server/**/*"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
> **SCOPE**: These rules apply specifically to the **server** directory.
|
|
7
|
-
|
|
8
|
-
# Observability, Logging & Monitoring
|
|
9
|
-
|
|
10
|
-
## Logging Stack
|
|
11
|
-
|
|
12
|
-
```json
|
|
13
|
-
{
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"pino": "^8.16.0",
|
|
16
|
-
"pino-pretty": "^10.2.0"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Logger Setup
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
// libs/logger.ts
|
|
25
|
-
import pino from 'pino';
|
|
26
|
-
|
|
27
|
-
export const logger = pino({
|
|
28
|
-
level: process.env.LOG_LEVEL || 'info',
|
|
29
|
-
transport: process.env.NODE_ENV === 'development'
|
|
30
|
-
? { target: 'pino-pretty', options: { colorize: true } }
|
|
31
|
-
: undefined,
|
|
32
|
-
redact: {
|
|
33
|
-
paths: ['req.headers.authorization', '*.password', '*.token'],
|
|
34
|
-
censor: '[REDACTED]',
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Fastify Integration
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
// app.ts
|
|
43
|
-
export const app = fastify({
|
|
44
|
-
logger: {
|
|
45
|
-
level: 'info',
|
|
46
|
-
serializers: {
|
|
47
|
-
req: (req) => ({ method: req.method, url: req.url, requestId: req.id }),
|
|
48
|
-
res: (res) => ({ statusCode: res.statusCode }),
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
requestIdHeader: 'x-request-id',
|
|
52
|
-
genReqId: (req) => req.headers['x-request-id'] || crypto.randomUUID(),
|
|
53
|
-
});
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Log Levels
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
logger.info({ userId }, 'Normal operations');
|
|
60
|
-
logger.warn({ attempts: 3 }, 'Approaching limit');
|
|
61
|
-
logger.error({ error, userId }, 'Errors needing attention');
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**Production:** `info` | **Development:** `debug`
|
|
65
|
-
|
|
66
|
-
## Error Logging
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
app.setErrorHandler((error, request, reply) => {
|
|
70
|
-
request.log.error({
|
|
71
|
-
err: error,
|
|
72
|
-
requestId: request.id,
|
|
73
|
-
userId: request.user?.id,
|
|
74
|
-
}, error.message);
|
|
75
|
-
|
|
76
|
-
reply.status(error.statusCode || 500).send({
|
|
77
|
-
success: false,
|
|
78
|
-
error: { code: error.code || 'INTERNAL_ERROR', message: error.message },
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Slow Query Logging
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
// libs/db.ts
|
|
87
|
-
prisma.$on('query', (e) => {
|
|
88
|
-
if (e.duration > 1000) {
|
|
89
|
-
logger.warn({ query: e.query, duration: e.duration }, 'Slow query');
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Error Tracking (Sentry)
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
// libs/sentry.ts
|
|
98
|
-
import * as Sentry from '@sentry/node';
|
|
99
|
-
|
|
100
|
-
export function initSentry() {
|
|
101
|
-
if (process.env.NODE_ENV === 'production') {
|
|
102
|
-
Sentry.init({
|
|
103
|
-
dsn: process.env.SENTRY_DSN,
|
|
104
|
-
tracesSampleRate: 0.1,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Health Check Endpoint
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
app.get('/health', async (request, reply) => {
|
|
114
|
-
const checks = {
|
|
115
|
-
status: 'healthy',
|
|
116
|
-
database: 'unknown',
|
|
117
|
-
redis: 'unknown',
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
await prisma.$queryRaw`SELECT 1`;
|
|
122
|
-
checks.database = 'connected';
|
|
123
|
-
} catch {
|
|
124
|
-
checks.database = 'disconnected';
|
|
125
|
-
checks.status = 'unhealthy';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
await redis.ping();
|
|
130
|
-
checks.redis = 'connected';
|
|
131
|
-
} catch {
|
|
132
|
-
checks.redis = 'disconnected';
|
|
133
|
-
checks.status = 'unhealthy';
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return reply.status(checks.status === 'healthy' ? 200 : 503).send(checks);
|
|
137
|
-
});
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Performance Tracking
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
app.addHook('onRequest', (request, reply, done) => {
|
|
144
|
-
request.startTime = Date.now();
|
|
145
|
-
done();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
app.addHook('onResponse', (request, reply, done) => {
|
|
149
|
-
const duration = Date.now() - request.startTime;
|
|
150
|
-
|
|
151
|
-
if (duration > 500) {
|
|
152
|
-
logger.warn({ url: request.url, duration }, 'Slow response');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
done();
|
|
156
|
-
});
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Best Practices
|
|
160
|
-
|
|
161
|
-
### ✅ DO:
|
|
162
|
-
- Use structured logging (JSON)
|
|
163
|
-
- Include request IDs for tracing
|
|
164
|
-
- Redact sensitive data
|
|
165
|
-
- Log errors with full context
|
|
166
|
-
- Monitor response times
|
|
167
|
-
|
|
168
|
-
### ❌ DON'T:
|
|
169
|
-
- Log passwords, tokens, API keys
|
|
170
|
-
- Use `console.log` in production
|
|
171
|
-
- Log sensitive user data
|
|
172
|
-
- Skip error context
|
|
173
|
-
|
|
174
|
-
## Checklist
|
|
175
|
-
|
|
176
|
-
- [ ] Pino logger configured
|
|
177
|
-
- [ ] Request ID tracking enabled
|
|
178
|
-
- [ ] Sensitive data redacted
|
|
179
|
-
- [ ] Health check endpoint
|
|
180
|
-
- [ ] Slow query logging
|
|
181
|
-
- [ ] Error tracking (Sentry) in production
|