create-warlock 4.2.5 → 4.2.7
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/CHANGELOG.md +14 -0
- package/bin/create-app.js +5 -0
- package/package.json +2 -2
- package/templates/warlock/.env.example +36 -0
- package/templates/warlock/.gitattributes +1 -0
- package/templates/warlock/.husky/pre-commit +4 -0
- package/templates/warlock/.prettierignore +4 -0
- package/templates/warlock/.prettierrc.json +10 -0
- package/templates/warlock/.vscode/settings.json +41 -0
- package/templates/warlock/README.md +57 -0
- package/templates/warlock/_.gitignore +6 -0
- package/templates/warlock/docs/new-module.md +551 -0
- package/templates/warlock/eslint.config.js +98 -0
- package/templates/warlock/package.json +74 -0
- package/templates/warlock/public/home.css +523 -0
- package/templates/warlock/skills/api-design/SKILL.md +461 -0
- package/templates/warlock/skills/code-standards/SKILL.md +595 -0
- package/templates/warlock/skills/data-and-persistence/SKILL.md +330 -0
- package/templates/warlock/skills/git-workflow/SKILL.md +282 -0
- package/templates/warlock/skills/module-boundaries/SKILL.md +283 -0
- package/templates/warlock/skills/observability-and-resilience/SKILL.md +306 -0
- package/templates/warlock/skills/security-baseline/SKILL.md +352 -0
- package/templates/warlock/skills/testing-strategy/SKILL.md +323 -0
- package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +28 -0
- package/templates/warlock/src/app/auth/controllers/login.controller.ts +22 -0
- package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +16 -0
- package/templates/warlock/src/app/auth/controllers/logout.controller.ts +16 -0
- package/templates/warlock/src/app/auth/controllers/me.controller.ts +13 -0
- package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +29 -0
- package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +23 -0
- package/templates/warlock/src/app/auth/main.ts +9 -0
- package/templates/warlock/src/app/auth/models/otp/index.ts +1 -0
- package/templates/warlock/src/app/auth/models/otp/migrations/22-12-2025_10-30-20.otp-migration.ts +30 -0
- package/templates/warlock/src/app/auth/models/otp/otp.model.ts +69 -0
- package/templates/warlock/src/app/auth/requests/guarded.request.ts +10 -0
- package/templates/warlock/src/app/auth/routes.ts +22 -0
- package/templates/warlock/src/app/auth/schema/login.schema.ts +8 -0
- package/templates/warlock/src/app/auth/schema/reset-password.schema.ts +9 -0
- package/templates/warlock/src/app/auth/services/auth.service.ts +66 -0
- package/templates/warlock/src/app/auth/services/forgot-password.service.ts +28 -0
- package/templates/warlock/src/app/auth/services/otp.service.ts +173 -0
- package/templates/warlock/src/app/auth/services/reset-password.service.ts +39 -0
- package/templates/warlock/src/app/auth/utils/auth-error-code.ts +6 -0
- package/templates/warlock/src/app/auth/utils/locales.ts +89 -0
- package/templates/warlock/src/app/auth/utils/types.ts +14 -0
- package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +21 -0
- package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +30 -0
- package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +15 -0
- package/templates/warlock/src/app/posts/models/post/post.model.ts +23 -0
- package/templates/warlock/src/app/posts/resources/post.resource.ts +14 -0
- package/templates/warlock/src/app/posts/routes.ts +8 -0
- package/templates/warlock/src/app/posts/schema/create-post.schema.ts +9 -0
- package/templates/warlock/src/app/posts/schema/update-post.schema.ts +9 -0
- package/templates/warlock/src/app/shared/components/HomePageComponent.tsx +229 -0
- package/templates/warlock/src/app/shared/controllers/home-page.controller.ts +18 -0
- package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +8 -0
- package/templates/warlock/src/app/shared/routes.ts +4 -0
- package/templates/warlock/src/app/shared/services/scheduler.service.ts +3 -0
- package/templates/warlock/src/app/shared/tests/infrastructure.test.ts +22 -0
- package/templates/warlock/src/app/shared/utils/global-columns-schema.ts +8 -0
- package/templates/warlock/src/app/shared/utils/locales.ts +766 -0
- package/templates/warlock/src/app/shared/utils/router.ts +30 -0
- package/templates/warlock/src/app/uploads/controllers/fetch-uploaded-file.controller.ts +33 -0
- package/templates/warlock/src/app/uploads/routes.ts +4 -0
- package/templates/warlock/src/app/users/commands/hello-world.command.ts +8 -0
- package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +27 -0
- package/templates/warlock/src/app/users/controllers/list-users.controller.ts +12 -0
- package/templates/warlock/src/app/users/events/inject-created-by-user.into-model.event.ts +32 -0
- package/templates/warlock/src/app/users/events/sync.ts +5 -0
- package/templates/warlock/src/app/users/main.ts +5 -0
- package/templates/warlock/src/app/users/models/user/index.ts +1 -0
- package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +15 -0
- package/templates/warlock/src/app/users/models/user/user.model.ts +64 -0
- package/templates/warlock/src/app/users/repositories/users.repository.ts +23 -0
- package/templates/warlock/src/app/users/resources/user.resource.ts +14 -0
- package/templates/warlock/src/app/users/routes.ts +8 -0
- package/templates/warlock/src/app/users/schema/create-user.schema.ts +11 -0
- package/templates/warlock/src/app/users/seeds/users.seed.ts +21 -0
- package/templates/warlock/src/app/users/services/get-users.service.ts +5 -0
- package/templates/warlock/src/app/users/services/list-users.service.ts +17 -0
- package/templates/warlock/src/app/users/services/login-social.ts +19 -0
- package/templates/warlock/src/config/app.ts +12 -0
- package/templates/warlock/src/config/auth.ts +20 -0
- package/templates/warlock/src/config/cache.ts +59 -0
- package/templates/warlock/src/config/database.ts +65 -0
- package/templates/warlock/src/config/http.ts +23 -0
- package/templates/warlock/src/config/log.ts +22 -0
- package/templates/warlock/src/config/mail.ts +16 -0
- package/templates/warlock/src/config/repository.ts +11 -0
- package/templates/warlock/src/config/storage.ts +34 -0
- package/templates/warlock/src/config/tests.ts +5 -0
- package/templates/warlock/src/config/validation.ts +7 -0
- package/templates/warlock/storage/.gitignore +2 -0
- package/templates/warlock/tsconfig.json +27 -0
- package/templates/warlock/warlock.config.ts +15 -0
- package/templates/warlock/yarn.lock +2332 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-baseline
|
|
3
|
+
description: 'Project-level security floor for Warlock.js apps — input validation at every boundary via `@warlock.js/seal` schemas, authentication via `@warlock.js/auth` (JWT + refresh) with `userType` config, server-side authorization never trusting client-supplied `userId`/`organizationId`, secrets only via `env(...)` + typed config files (never `process.env.X` scattered), parameterized queries through Cascade ORM (never raw concatenation), rate limiting via `@fastify/rate-limit` configured in `config/http.ts`, PII / token redaction in logs (`log.configure({ redact })`), `bcryptjs` for password hashing, `@mongez/encryption` for symmetric encryption, dependency vulnerability scanning (`yarn audit`). Triggers: writing a route handler / controller that accepts external input; touching auth, login, signup, session, token, JWT, password, refresh-token; reading secrets / API keys / env vars; writing a Cascade query or repository filter; handling file uploads, webhook payloads, queue messages; setting rate limits; user asks "is this secure", "input validation", "SQL / NoSQL injection", "XSS", "CSRF", "rate limiting", "secrets management", "authentication vs authorization", "where does X env var go"; importing `@warlock.js/seal` or `@warlock.js/auth` or `bcryptjs`. Skip: pure UI / styling that handles no data; code-style questions (load `skills/code-standards/SKILL.md`); HTTP envelope shape (load `skills/api-design/SKILL.md`); cryptography internals / writing your own crypto — stop and use a vetted lib; advanced auth flows (OAuth, SAML) — load `@warlock.js/social-auth` skill.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Security baseline
|
|
7
|
+
|
|
8
|
+
**Status:** Stable
|
|
9
|
+
**Applies to:** Every code path that touches external input, identity, secrets, persistence, or the public network.
|
|
10
|
+
|
|
11
|
+
The non-negotiable security floor for any project on this template. Each rule maps to a real class of incident — most production breaches trace back to one of these eight being absent or sloppy.
|
|
12
|
+
|
|
13
|
+
> **Sub-agent rule:** Before writing any code that handles external input, identity, or secrets, read this file. Code-style rules live in `skills/code-standards/SKILL.md` — this skill is about what the code must do, not how it must look.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Input trust model
|
|
18
|
+
|
|
19
|
+
TypeScript types are runtime lies. The only thing standing between a malicious payload and your service is a runtime validator. Use `@warlock.js/seal`.
|
|
20
|
+
|
|
21
|
+
### 1.1 What counts as untrusted input
|
|
22
|
+
|
|
23
|
+
Anything that crosses the process boundary:
|
|
24
|
+
|
|
25
|
+
- Request bodies, query params, path params, headers
|
|
26
|
+
- File uploads (the binary AND the filename AND the MIME type)
|
|
27
|
+
- Webhook payloads
|
|
28
|
+
- Queue messages
|
|
29
|
+
- Env vars (validated once at boot — see § 3)
|
|
30
|
+
- Database rows from a *different* service (treat them as external if you don't own the writer)
|
|
31
|
+
|
|
32
|
+
### 1.2 Where validation runs
|
|
33
|
+
|
|
34
|
+
At the boundary, exactly once. Never inside services.
|
|
35
|
+
|
|
36
|
+
The Warlock pattern: schema lives in the module's `schema/` folder, the controller types itself with the schema and attaches it via `controller.validation.schema`. See `skills/api-design/SKILL.md` § 6 for the full controller shape.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// ✅ schema/create-order.schema.ts
|
|
40
|
+
import { v, type Infer } from "@warlock.js/seal";
|
|
41
|
+
|
|
42
|
+
export const createOrderSchema = v.object({
|
|
43
|
+
cartId: v.string(),
|
|
44
|
+
paymentToken: v.string(),
|
|
45
|
+
shippingAddress: v.object({
|
|
46
|
+
street: v.string().min(1),
|
|
47
|
+
city: v.string().min(1),
|
|
48
|
+
postalCode: v.string().min(1),
|
|
49
|
+
country: v.string().min(2).max(2),
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type CreateOrderSchema = Infer<typeof createOrderSchema>;
|
|
54
|
+
|
|
55
|
+
// ✅ controller — framework rejects invalid input with 422 before the handler runs
|
|
56
|
+
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
57
|
+
import { type CreateOrderSchema, createOrderSchema } from "../schema/create-order.schema";
|
|
58
|
+
|
|
59
|
+
export const createOrderController: GuardedRequestHandler<CreateOrderSchema> = async (
|
|
60
|
+
request,
|
|
61
|
+
response,
|
|
62
|
+
) => {
|
|
63
|
+
const order = await placeOrderService(request.validated()); // already typed + safe
|
|
64
|
+
return response.success({ order });
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
createOrderController.validation = { schema: createOrderSchema };
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The framework returns `422 Unprocessable Entity` with structured field errors automatically. **Never** catch validation errors in the controller — the boundary handles them.
|
|
71
|
+
|
|
72
|
+
### 1.3 File uploads
|
|
73
|
+
|
|
74
|
+
- Cap the size at the route level (`fileUploadLimit` in `config/http.ts` is the project floor; tighten per-route).
|
|
75
|
+
- Sniff the real MIME type from the bytes; don't trust the `Content-Type` header.
|
|
76
|
+
- Never use the client-supplied filename as a path component — generate a server-side identifier.
|
|
77
|
+
- Quarantine until processed; scan if user-visible.
|
|
78
|
+
|
|
79
|
+
### 1.4 Webhook payloads
|
|
80
|
+
|
|
81
|
+
- **Verify the signature first**, schema-validate second. A signature failure aborts before any other work.
|
|
82
|
+
- Reject requests older than the provider's replay window (typically 5 minutes).
|
|
83
|
+
- Idempotency-key the handler (see `skills/observability-and-resilience/SKILL.md` § 4) — providers retry.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 2. Authentication vs authorization
|
|
88
|
+
|
|
89
|
+
AuthN proves who you are. AuthZ decides what you can do. Conflating them is how incidents start.
|
|
90
|
+
|
|
91
|
+
### 2.1 Identity comes from the session, never the request body
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// ❌ trusting the client
|
|
95
|
+
const order = await ordersRepository.find({ id, userId: request.input("userId") });
|
|
96
|
+
|
|
97
|
+
// ✅ identity from the authenticated session
|
|
98
|
+
const user = request.user;
|
|
99
|
+
const order = await ordersRepository.find({ id, userId: user.id });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Any field whose value would determine permission (`userId`, `organizationId`, `roleId`, `isAdmin`) is read from the authenticated session — never from the request body, query, or header.
|
|
103
|
+
|
|
104
|
+
### 2.2 Authentication via `@warlock.js/auth`
|
|
105
|
+
|
|
106
|
+
The project's auth stack: JWT access tokens (1h) + refresh tokens (30d), configured in `config/auth.ts`. Password hashing via `bcryptjs`. The framework's `authService` (`@warlock.js/auth`) is the single entry point:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// ✅ thin wrapper service — the framework does the work
|
|
110
|
+
import { authService } from "@warlock.js/auth";
|
|
111
|
+
import { User } from "app/users/models/user/user.model";
|
|
112
|
+
|
|
113
|
+
export async function loginService(credentials, deviceInfo) {
|
|
114
|
+
return authService.login(User, credentials, deviceInfo);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2.3 Authorization happens server-side, every endpoint, every time
|
|
119
|
+
|
|
120
|
+
A `guarded(() => { ... })` wrapper around routes enforces authentication. **Authorization is separate** — being authenticated proves identity, not permission.
|
|
121
|
+
|
|
122
|
+
Pattern: at the start of every mutating service, check ownership and role:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// ✅
|
|
126
|
+
export async function updateOrderService(orderId: string, input, user) {
|
|
127
|
+
const order = await ordersRepository.find({ id: orderId });
|
|
128
|
+
|
|
129
|
+
if (!order) {
|
|
130
|
+
throw new OrderNotFoundError(orderId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (order.userId !== user.id && !user.isAdmin) {
|
|
134
|
+
throw new ForbiddenError("not your order");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ... mutate
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Resource ownership checks live in the service, not the controller. The controller only orchestrates.
|
|
142
|
+
|
|
143
|
+
### 2.4 Role checks
|
|
144
|
+
|
|
145
|
+
Use centralized role helpers, never inline string comparisons (`user.role === "admin"` is a typo waiting to happen). Define roles as an enum in `app/shared/utils/enums.ts`.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 3. Secrets management
|
|
150
|
+
|
|
151
|
+
### 3.1 `.env` is never committed
|
|
152
|
+
|
|
153
|
+
- `.env.local` and `.env` are in `.gitignore` from day one.
|
|
154
|
+
- `.env.example` is committed and documents every variable the app needs.
|
|
155
|
+
- Production secrets come from the deployment platform's secret manager (Doppler, AWS Secrets Manager, Kubernetes secrets), not files on disk.
|
|
156
|
+
|
|
157
|
+
### 3.2 Secrets enter through validated config
|
|
158
|
+
|
|
159
|
+
The pattern is already established in `src/config/*.ts`. Every config file reads `env(...)` and exports a typed object. Never read `process.env.X` outside `config/`.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// ✅ config/auth.ts — the project's actual pattern
|
|
163
|
+
import { type AuthConfigurations } from "@warlock.js/auth";
|
|
164
|
+
import { env } from "@warlock.js/core";
|
|
165
|
+
|
|
166
|
+
const authConfigurations: AuthConfigurations = {
|
|
167
|
+
userType: { user: User },
|
|
168
|
+
jwt: {
|
|
169
|
+
secret: env("JWT_SECRET"),
|
|
170
|
+
expiresIn: "1h",
|
|
171
|
+
refresh: { /* ... */ },
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A missing critical env var should fail loudly at boot. Don't silently fall back to a default for security-relevant values (`JWT_SECRET` has no sensible default).
|
|
177
|
+
|
|
178
|
+
### 3.3 Secrets never appear in logs
|
|
179
|
+
|
|
180
|
+
The logger redaction layer is the safety net, but the first line of defence is not putting them in context at all.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// ❌
|
|
184
|
+
log.info("auth", "login", "User authenticated", { user, password, token });
|
|
185
|
+
|
|
186
|
+
// ✅
|
|
187
|
+
log.info("auth", "login", "User authenticated", { userId: user.id });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Configure `log.configure({ redact: { paths: ["password", "token", "*.secret", "*.apiKey"] } })` at startup — see `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md`.
|
|
191
|
+
|
|
192
|
+
### 3.4 Token lifetimes
|
|
193
|
+
|
|
194
|
+
Short-lived access tokens (≤ 1h), longer-lived refresh tokens (≤ 30d) with rotation on every refresh, revocation on logout. The default in `config/auth.ts` matches this — don't extend access-token lifetime to "make it easier" for the frontend.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 4. Injection prevention
|
|
199
|
+
|
|
200
|
+
### 4.1 Cascade ORM — always parameterized
|
|
201
|
+
|
|
202
|
+
Never compose query strings from user input. Cascade's repository and query builder are parameterized by default; never reach for a raw query mode.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// ✅
|
|
206
|
+
const orders = await ordersRepository.list({ status: input.status });
|
|
207
|
+
|
|
208
|
+
// ❌ string-built filter with user value
|
|
209
|
+
const orders = await ordersRepository.raw(`status = '${input.status}'`);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 4.2 NoSQL operator injection
|
|
213
|
+
|
|
214
|
+
If a request body is `{ status: { $ne: null } }`, a naive `find({ status: input.status })` becomes a `$ne: null` match — returns every row. The seal schema is what prevents this: declaring `status: v.string()` rejects an object value at the boundary.
|
|
215
|
+
|
|
216
|
+
This applies to **query strings too**, not just bodies — a `?status[$ne]=` smuggled through the query is the same attack. That's why list endpoints validate the query with a schema and never pass raw `request.all()` to the repository (see `skills/api-design/SKILL.md` § 3.5).
|
|
217
|
+
|
|
218
|
+
This is a real reason § 1 is non-negotiable.
|
|
219
|
+
|
|
220
|
+
### 4.3 Command execution
|
|
221
|
+
|
|
222
|
+
- Avoid `exec` / `spawn` with user input entirely.
|
|
223
|
+
- If unavoidable, use the array-args form (`spawn(cmd, [arg1, arg2])`), never the string form.
|
|
224
|
+
- Validate the input against a strict allowlist.
|
|
225
|
+
|
|
226
|
+
### 4.4 Path traversal
|
|
227
|
+
|
|
228
|
+
- Never concatenate user input into a file path.
|
|
229
|
+
- Canonicalize (`path.resolve`) and verify the result starts with the expected prefix.
|
|
230
|
+
- File uploads land under a server-generated path, never a client-supplied one.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 5. Output safety
|
|
235
|
+
|
|
236
|
+
### 5.1 HTML escaping
|
|
237
|
+
|
|
238
|
+
For any user-controlled string rendered into HTML (email templates, server-rendered pages), escape at the template engine level. React Email handles this by default; if you reach for a raw string, you've taken on the responsibility.
|
|
239
|
+
|
|
240
|
+
### 5.2 JSON responses
|
|
241
|
+
|
|
242
|
+
The framework sets `Content-Type: application/json` for `response.success(...)` automatically. Don't override unless you genuinely mean to return a different type.
|
|
243
|
+
|
|
244
|
+
### 5.3 Open redirects
|
|
245
|
+
|
|
246
|
+
If you implement a "redirect back to where the user came from" feature, validate the destination URL against a whitelist of internal paths. Never blindly follow a user-supplied URL.
|
|
247
|
+
|
|
248
|
+
### 5.4 CORS
|
|
249
|
+
|
|
250
|
+
`config/http.ts` controls the CORS policy. The template default `origin: "*"` is **fine for early development** — but before production:
|
|
251
|
+
|
|
252
|
+
- Replace `*` with the explicit frontend origin(s).
|
|
253
|
+
- If credentials cross the boundary (cookies, auth headers), `*` is no longer allowed by the spec anyway.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 6. Rate limiting and abuse
|
|
258
|
+
|
|
259
|
+
### 6.1 Default limiter
|
|
260
|
+
|
|
261
|
+
`config/http.ts` ships with `rateLimit: { max: 260, duration: 60_000 }` — 260 requests per minute per IP. That's the project floor for all routes.
|
|
262
|
+
|
|
263
|
+
### 6.2 Aggressive limiter on `/auth/*`
|
|
264
|
+
|
|
265
|
+
Login, signup, password-reset, and refresh-token endpoints get a much tighter limit (e.g. 10/min/IP, 5/hour/email). Brute-force is the cheapest attack; defending it is cheap too.
|
|
266
|
+
|
|
267
|
+
### 6.3 Account-targeted limits
|
|
268
|
+
|
|
269
|
+
For `/auth/login` and `/auth/reset-password`, key the limiter on the **email or user-id** in the payload as well as the IP — otherwise an attacker rotating IPs has free rein on a target account.
|
|
270
|
+
|
|
271
|
+
### 6.4 Progressive lockout
|
|
272
|
+
|
|
273
|
+
After N failed login attempts:
|
|
274
|
+
- 3 failures → 1-minute delay
|
|
275
|
+
- 5 failures → 5-minute delay + email notification
|
|
276
|
+
- 10 failures → temporary account lock + manual review
|
|
277
|
+
|
|
278
|
+
Never silently succeed-and-fail (returning 200 with no body to hide whether the account exists) — that's a UX disaster and doesn't actually slow attackers. Honest `401` + rate limiter is the right answer.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 7. Dependency hygiene
|
|
283
|
+
|
|
284
|
+
### 7.1 `yarn audit` in CI
|
|
285
|
+
|
|
286
|
+
CI runs `yarn audit --level=high` on every PR. High and critical vulnerabilities block merge. Moderate goes on the backlog with a fix-by date.
|
|
287
|
+
|
|
288
|
+
### 7.2 Lockfile committed
|
|
289
|
+
|
|
290
|
+
`yarn.lock` is checked in. Always. CI installs with `--frozen-lockfile`.
|
|
291
|
+
|
|
292
|
+
### 7.3 No `latest` in `package.json`
|
|
293
|
+
|
|
294
|
+
Every dependency is pinned or carets-pinned. Never `"some-dep": "latest"` — that's a supply-chain attack waiting to happen.
|
|
295
|
+
|
|
296
|
+
### 7.4 Prefer framework / std lib
|
|
297
|
+
|
|
298
|
+
Every dependency is future risk. Before adding a 5-line utility, ask whether it's already covered by `@warlock.js/*`, `@mongez/*`, or the standard library.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 8. Logging and PII
|
|
303
|
+
|
|
304
|
+
See `skills/code-standards/SKILL.md` § 8 for the call signature. This is the **what not to log** list:
|
|
305
|
+
|
|
306
|
+
### 8.1 Never log
|
|
307
|
+
|
|
308
|
+
- Passwords (raw or hashed), API keys, JWT tokens, session ids, refresh tokens
|
|
309
|
+
- Authorization headers
|
|
310
|
+
- OTP codes
|
|
311
|
+
- Payment data — card numbers, CVVs, card-holder data of any kind
|
|
312
|
+
- Cookies
|
|
313
|
+
|
|
314
|
+
### 8.2 Redact unless required
|
|
315
|
+
|
|
316
|
+
- Personal identifiers: email, phone, full name, address
|
|
317
|
+
- IP address (legal in some jurisdictions, regulated in others — check)
|
|
318
|
+
- Device identifiers
|
|
319
|
+
|
|
320
|
+
When a debugging incident genuinely needs personal data, route it through a separate, access-controlled channel — never the default log stream.
|
|
321
|
+
|
|
322
|
+
### 8.3 Configure once
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
log.configure({
|
|
326
|
+
redact: {
|
|
327
|
+
paths: ["password", "token", "*.secret", "*.apiKey", "authorization", "cookie"],
|
|
328
|
+
},
|
|
329
|
+
channels: [/* ... */],
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
See `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md` for the additive per-channel layer.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 9. Review checklist
|
|
338
|
+
|
|
339
|
+
Before merging a change that touches input, identity, secrets, or persistence:
|
|
340
|
+
|
|
341
|
+
- [ ] Every untrusted input has a `@warlock.js/seal` schema attached at the controller
|
|
342
|
+
- [ ] No identity field (`userId`, `organizationId`, role) is read from the request body
|
|
343
|
+
- [ ] Ownership / authorization check runs in the service, not the controller
|
|
344
|
+
- [ ] No `process.env.X` outside the `config/` folder
|
|
345
|
+
- [ ] No secret value appears in any log call
|
|
346
|
+
- [ ] All DB queries go through Cascade — no raw string-built queries
|
|
347
|
+
- [ ] Rate limit appropriate for the route family (default for general, aggressive for auth)
|
|
348
|
+
- [ ] No `.env` or secrets file in the commit
|
|
349
|
+
- [ ] `yarn audit --level=high` passes
|
|
350
|
+
- [ ] Logger redaction config covers any new sensitive field introduced
|
|
351
|
+
- [ ] File uploads cap size, sniff MIME, server-generate the stored filename
|
|
352
|
+
- [ ] Webhooks verify signature before any other work
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-strategy
|
|
3
|
+
description: 'Test pyramid posture for Warlock.js apps — Vitest co-located (`foo.ts` → `foo.spec.ts`); **when testing is enabled in the project (a `test`/`test:*` script in package.json OR a `vitest.config.*` / `vite.config.*` file), it is mandatory that every service ships a co-located `.service.spec.ts` and every HTTP controller / request handler ships a test — a new service or endpoint without a spec is incomplete**; unit tests dominate (~70%), integration tests via `@testcontainers/mongodb` + `@testcontainers/postgresql` (~25%), e2e for critical user journeys only (~5%); coverage is a smoke detector not a target (`yarn test:coverage` via `@vitest/coverage-v8`); flaky-test zero-tolerance (quarantine in 24h, fix or delete in 7); test isolation (no shared state, no order dependency); AAA structure with blank-line phases; mock at the boundary not deep inside; `MockSDK` from `@warlock.js/ai` for AI calls; real DB via testcontainers for integration; snapshot tests only for small deterministic output; contract tests for external integrations. Triggers: deciding whether to write a unit / integration / e2e test for a change; setting up a new `.spec.ts`; debugging a flaky test; reviewing the test plan in a PR; user asks "should I unit test this", "what is our coverage policy", "this test keeps failing intermittently", "what should I mock", "test pyramid", "integration test vs unit test", "snapshot tests", "how do I test a service that hits the DB", "how do I test AI code". Skip: Vitest assertion API mechanics — that''s the testing framework''s docs; language / runtime questions; load testing / performance benchmarking; framework-package internals — load the relevant `@warlock.js/*` skill.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing strategy
|
|
7
|
+
|
|
8
|
+
**Status:** Stable
|
|
9
|
+
**Applies to:** Every test in the project, and every code change that should but doesn't have one.
|
|
10
|
+
|
|
11
|
+
Tests are insurance, not ceremony. The right tests prevent the right bugs at the right cost. Wrong-shape tests slow the team down and rot until everyone ignores them.
|
|
12
|
+
|
|
13
|
+
> **Sub-agent rule:** Before writing tests, read this file to decide what kind of test belongs here.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 0. When this skill applies — and the mandate
|
|
18
|
+
|
|
19
|
+
This skill governs projects that have **opted into testing**. Testing is **enabled** when **either** is true:
|
|
20
|
+
|
|
21
|
+
- `package.json` has a `test` (or `test:*`) script, **or**
|
|
22
|
+
- a `vitest.config.*` / `vite.config.*` file exists at the project root.
|
|
23
|
+
|
|
24
|
+
When the `test` feature is selected at scaffold time, both land automatically (Vitest, `@vitest/coverage-v8`, testcontainers, and a per-worker DB/cache test setup).
|
|
25
|
+
|
|
26
|
+
### 0.1 If testing is enabled, tests are mandatory — not optional
|
|
27
|
+
|
|
28
|
+
- **Every service** ships a co-located unit spec: `place-order.service.ts` → `place-order.service.spec.ts`. Cover the happy path, every branch, and every error path. Mock the repository — the service's logic is the unit under test.
|
|
29
|
+
- **Every HTTP controller / request handler** ships a test: a unit test with mocked services for its branch / validation / authorization logic, **or** an integration test through the route when the wiring is the point. A new endpoint without a test is an incomplete endpoint.
|
|
30
|
+
|
|
31
|
+
Write the spec in the **same change** that adds the service or controller. A reviewer treats a missing spec as a blocking defect, not a follow-up.
|
|
32
|
+
|
|
33
|
+
### 0.2 If testing is NOT enabled
|
|
34
|
+
|
|
35
|
+
No `test` script and no vitest/vite config — e.g. a scaffold created without the `test` feature. **Do not** add tests or testing dependencies unprompted, and don't invent a `vitest.config`. If the project is outgrowing prototype stage, *suggest* enabling the `test` feature — but never scaffold a suite the project didn't ask for.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. The pyramid
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
/\
|
|
43
|
+
/ \ e2e (~5%) — slow, full system, critical journeys only
|
|
44
|
+
/----\
|
|
45
|
+
/ \ integration (~25%) — real DB / queue, exercises wiring
|
|
46
|
+
/--------\
|
|
47
|
+
/ \ unit (~70%) — fast, isolated, pure logic
|
|
48
|
+
/____________\
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 1.1 Unit — most of the suite
|
|
52
|
+
|
|
53
|
+
- Services with mocked repositories
|
|
54
|
+
- Utility functions (pure)
|
|
55
|
+
- Model getters, validators, computed fields
|
|
56
|
+
- Resource mappers
|
|
57
|
+
- Error class construction
|
|
58
|
+
|
|
59
|
+
Fast (< 50ms each), isolated (no I/O), pinned to a single function or class.
|
|
60
|
+
|
|
61
|
+
### 1.2 Integration — real wiring
|
|
62
|
+
|
|
63
|
+
- Services + real DB (via testcontainers)
|
|
64
|
+
- Repository queries against a real DB
|
|
65
|
+
- Queue producer + consumer wired together
|
|
66
|
+
- Auth flow through the framework's middleware
|
|
67
|
+
|
|
68
|
+
Medium (100ms–2s each), real dependencies, fewer than unit tests.
|
|
69
|
+
|
|
70
|
+
### 1.3 E2E — critical journeys
|
|
71
|
+
|
|
72
|
+
- "User can sign up and place an order"
|
|
73
|
+
- "User can log in, hit rate-limit, recover"
|
|
74
|
+
- Payment + webhook handshake
|
|
75
|
+
|
|
76
|
+
Slow (5–30s each), small number (10–30 per app), only for journeys whose breakage is a business-impacting incident.
|
|
77
|
+
|
|
78
|
+
### 1.4 The 70 / 25 / 5 split
|
|
79
|
+
|
|
80
|
+
Not enforced numerically. Enforced culturally — when a reviewer sees ten new e2e tests for one feature, they push back.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 2. What to test, what not to
|
|
85
|
+
|
|
86
|
+
### 2.1 Test
|
|
87
|
+
|
|
88
|
+
- **Public surface of a module** — services, controllers, resources, models' public methods
|
|
89
|
+
- **Branches and edge cases** — every `if` / `else` worth its salt has a test
|
|
90
|
+
- **Error paths** — what happens when the dependency fails / the input is invalid / the user lacks permission
|
|
91
|
+
- **Security-sensitive logic** — auth, authorization, input validation, rate limiting
|
|
92
|
+
- **Money and time arithmetic** — these break silently; tests are the only safety net
|
|
93
|
+
- **Business rules** — anything where "the spec says X" matters more than "the code does Y"
|
|
94
|
+
|
|
95
|
+
### 2.2 Don't test
|
|
96
|
+
|
|
97
|
+
- **The framework** — `@warlock.js/*` has its own tests. Don't re-test that `response.success()` returns 200.
|
|
98
|
+
- **Trivial getters / setters** — `cart.totalCents` returning `this.get<number>("total_cents", 0)` doesn't need a test
|
|
99
|
+
- **One-line passthroughs** — `loginService(...)` just calling `authService.login(...)` doesn't need a unit test (an integration test of the wiring is better)
|
|
100
|
+
- **Generated code** — migrations, resource scaffolds
|
|
101
|
+
|
|
102
|
+
### 2.3 The rule of thumb
|
|
103
|
+
|
|
104
|
+
If a future change to this function could silently break behaviour someone cares about, write a test. If a future change would either be obviously broken or obviously fine, skip it.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 3. Test isolation — non-negotiable
|
|
109
|
+
|
|
110
|
+
### 3.1 No shared mutable state
|
|
111
|
+
|
|
112
|
+
Each test sets up and tears down its own world. No `let users = []` at the top of the file mutated across tests.
|
|
113
|
+
|
|
114
|
+
### 3.2 Test order independence
|
|
115
|
+
|
|
116
|
+
`vitest --shuffle` should still pass. If it doesn't, you have shared state somewhere — fix it before adding more tests.
|
|
117
|
+
|
|
118
|
+
### 3.3 Database tests
|
|
119
|
+
|
|
120
|
+
Use transactions that roll back, or use testcontainers with a fresh container per file. Never share rows across tests.
|
|
121
|
+
|
|
122
|
+
The project's testcontainers setup makes "fresh DB per test file" cheap — use it.
|
|
123
|
+
|
|
124
|
+
### 3.4 Time and randomness
|
|
125
|
+
|
|
126
|
+
Stub `Date.now()` / `Math.random()` / UUID generation when the test depends on the value. `vi.useFakeTimers()` and `vi.spyOn()` are your tools.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 4. Mocking philosophy
|
|
131
|
+
|
|
132
|
+
### 4.1 Mock at the boundary
|
|
133
|
+
|
|
134
|
+
Mock the HTTP client, not the internal functions that use it. Mock the AI provider, not the service that calls it. The further from the boundary you mock, the more your tests describe implementation rather than behaviour.
|
|
135
|
+
|
|
136
|
+
### 4.2 AI calls — `MockSDK`
|
|
137
|
+
|
|
138
|
+
Never hit a real provider in tests. Use `MockSDK` from `@warlock.js/ai`:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// ✅
|
|
142
|
+
import { MockSDK } from "@warlock.js/ai";
|
|
143
|
+
|
|
144
|
+
const ai = MockSDK({
|
|
145
|
+
responses: [{ text: "mocked response" }],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await summarizeContentService(input, { provider: ai });
|
|
149
|
+
expect(result).toBe("mocked response");
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
See `@warlock.js/ai/skills/run-ai-agent/SKILL.md` for the full mock surface.
|
|
153
|
+
|
|
154
|
+
### 4.3 Database — real, via testcontainers
|
|
155
|
+
|
|
156
|
+
For integration tests, run a real Mongo or Postgres container via `@testcontainers/mongodb` or `@testcontainers/postgresql`. The cost (a few seconds at suite start) is worth the fidelity (catching real-world query issues, real index usage, real type coercion).
|
|
157
|
+
|
|
158
|
+
For pure-unit tests of services, mock the repository — the service's logic is the unit under test, not the DB.
|
|
159
|
+
|
|
160
|
+
### 4.4 Time — stub, don't sleep
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// ❌
|
|
164
|
+
await sleep(1000);
|
|
165
|
+
|
|
166
|
+
// ✅
|
|
167
|
+
vi.useFakeTimers();
|
|
168
|
+
vi.advanceTimersByTime(1000);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
A test that sleeps is a test that's slow and flaky. Stub the clock.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 5. AAA structure
|
|
176
|
+
|
|
177
|
+
Every test has three phases, separated by blank lines:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
it("denies cart updates after checkout", async () => {
|
|
181
|
+
// Arrange
|
|
182
|
+
const cart = await cartFactory.create({ status: CartStatus.CHECKED_OUT });
|
|
183
|
+
const update = { totalCents: 99999 };
|
|
184
|
+
|
|
185
|
+
// Act
|
|
186
|
+
const promise = updateCartService(cart.id, update, mockUser);
|
|
187
|
+
|
|
188
|
+
// Assert
|
|
189
|
+
await expect(promise).rejects.toThrow(CartLockedError);
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 5.1 One assertion target per test
|
|
194
|
+
|
|
195
|
+
Multiple `expect` calls are fine if they all verify one outcome. A test that asserts five unrelated things hides which one broke.
|
|
196
|
+
|
|
197
|
+
### 5.2 Descriptive `it(...)` names
|
|
198
|
+
|
|
199
|
+
The name describes the expected behaviour, not the implementation. "rejects cart updates after checkout" beats "throws CartLockedError when status is CHECKED_OUT" (the implementation detail is irrelevant to the reader scanning the suite).
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 6. Coverage policy
|
|
204
|
+
|
|
205
|
+
### 6.1 Coverage is a smoke detector
|
|
206
|
+
|
|
207
|
+
Low coverage = guaranteed gaps. High coverage = no guarantee of correctness. Treat it as a "definitely look here" signal, not a quality metric.
|
|
208
|
+
|
|
209
|
+
### 6.2 Suggested floors
|
|
210
|
+
|
|
211
|
+
- `src/app/**/services/**` — ≥ 70% line coverage
|
|
212
|
+
- `src/app/**/utils/**` — ≥ 80% (pure code, easy to test)
|
|
213
|
+
- `src/app/**/resources/**` — no floor (output-only, low risk)
|
|
214
|
+
- `src/app/**/models/**` — no floor (mostly getters; framework tests cover the rest)
|
|
215
|
+
- `src/app/**/types/**` — no floor (no runtime behaviour)
|
|
216
|
+
|
|
217
|
+
### 6.3 Aim for "the meaningful paths are tested", not 100%
|
|
218
|
+
|
|
219
|
+
Chasing the last 5% of coverage produces brittle tests over edge-case code that nobody cares about. Spend the time on tests that catch real regressions.
|
|
220
|
+
|
|
221
|
+
### 6.4 The `yarn test:coverage` command
|
|
222
|
+
|
|
223
|
+
Runs Vitest with `@vitest/coverage-v8` (already in dependencies). Add coverage as a CI gate per the floors above once the suite is large enough.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 7. Flaky test policy — zero tolerance
|
|
228
|
+
|
|
229
|
+
A test that fails intermittently is a bug, not a nuisance.
|
|
230
|
+
|
|
231
|
+
### 7.1 The deadline
|
|
232
|
+
|
|
233
|
+
- **24 hours** to quarantine (mark `.skip` with a tracking ticket).
|
|
234
|
+
- **7 days** to fix or delete.
|
|
235
|
+
|
|
236
|
+
### 7.2 Why the deadline matters
|
|
237
|
+
|
|
238
|
+
Once a team accepts flakes, the suite dies. Every flake makes the next failure easier to dismiss ("probably flaky"). The signal-to-noise ratio collapses, real failures get ignored, the suite stops being trusted, the team stops writing tests.
|
|
239
|
+
|
|
240
|
+
Guard against this actively — flake reports are first-priority bugs.
|
|
241
|
+
|
|
242
|
+
### 7.3 Common causes
|
|
243
|
+
|
|
244
|
+
- Shared state between tests (see § 3)
|
|
245
|
+
- Real I/O without proper teardown
|
|
246
|
+
- Timing assumptions (`setTimeout`, polling)
|
|
247
|
+
- Test order dependency
|
|
248
|
+
- Race conditions in the code under test (test exposed a real bug — fix the code)
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 8. Snapshot tests
|
|
253
|
+
|
|
254
|
+
### 8.1 Use for
|
|
255
|
+
|
|
256
|
+
- Small structured output that's deterministic
|
|
257
|
+
- Error response shapes
|
|
258
|
+
- Resource serialization output (small ones)
|
|
259
|
+
|
|
260
|
+
### 8.2 Don't use for
|
|
261
|
+
|
|
262
|
+
- Large objects (the diff becomes unreadable; reviewers rubber-stamp)
|
|
263
|
+
- Non-deterministic output (timestamps, generated ids — stub them first or omit)
|
|
264
|
+
- HTML / large strings (use targeted string assertions instead)
|
|
265
|
+
|
|
266
|
+
### 8.3 Update with care
|
|
267
|
+
|
|
268
|
+
`vitest -u` regenerates snapshots. **Read every line of the diff before committing the update.** A blind `-u` is how silent regressions land.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 9. Contract tests
|
|
273
|
+
|
|
274
|
+
### 9.1 What and why
|
|
275
|
+
|
|
276
|
+
For any external integration whose shape might drift (third-party API, partner webhook):
|
|
277
|
+
|
|
278
|
+
- **Provider contract** — what we send, what we expect back
|
|
279
|
+
- **Consumer contract** — what they send us, what we expect to handle
|
|
280
|
+
|
|
281
|
+
Run against a recorded fixture in CI (fast, deterministic). Run against the real endpoint in a nightly job — if their API drifts, you catch it within 24h instead of in production.
|
|
282
|
+
|
|
283
|
+
### 9.2 Where they live
|
|
284
|
+
|
|
285
|
+
Co-located with the integration: `src/app/payments/clients/stripe-client.contract.spec.ts`.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 10. File layout
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
src/app/orders/services/
|
|
293
|
+
place-order.service.ts
|
|
294
|
+
place-order.service.spec.ts ← unit test, mocks repository
|
|
295
|
+
place-order.service.integration.spec.ts ← integration, real DB
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
- `.spec.ts` — default suffix; covers unit tests
|
|
299
|
+
- `.integration.spec.ts` — when the test needs real I/O (DB, queue)
|
|
300
|
+
- `.e2e.spec.ts` — full-system journey tests (sparse; one per critical flow)
|
|
301
|
+
|
|
302
|
+
Tests live next to the file under test, always. Never in a separate `__tests__/` or `test/` folder — colocation makes the link obvious and survives refactors.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 11. Review checklist
|
|
307
|
+
|
|
308
|
+
Before merging a change that should include tests (testing enabled — see § 0):
|
|
309
|
+
|
|
310
|
+
- [ ] Every new/changed **service** has a co-located `.service.spec.ts`
|
|
311
|
+
- [ ] Every new/changed **HTTP controller / request handler** has a test (unit with mocked services, or integration through the route)
|
|
312
|
+
- [ ] New public function has at least one test
|
|
313
|
+
- [ ] Branches / error paths / edge cases covered
|
|
314
|
+
- [ ] No real provider API hit in tests (use `MockSDK` for AI, mock HTTP clients otherwise)
|
|
315
|
+
- [ ] Integration tests use testcontainers, not mocked DBs
|
|
316
|
+
- [ ] AAA structure, blank-line phase separation
|
|
317
|
+
- [ ] One assertion target per test (where practical)
|
|
318
|
+
- [ ] No `await sleep(...)` — fake timers instead
|
|
319
|
+
- [ ] No shared mutable state between tests
|
|
320
|
+
- [ ] Test names describe behaviour, not implementation
|
|
321
|
+
- [ ] No `.only`, no `.skip` without a tracking ticket
|
|
322
|
+
- [ ] Snapshots reviewed line-by-line if updated
|
|
323
|
+
- [ ] `yarn test --run` passes; `yarn test:coverage` doesn't drop the floor
|