aigent-team 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/chunk-N3RYHWTR.js +267 -0
- package/dist/cli.js +576 -0
- package/dist/index.d.ts +234 -0
- package/dist/index.js +27 -0
- package/package.json +67 -0
- package/templates/shared/git-workflow.md +44 -0
- package/templates/shared/project-conventions.md +48 -0
- package/templates/teams/ba/agent.yaml +25 -0
- package/templates/teams/ba/references/acceptance-criteria.md +87 -0
- package/templates/teams/ba/references/api-contract-design.md +110 -0
- package/templates/teams/ba/references/requirements-analysis.md +83 -0
- package/templates/teams/ba/references/user-story-mapping.md +73 -0
- package/templates/teams/ba/skill.md +85 -0
- package/templates/teams/be/agent.yaml +34 -0
- package/templates/teams/be/conventions.md +102 -0
- package/templates/teams/be/references/api-design.md +91 -0
- package/templates/teams/be/references/async-processing.md +86 -0
- package/templates/teams/be/references/auth-security.md +58 -0
- package/templates/teams/be/references/caching.md +79 -0
- package/templates/teams/be/references/database.md +65 -0
- package/templates/teams/be/references/error-handling.md +106 -0
- package/templates/teams/be/references/observability.md +83 -0
- package/templates/teams/be/references/review-checklist.md +50 -0
- package/templates/teams/be/references/testing.md +100 -0
- package/templates/teams/be/review-checklist.md +54 -0
- package/templates/teams/be/skill.md +71 -0
- package/templates/teams/devops/agent.yaml +35 -0
- package/templates/teams/devops/conventions.md +133 -0
- package/templates/teams/devops/references/ci-cd.md +218 -0
- package/templates/teams/devops/references/cost-optimization.md +218 -0
- package/templates/teams/devops/references/disaster-recovery.md +199 -0
- package/templates/teams/devops/references/docker.md +237 -0
- package/templates/teams/devops/references/infrastructure-as-code.md +238 -0
- package/templates/teams/devops/references/kubernetes.md +397 -0
- package/templates/teams/devops/references/monitoring.md +224 -0
- package/templates/teams/devops/references/review-checklist.md +149 -0
- package/templates/teams/devops/references/security.md +225 -0
- package/templates/teams/devops/review-checklist.md +72 -0
- package/templates/teams/devops/skill.md +131 -0
- package/templates/teams/fe/agent.yaml +28 -0
- package/templates/teams/fe/conventions.md +80 -0
- package/templates/teams/fe/references/accessibility.md +92 -0
- package/templates/teams/fe/references/component-architecture.md +87 -0
- package/templates/teams/fe/references/css-styling.md +89 -0
- package/templates/teams/fe/references/forms.md +73 -0
- package/templates/teams/fe/references/performance.md +104 -0
- package/templates/teams/fe/references/review-checklist.md +51 -0
- package/templates/teams/fe/references/security.md +90 -0
- package/templates/teams/fe/references/state-management.md +117 -0
- package/templates/teams/fe/references/testing.md +112 -0
- package/templates/teams/fe/review-checklist.md +53 -0
- package/templates/teams/fe/skill.md +68 -0
- package/templates/teams/lead/agent.yaml +18 -0
- package/templates/teams/lead/references/cross-team-coordination.md +68 -0
- package/templates/teams/lead/references/quality-gates.md +64 -0
- package/templates/teams/lead/references/task-decomposition.md +69 -0
- package/templates/teams/lead/skill.md +83 -0
- package/templates/teams/qa/agent.yaml +32 -0
- package/templates/teams/qa/conventions.md +130 -0
- package/templates/teams/qa/references/ci-integration.md +337 -0
- package/templates/teams/qa/references/e2e-testing.md +292 -0
- package/templates/teams/qa/references/mocking.md +249 -0
- package/templates/teams/qa/references/performance-testing.md +288 -0
- package/templates/teams/qa/references/review-checklist.md +143 -0
- package/templates/teams/qa/references/security-testing.md +271 -0
- package/templates/teams/qa/references/test-data.md +275 -0
- package/templates/teams/qa/references/test-strategy.md +192 -0
- package/templates/teams/qa/review-checklist.md +53 -0
- package/templates/teams/qa/skill.md +131 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Performance Testing Reference
|
|
2
|
+
|
|
3
|
+
## Load Scenario Types
|
|
4
|
+
|
|
5
|
+
| Scenario | Purpose | VUs | Duration | When to run |
|
|
6
|
+
|---|---|---|---|---|
|
|
7
|
+
| **Smoke** | Verify script works, baseline response | 1-5 | 1 min | Every PR (fast gate) |
|
|
8
|
+
| **Load** | Validate SLOs under expected traffic | Target VUs | 10-30 min | Nightly / pre-release |
|
|
9
|
+
| **Stress** | Find breaking point | Ramp beyond target | 15-30 min | Weekly / pre-release |
|
|
10
|
+
| **Soak** | Detect memory leaks, connection exhaustion | Target VUs | 2-8 hours | Weekly (off-hours) |
|
|
11
|
+
| **Spike** | Verify recovery from sudden traffic burst | 0 → peak → 0 | 5-10 min | Pre-release |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## k6 Script Examples
|
|
16
|
+
|
|
17
|
+
### Basic Load Test
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
// tests/performance/load-api.js
|
|
21
|
+
import http from 'k6/http';
|
|
22
|
+
import { check, sleep } from 'k6';
|
|
23
|
+
import { Rate, Trend } from 'k6/metrics';
|
|
24
|
+
|
|
25
|
+
const errorRate = new Rate('errors');
|
|
26
|
+
const orderLatency = new Trend('order_latency');
|
|
27
|
+
|
|
28
|
+
export const options = {
|
|
29
|
+
scenarios: {
|
|
30
|
+
load: {
|
|
31
|
+
executor: 'ramping-vus',
|
|
32
|
+
startVUs: 0,
|
|
33
|
+
stages: [
|
|
34
|
+
{ duration: '2m', target: 50 }, // ramp up
|
|
35
|
+
{ duration: '5m', target: 50 }, // steady state
|
|
36
|
+
{ duration: '2m', target: 0 }, // ramp down
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
thresholds: {
|
|
41
|
+
http_req_duration: ['p(95)<500', 'p(99)<1500'],
|
|
42
|
+
errors: ['rate<0.01'],
|
|
43
|
+
order_latency: ['p(95)<800'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
|
|
48
|
+
|
|
49
|
+
export default function () {
|
|
50
|
+
// GET — list products
|
|
51
|
+
const listRes = http.get(`${BASE_URL}/api/products`);
|
|
52
|
+
check(listRes, {
|
|
53
|
+
'list status 200': (r) => r.status === 200,
|
|
54
|
+
'list has items': (r) => JSON.parse(r.body).length > 0,
|
|
55
|
+
}) || errorRate.add(1);
|
|
56
|
+
|
|
57
|
+
sleep(1); // think time between actions
|
|
58
|
+
|
|
59
|
+
// POST — create order
|
|
60
|
+
const payload = JSON.stringify({
|
|
61
|
+
items: [{ sku: 'WIDGET-1', quantity: 1 }],
|
|
62
|
+
});
|
|
63
|
+
const orderRes = http.post(`${BASE_URL}/api/orders`, payload, {
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
});
|
|
66
|
+
orderLatency.add(orderRes.timings.duration);
|
|
67
|
+
check(orderRes, {
|
|
68
|
+
'order status 201': (r) => r.status === 201,
|
|
69
|
+
}) || errorRate.add(1);
|
|
70
|
+
|
|
71
|
+
sleep(2);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Smoke Test (CI-friendly)
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// tests/performance/smoke-api.js
|
|
79
|
+
import http from 'k6/http';
|
|
80
|
+
import { check } from 'k6';
|
|
81
|
+
|
|
82
|
+
export const options = {
|
|
83
|
+
vus: 1,
|
|
84
|
+
duration: '30s',
|
|
85
|
+
thresholds: {
|
|
86
|
+
http_req_duration: ['p(95)<300'],
|
|
87
|
+
http_req_failed: ['rate<0.01'],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default function () {
|
|
92
|
+
const res = http.get(`${__ENV.BASE_URL}/api/health`);
|
|
93
|
+
check(res, {
|
|
94
|
+
'status 200': (r) => r.status === 200,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Stress Test
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
// tests/performance/stress-api.js
|
|
103
|
+
export const options = {
|
|
104
|
+
scenarios: {
|
|
105
|
+
stress: {
|
|
106
|
+
executor: 'ramping-vus',
|
|
107
|
+
startVUs: 0,
|
|
108
|
+
stages: [
|
|
109
|
+
{ duration: '2m', target: 50 },
|
|
110
|
+
{ duration: '3m', target: 100 },
|
|
111
|
+
{ duration: '3m', target: 200 }, // beyond expected capacity
|
|
112
|
+
{ duration: '3m', target: 400 }, // find the breaking point
|
|
113
|
+
{ duration: '2m', target: 0 },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
thresholds: {
|
|
118
|
+
http_req_duration: ['p(95)<2000'], // relaxed for stress
|
|
119
|
+
http_req_failed: ['rate<0.05'], // 5% error rate acceptable
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Spike Test
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
export const options = {
|
|
128
|
+
scenarios: {
|
|
129
|
+
spike: {
|
|
130
|
+
executor: 'ramping-vus',
|
|
131
|
+
startVUs: 0,
|
|
132
|
+
stages: [
|
|
133
|
+
{ duration: '1m', target: 10 }, // baseline
|
|
134
|
+
{ duration: '10s', target: 500 }, // spike
|
|
135
|
+
{ duration: '2m', target: 500 }, // hold spike
|
|
136
|
+
{ duration: '10s', target: 10 }, // drop back
|
|
137
|
+
{ duration: '2m', target: 10 }, // recovery
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Artillery Script Example
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
# tests/performance/load.yml
|
|
150
|
+
config:
|
|
151
|
+
target: "http://localhost:3000"
|
|
152
|
+
phases:
|
|
153
|
+
- duration: 120
|
|
154
|
+
arrivalRate: 10
|
|
155
|
+
name: "Warm up"
|
|
156
|
+
- duration: 300
|
|
157
|
+
arrivalRate: 50
|
|
158
|
+
name: "Sustained load"
|
|
159
|
+
ensure:
|
|
160
|
+
p95: 500
|
|
161
|
+
maxErrorRate: 1
|
|
162
|
+
|
|
163
|
+
scenarios:
|
|
164
|
+
- name: "Browse and order"
|
|
165
|
+
flow:
|
|
166
|
+
- get:
|
|
167
|
+
url: "/api/products"
|
|
168
|
+
capture:
|
|
169
|
+
- json: "$[0].id"
|
|
170
|
+
as: "productId"
|
|
171
|
+
- think: 2
|
|
172
|
+
- post:
|
|
173
|
+
url: "/api/orders"
|
|
174
|
+
json:
|
|
175
|
+
items:
|
|
176
|
+
- productId: "{{ productId }}"
|
|
177
|
+
quantity: 1
|
|
178
|
+
expect:
|
|
179
|
+
- statusCode: 201
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Threshold Definitions
|
|
185
|
+
|
|
186
|
+
Thresholds must be derived from SLOs, not arbitrary numbers.
|
|
187
|
+
|
|
188
|
+
### Mapping SLOs to Thresholds
|
|
189
|
+
|
|
190
|
+
| SLO | k6 Threshold |
|
|
191
|
+
|---|---|
|
|
192
|
+
| 99.9 % availability | `http_req_failed: ['rate<0.001']` |
|
|
193
|
+
| p95 latency < 500 ms | `http_req_duration: ['p(95)<500']` |
|
|
194
|
+
| p99 latency < 1500 ms | `http_req_duration: ['p(99)<1500']` |
|
|
195
|
+
| Zero errors on critical path | Custom metric with `rate<0.0` |
|
|
196
|
+
|
|
197
|
+
### Custom Thresholds
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
export const options = {
|
|
201
|
+
thresholds: {
|
|
202
|
+
// Built-in metrics
|
|
203
|
+
http_req_duration: ['p(95)<500', 'p(99)<1500'],
|
|
204
|
+
http_req_failed: ['rate<0.01'],
|
|
205
|
+
|
|
206
|
+
// Custom metrics (tagged)
|
|
207
|
+
'http_req_duration{name:createOrder}': ['p(95)<800'],
|
|
208
|
+
'http_req_duration{name:listProducts}': ['p(95)<200'],
|
|
209
|
+
|
|
210
|
+
// Custom counters
|
|
211
|
+
errors: ['rate<0.01'],
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Tagging Requests
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
http.post(`${BASE_URL}/api/orders`, payload, {
|
|
220
|
+
tags: { name: 'createOrder' },
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Production-Like Environment Requirement
|
|
227
|
+
|
|
228
|
+
Performance tests are meaningless unless the environment matches production:
|
|
229
|
+
|
|
230
|
+
### Checklist
|
|
231
|
+
|
|
232
|
+
- [ ] Same instance types / resource limits (CPU, memory)
|
|
233
|
+
- [ ] Same database engine, version, and dataset size (or representative subset)
|
|
234
|
+
- [ ] Same network topology (load balancer, CDN excluded or simulated)
|
|
235
|
+
- [ ] Same connection pool sizes and timeouts
|
|
236
|
+
- [ ] Same external service latencies (mock with realistic delays)
|
|
237
|
+
- [ ] Same application configuration (caching, rate limits, feature flags)
|
|
238
|
+
|
|
239
|
+
### Data Volume
|
|
240
|
+
|
|
241
|
+
- Load a representative dataset before running tests.
|
|
242
|
+
- For a 10M-row production table, use at least 1M rows in perf test DB.
|
|
243
|
+
- Index behaviour changes with data volume — small datasets hide slow queries.
|
|
244
|
+
|
|
245
|
+
### Isolation
|
|
246
|
+
|
|
247
|
+
- Never run performance tests against production.
|
|
248
|
+
- Never share the perf environment with other test suites during a run.
|
|
249
|
+
- Reset environment state between runs for reproducibility.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Baseline Tracking
|
|
254
|
+
|
|
255
|
+
### Why Track Baselines?
|
|
256
|
+
|
|
257
|
+
A single performance test run is a snapshot. Trends tell the real story.
|
|
258
|
+
Track baselines to detect gradual degradation.
|
|
259
|
+
|
|
260
|
+
### Process
|
|
261
|
+
|
|
262
|
+
1. **Establish baseline**: Run load test 3 times on a known-good build.
|
|
263
|
+
Average the results.
|
|
264
|
+
2. **Store results**: Push metrics to a time-series database (InfluxDB,
|
|
265
|
+
Prometheus) or flat JSON files in the repo.
|
|
266
|
+
3. **Compare on PR**: Run smoke perf test, compare against baseline.
|
|
267
|
+
Fail if p95 regresses by > 20 %.
|
|
268
|
+
4. **Update baseline**: After each release, re-run and update.
|
|
269
|
+
|
|
270
|
+
### k6 Output to JSON
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
k6 run --out json=results.json tests/performance/load-api.js
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### k6 Output to InfluxDB
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
k6 run --out influxdb=http://influxdb:8086/k6 tests/performance/load-api.js
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Grafana Dashboard
|
|
283
|
+
|
|
284
|
+
Pair InfluxDB with Grafana for visual trend tracking. Key panels:
|
|
285
|
+
- p50 / p95 / p99 latency over time
|
|
286
|
+
- Error rate over time
|
|
287
|
+
- VUs vs response time (to spot saturation)
|
|
288
|
+
- Throughput (req/s) over time
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Test Code Review Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist when reviewing any PR that adds or modifies test code.
|
|
4
|
+
Not every section applies to every PR — skip sections that are not relevant.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Test Level Selection
|
|
9
|
+
|
|
10
|
+
- [ ] Test is at the lowest level that can catch the bug
|
|
11
|
+
- [ ] Unit tests do not involve I/O (network, file system, database)
|
|
12
|
+
- [ ] Integration tests are justified — they test module collaboration, not
|
|
13
|
+
logic that a unit test could cover
|
|
14
|
+
- [ ] E2E tests protect critical user journeys only — not redundant with
|
|
15
|
+
integration tests
|
|
16
|
+
- [ ] Contract tests exist for cross-service API calls
|
|
17
|
+
- [ ] Comment explains why E2E was chosen over integration (if applicable)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Test Quality
|
|
22
|
+
|
|
23
|
+
- [ ] Test name describes behaviour, not implementation
|
|
24
|
+
(`"calculates tax for multi-state order"`, not `"test calculateTax"`)
|
|
25
|
+
- [ ] Each test has a single clear assertion focus (one reason to fail)
|
|
26
|
+
- [ ] Tests are independent — no shared mutable state between tests
|
|
27
|
+
- [ ] No test ordering dependency (would pass with randomised order)
|
|
28
|
+
- [ ] Arrange-Act-Assert structure is clear
|
|
29
|
+
- [ ] No logic in tests (no `if`, no loops, no try/catch) — use
|
|
30
|
+
`test.each` for parameterised cases
|
|
31
|
+
- [ ] No `console.log` or debugging artefacts left in
|
|
32
|
+
- [ ] Test fails for the right reason when the feature is broken
|
|
33
|
+
(verify by mentally removing the feature code)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. Test Data
|
|
38
|
+
|
|
39
|
+
- [ ] Uses factories for object creation (not inline literals)
|
|
40
|
+
- [ ] Only overrides fields relevant to the test (sensible defaults for rest)
|
|
41
|
+
- [ ] No hardcoded IDs that could collide across tests
|
|
42
|
+
- [ ] Faker seed is set if deterministic reproduction is needed
|
|
43
|
+
- [ ] Database state is isolated (transaction rollback, truncate, or unique IDs)
|
|
44
|
+
- [ ] No test data leaked between tests (check `beforeEach` / `afterEach`)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 4. Mocking
|
|
49
|
+
|
|
50
|
+
- [ ] Mocks are at system boundaries only (HTTP, clock, random)
|
|
51
|
+
- [ ] No internal modules are mocked — use real implementations
|
|
52
|
+
- [ ] MSW handlers use `onUnhandledRequest: 'error'`
|
|
53
|
+
- [ ] Per-test overrides use `server.use()` and are reset in `afterEach`
|
|
54
|
+
- [ ] Mock responses are realistic (match actual API shape)
|
|
55
|
+
- [ ] Mock contracts are verified against real API (schema validation or
|
|
56
|
+
contract test exists)
|
|
57
|
+
- [ ] `vi.restoreAllMocks()` runs in global `afterEach`
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 5. E2E Tests
|
|
62
|
+
|
|
63
|
+
- [ ] Uses Page Object Model (no raw selectors in test files)
|
|
64
|
+
- [ ] Element selection follows priority: `getByRole` > `getByLabel` >
|
|
65
|
+
`getByTestId`
|
|
66
|
+
- [ ] No `waitForTimeout` / `sleep` — uses explicit waits
|
|
67
|
+
- [ ] Test data seeded via API, not through UI interactions
|
|
68
|
+
- [ ] Test is parallelisation-safe (no shared global state)
|
|
69
|
+
- [ ] Viewport is fixed in config (no layout-dependent flakiness)
|
|
70
|
+
- [ ] Animations disabled in test config
|
|
71
|
+
- [ ] Screenshots and traces captured on failure
|
|
72
|
+
- [ ] Test passes with `--repeat-each=5` locally
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 6. Assertions
|
|
77
|
+
|
|
78
|
+
- [ ] Assertions are specific (not just `toBeTruthy` or `toMatchSnapshot`)
|
|
79
|
+
- [ ] Error cases are tested, not just happy path
|
|
80
|
+
- [ ] Boundary values are tested where applicable
|
|
81
|
+
- [ ] Negative assertions are used correctly (`not.toContain` vs missing check)
|
|
82
|
+
- [ ] Async assertions use proper `await` / `resolves` / `rejects`
|
|
83
|
+
- [ ] No snapshot tests for logic (snapshots only for serialised output
|
|
84
|
+
like HTML or JSON schema)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 7. Flakiness Risk
|
|
89
|
+
|
|
90
|
+
- [ ] No time-dependent logic without clock mocking
|
|
91
|
+
- [ ] No reliance on execution speed (no "this runs fast enough")
|
|
92
|
+
- [ ] No reliance on random values without seeding
|
|
93
|
+
- [ ] No reliance on network availability (all external calls mocked)
|
|
94
|
+
- [ ] No reliance on file system state (uses temp dirs or mocks)
|
|
95
|
+
- [ ] Database queries do not assume row ordering without `ORDER BY`
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 8. Performance
|
|
100
|
+
|
|
101
|
+
- [ ] Unit tests do not perform I/O (should complete in < 50 ms each)
|
|
102
|
+
- [ ] Integration test setup is batched (not N+1 inserts)
|
|
103
|
+
- [ ] E2E tests do not duplicate coverage available at lower levels
|
|
104
|
+
- [ ] No excessive `beforeEach` setup that could be `beforeAll`
|
|
105
|
+
(only when safe — consider isolation)
|
|
106
|
+
- [ ] Test suite stays within time budget (unit < 10 s, integration < 2 min,
|
|
107
|
+
E2E smoke < 10 min)
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 9. CI Integration
|
|
112
|
+
|
|
113
|
+
- [ ] Test runs in CI (not just locally)
|
|
114
|
+
- [ ] Test is in the correct pipeline stage (unit/integration/E2E)
|
|
115
|
+
- [ ] Test outputs JUnit XML or other parseable format
|
|
116
|
+
- [ ] Failure produces useful output (assertion messages, screenshots, traces)
|
|
117
|
+
- [ ] No CI-specific environment assumptions without fallback
|
|
118
|
+
(`process.env.CI` checks are guarded)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 10. Coverage
|
|
123
|
+
|
|
124
|
+
- [ ] New code has tests (not just modified code)
|
|
125
|
+
- [ ] Tests cover happy path, error path, and edge cases
|
|
126
|
+
- [ ] If coverage decreased, there is a justification
|
|
127
|
+
- [ ] No tests written solely to increase coverage percentage
|
|
128
|
+
(every test must catch a real bug class)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Quick Reject Criteria
|
|
133
|
+
|
|
134
|
+
Flag the PR immediately if any of these are present:
|
|
135
|
+
|
|
136
|
+
- `sleep()` or `waitForTimeout()` in E2E tests
|
|
137
|
+
- Mocking internal modules
|
|
138
|
+
- Shared mutable state between tests
|
|
139
|
+
- `test.skip` or `test.todo` without a linked issue
|
|
140
|
+
- Snapshot tests for business logic
|
|
141
|
+
- Hardcoded test data that will collide in parallel runs
|
|
142
|
+
- No assertions in a test (or only `toMatchSnapshot`)
|
|
143
|
+
- `console.log` left in test code
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Security Testing Reference
|
|
2
|
+
|
|
3
|
+
## OWASP Top 10 — Test Coverage
|
|
4
|
+
|
|
5
|
+
### SQL Injection
|
|
6
|
+
|
|
7
|
+
Test every user-controlled input that reaches a database query.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const sqlInjectionPayloads = [
|
|
11
|
+
"'; DROP TABLE users; --",
|
|
12
|
+
"1' OR '1'='1",
|
|
13
|
+
"1; SELECT * FROM users",
|
|
14
|
+
"' UNION SELECT username, password FROM users --",
|
|
15
|
+
"1' AND SLEEP(5) --",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
test.each(sqlInjectionPayloads)('rejects SQL injection: %s', async (payload) => {
|
|
19
|
+
const res = await request(app)
|
|
20
|
+
.get('/api/users')
|
|
21
|
+
.query({ search: payload });
|
|
22
|
+
|
|
23
|
+
// Should NOT return 500 (indicates unhandled query error)
|
|
24
|
+
expect(res.status).not.toBe(500);
|
|
25
|
+
// Should NOT return other users' data
|
|
26
|
+
expect(res.body).not.toContainEqual(
|
|
27
|
+
expect.objectContaining({ email: expect.stringContaining('@') }),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Prevention**: Use parameterised queries exclusively. Never concatenate user
|
|
33
|
+
input into SQL strings. ORM usage does not guarantee safety — verify raw
|
|
34
|
+
queries and `whereRaw` calls.
|
|
35
|
+
|
|
36
|
+
### Cross-Site Scripting (XSS)
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const xssPayloads = [
|
|
40
|
+
'<script>alert("xss")</script>',
|
|
41
|
+
'<img src=x onerror=alert("xss")>',
|
|
42
|
+
'"><svg onload=alert("xss")>',
|
|
43
|
+
"javascript:alert('xss')",
|
|
44
|
+
'<div onmouseover="alert(\'xss\')">hover</div>',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
test.each(xssPayloads)('sanitises XSS in user input: %s', async (payload) => {
|
|
48
|
+
// Submit payload via API
|
|
49
|
+
await request(app)
|
|
50
|
+
.post('/api/comments')
|
|
51
|
+
.send({ body: payload });
|
|
52
|
+
|
|
53
|
+
// Retrieve and verify output is escaped
|
|
54
|
+
const res = await request(app).get('/api/comments');
|
|
55
|
+
const comment = res.body[res.body.length - 1];
|
|
56
|
+
expect(comment.body).not.toContain('<script');
|
|
57
|
+
expect(comment.body).not.toContain('onerror');
|
|
58
|
+
expect(comment.body).not.toContain('onload');
|
|
59
|
+
expect(comment.body).not.toContain('onmouseover');
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Prevention**: Escape output by default (React does this). Sanitise HTML
|
|
64
|
+
input with a strict allowlist (DOMPurify). Set `Content-Security-Policy`
|
|
65
|
+
headers.
|
|
66
|
+
|
|
67
|
+
### Insecure Direct Object Reference (IDOR)
|
|
68
|
+
|
|
69
|
+
Test that users cannot access resources belonging to other users by
|
|
70
|
+
manipulating IDs.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
test('user cannot access another user\'s order', async () => {
|
|
74
|
+
// Create order as user A
|
|
75
|
+
const orderRes = await request(app)
|
|
76
|
+
.post('/api/orders')
|
|
77
|
+
.set('Authorization', `Bearer ${userAToken}`)
|
|
78
|
+
.send({ items: [{ sku: 'WIDGET-1', qty: 1 }] });
|
|
79
|
+
const orderId = orderRes.body.id;
|
|
80
|
+
|
|
81
|
+
// Attempt access as user B
|
|
82
|
+
const res = await request(app)
|
|
83
|
+
.get(`/api/orders/${orderId}`)
|
|
84
|
+
.set('Authorization', `Bearer ${userBToken}`);
|
|
85
|
+
|
|
86
|
+
expect(res.status).toBe(403);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('user cannot update another user\'s profile', async () => {
|
|
90
|
+
const res = await request(app)
|
|
91
|
+
.patch(`/api/users/${userAId}`)
|
|
92
|
+
.set('Authorization', `Bearer ${userBToken}`)
|
|
93
|
+
.send({ name: 'Hacked' });
|
|
94
|
+
|
|
95
|
+
expect(res.status).toBe(403);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Prevention**: Always check resource ownership in the authorization layer.
|
|
100
|
+
Never rely on obscured IDs (UUIDs) as a security mechanism.
|
|
101
|
+
|
|
102
|
+
### Authentication Bypass
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
describe('Auth bypass checks', () => {
|
|
106
|
+
const protectedEndpoints = [
|
|
107
|
+
{ method: 'GET', path: '/api/users/me' },
|
|
108
|
+
{ method: 'POST', path: '/api/orders' },
|
|
109
|
+
{ method: 'DELETE', path: '/api/users/123' },
|
|
110
|
+
{ method: 'GET', path: '/api/admin/dashboard' },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
test.each(protectedEndpoints)(
|
|
114
|
+
'$method $path returns 401 without token',
|
|
115
|
+
async ({ method, path }) => {
|
|
116
|
+
const res = await request(app)[method.toLowerCase()](path);
|
|
117
|
+
expect(res.status).toBe(401);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
test('expired token is rejected', async () => {
|
|
122
|
+
const expiredToken = signToken({ userId: '1' }, { expiresIn: '-1h' });
|
|
123
|
+
const res = await request(app)
|
|
124
|
+
.get('/api/users/me')
|
|
125
|
+
.set('Authorization', `Bearer ${expiredToken}`);
|
|
126
|
+
expect(res.status).toBe(401);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('tampered token is rejected', async () => {
|
|
130
|
+
const token = signToken({ userId: '1', role: 'member' });
|
|
131
|
+
// Modify payload to escalate role
|
|
132
|
+
const [header, , signature] = token.split('.');
|
|
133
|
+
const tamperedPayload = btoa(JSON.stringify({ userId: '1', role: 'admin' }));
|
|
134
|
+
const tamperedToken = `${header}.${tamperedPayload}.${signature}`;
|
|
135
|
+
|
|
136
|
+
const res = await request(app)
|
|
137
|
+
.get('/api/admin/dashboard')
|
|
138
|
+
.set('Authorization', `Bearer ${tamperedToken}`);
|
|
139
|
+
expect(res.status).toBe(401);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## DAST with OWASP ZAP
|
|
147
|
+
|
|
148
|
+
Dynamic Application Security Testing scans a running application for
|
|
149
|
+
vulnerabilities.
|
|
150
|
+
|
|
151
|
+
### Automated Scan in CI
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
# .github/workflows/security.yml
|
|
155
|
+
name: DAST Scan
|
|
156
|
+
on:
|
|
157
|
+
schedule:
|
|
158
|
+
- cron: '0 3 * * 1' # Weekly Monday 3 AM
|
|
159
|
+
workflow_dispatch:
|
|
160
|
+
|
|
161
|
+
jobs:
|
|
162
|
+
zap-scan:
|
|
163
|
+
runs-on: ubuntu-latest
|
|
164
|
+
services:
|
|
165
|
+
app:
|
|
166
|
+
image: your-app:latest
|
|
167
|
+
ports: ['3000:3000']
|
|
168
|
+
steps:
|
|
169
|
+
- name: ZAP Baseline Scan
|
|
170
|
+
uses: zaproxy/action-baseline@v0.12.0
|
|
171
|
+
with:
|
|
172
|
+
target: 'http://localhost:3000'
|
|
173
|
+
rules_file_name: '.zap/rules.tsv'
|
|
174
|
+
cmd_options: '-a -j'
|
|
175
|
+
|
|
176
|
+
- name: Upload ZAP Report
|
|
177
|
+
uses: actions/upload-artifact@v4
|
|
178
|
+
if: always()
|
|
179
|
+
with:
|
|
180
|
+
name: zap-report
|
|
181
|
+
path: report_html.html
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Custom Rules File
|
|
185
|
+
|
|
186
|
+
```tsv
|
|
187
|
+
# .zap/rules.tsv — customize alert thresholds
|
|
188
|
+
# ID Action (IGNORE, WARN, FAIL)
|
|
189
|
+
10010 IGNORE # Cookie No HttpOnly Flag (handled by framework)
|
|
190
|
+
10011 FAIL # Cookie Without Secure Flag
|
|
191
|
+
10015 FAIL # Incomplete or No Cache-control Header
|
|
192
|
+
10021 FAIL # X-Content-Type-Options Header Missing
|
|
193
|
+
10038 FAIL # Content Security Policy Header Not Set
|
|
194
|
+
40012 FAIL # Cross Site Scripting (Reflected)
|
|
195
|
+
40014 FAIL # Cross Site Scripting (Persistent)
|
|
196
|
+
90022 FAIL # Application Error Disclosure
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Authenticated Scan
|
|
200
|
+
|
|
201
|
+
For scanning behind login, provide ZAP with auth context:
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
- name: ZAP Full Scan
|
|
205
|
+
uses: zaproxy/action-full-scan@v0.10.0
|
|
206
|
+
with:
|
|
207
|
+
target: 'http://localhost:3000'
|
|
208
|
+
cmd_options: >
|
|
209
|
+
-z "-config auth.method=2
|
|
210
|
+
-config auth.method.loginindicator=Dashboard
|
|
211
|
+
-config auth.method.logoutindicator=Sign in"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Rate Limit Verification
|
|
217
|
+
|
|
218
|
+
Test that rate limits are enforced and return proper responses.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
describe('Rate limiting', () => {
|
|
222
|
+
test('login endpoint rate-limits after 5 attempts', async () => {
|
|
223
|
+
const results = [];
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < 10; i++) {
|
|
226
|
+
const res = await request(app)
|
|
227
|
+
.post('/api/auth/login')
|
|
228
|
+
.send({ email: 'test@test.com', password: 'wrong' });
|
|
229
|
+
results.push(res.status);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// First 5 should be 401 (wrong password)
|
|
233
|
+
expect(results.slice(0, 5).every((s) => s === 401)).toBe(true);
|
|
234
|
+
// Remaining should be 429 (rate limited)
|
|
235
|
+
expect(results.slice(5).every((s) => s === 429)).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('rate limit returns Retry-After header', async () => {
|
|
239
|
+
// Exhaust rate limit
|
|
240
|
+
for (let i = 0; i < 6; i++) {
|
|
241
|
+
await request(app)
|
|
242
|
+
.post('/api/auth/login')
|
|
243
|
+
.send({ email: 'test@test.com', password: 'wrong' });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const res = await request(app)
|
|
247
|
+
.post('/api/auth/login')
|
|
248
|
+
.send({ email: 'test@test.com', password: 'wrong' });
|
|
249
|
+
|
|
250
|
+
expect(res.status).toBe(429);
|
|
251
|
+
expect(res.headers['retry-after']).toBeDefined();
|
|
252
|
+
expect(parseInt(res.headers['retry-after'])).toBeGreaterThan(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('rate limit resets after window', async () => {
|
|
256
|
+
// This test requires clock mocking or a short rate limit window in test config
|
|
257
|
+
// Exhaust limit, advance time, verify access restored
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Additional Security Checks
|
|
263
|
+
|
|
264
|
+
- **CORS**: Verify `Access-Control-Allow-Origin` does not include `*` on
|
|
265
|
+
authenticated endpoints.
|
|
266
|
+
- **Headers**: Verify `X-Content-Type-Options: nosniff`,
|
|
267
|
+
`Strict-Transport-Security`, `X-Frame-Options`.
|
|
268
|
+
- **Sensitive data in URLs**: Verify tokens and passwords are never in query
|
|
269
|
+
strings (they appear in logs).
|
|
270
|
+
- **Error messages**: Verify error responses do not leak stack traces,
|
|
271
|
+
database schema, or internal paths.
|