openmoneta-dev-kit 1.9.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/README.md +103 -0
- package/agents/qa-autonomous.md +131 -0
- package/agents/requirement-analyst.md +98 -0
- package/agents/security-auditor.md +120 -0
- package/agents/ui-tester.md +186 -0
- package/bin/openmoneta.js +11 -0
- package/hooks/check-plan-exists.sh +154 -0
- package/hooks/enforce-docs-first.sh +169 -0
- package/hooks/inject-process-context.sh +117 -0
- package/hooks/track-changes.sh +46 -0
- package/hooks/verify-completion.sh +165 -0
- package/hooks.json +30 -0
- package/opencode/AGENTS.md.tpl +38 -0
- package/opencode/agents/qa-autonomous.md +42 -0
- package/opencode/agents/requirement-analyst.md +51 -0
- package/opencode/agents/security-auditor.md +46 -0
- package/opencode/agents/ui-tester.md +43 -0
- package/opencode/plugins/openmoneta-guard.ts +389 -0
- package/package.json +41 -0
- package/scripts/debug-hooks.sh +54 -0
- package/scripts/init-project.sh +438 -0
- package/scripts/list-affected-modules.sh +74 -0
- package/skills/auth-bypass-testing/SKILL.md +236 -0
- package/skills/automated-testing/SKILL.md +162 -0
- package/skills/automated-testing/scripts/install-playwright.sh +134 -0
- package/skills/module-architect/SKILL.md +256 -0
- package/skills/plan-writer/SKILL.md +229 -0
- package/skills/requirement-analysis/SKILL.md +163 -0
- package/skills/safe-push/SKILL.md +182 -0
- package/skills/security-checklist/SKILL.md +116 -0
- package/skills/test-strategy/SKILL.md +135 -0
- package/skills/ui-test-loop/SKILL.md +161 -0
- package/src/cli.js +63 -0
- package/src/commands/check.js +30 -0
- package/src/commands/init.js +43 -0
- package/src/commands/install.js +50 -0
- package/src/commands/uninstall.js +74 -0
- package/src/commands/update.js +81 -0
- package/src/lib/paths.js +46 -0
- package/src/lib/version.js +45 -0
- package/templates/AGENTS.md.tpl +106 -0
- package/templates/docs-INDEX.md.tpl +62 -0
- package/templates/env.test.tpl +16 -0
- package/templates/karpathy-reference.md +49 -0
- package/templates/plans-INDEX.md.tpl +38 -0
- package/templates/playwright.config.ts.tpl +44 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auth-bypass-testing
|
|
3
|
+
description: "ON-DEMAND ONLY: chỉ kích hoạt khi user explicitly yêu cầu bypass auth cho test (vd 'thêm test bypass cho Playwright login', 'setup test user pattern'). KHÔNG tự trigger trong quy trình bình thường. Triển khai Test Bypass Flag an toàn để Playwright test có thể bypass OAuth/login mà không phá bảo mật production. 6 layer defense (env guard, IP whitelist 127.0.0.1, token rotation, double header, audit log, CI/CD guard)."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Auth Bypass Testing — An toàn cho production
|
|
7
|
+
|
|
8
|
+
Pattern để Playwright test **bypass OAuth/login** (Google, GitHub, ...) khi test mà KHÔNG mở lỗ hổng cho production.
|
|
9
|
+
|
|
10
|
+
## Nguyên tắc 6 Layer Defense
|
|
11
|
+
|
|
12
|
+
Tất cả layer phải pass cùng lúc thì bypass mới hoạt động:
|
|
13
|
+
|
|
14
|
+
| Layer | Cơ chế | Vi phạm = chặn |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| 1. Env guard | `NODE_ENV !== 'production'` AND `ENABLE_TEST_BYPASS === 'true'` | Tree-shake trong build prod |
|
|
17
|
+
| 2. IP whitelist | Chỉ accept `127.0.0.1`, `::1`, `localhost` | Reject mọi IP khác |
|
|
18
|
+
| 3. Token rotation | Token random 64-char, sinh mỗi bootstrap, lưu `.env.test` (gitignored) | Token sai = 401 |
|
|
19
|
+
| 4. Double header | Phải có `X-Test-Bypass-Token` AND `X-Test-User-Id` | Thiếu = 401 |
|
|
20
|
+
| 5. Audit log | Log mọi request bypass vào `logs/test-bypass.log` | Để forensic |
|
|
21
|
+
| 6. CI/CD guard | Pre-deploy script grep `ENABLE_TEST_BYPASS=true` trong prod config | Fail build |
|
|
22
|
+
|
|
23
|
+
## Triển khai
|
|
24
|
+
|
|
25
|
+
### Bước 1 — Tạo middleware bypass (theo stack)
|
|
26
|
+
|
|
27
|
+
#### Express / Node.js
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// src/middleware/test-bypass.ts
|
|
31
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
32
|
+
import * as fs from 'fs';
|
|
33
|
+
import * as path from 'path';
|
|
34
|
+
|
|
35
|
+
const ALLOWED_IPS = (process.env.TEST_BYPASS_ALLOWED_IPS || '127.0.0.1,::1,localhost')
|
|
36
|
+
.split(',').map(s => s.trim());
|
|
37
|
+
|
|
38
|
+
const LOG_PATH = path.resolve(process.cwd(), 'logs/test-bypass.log');
|
|
39
|
+
|
|
40
|
+
export function testBypassMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
41
|
+
// Layer 1: Env guard - sẽ bị tree-shake trong production build
|
|
42
|
+
if (process.env.NODE_ENV === 'production' || process.env.ENABLE_TEST_BYPASS !== 'true') {
|
|
43
|
+
return next();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const token = req.header('X-Test-Bypass-Token');
|
|
47
|
+
const userId = req.header('X-Test-User-Id');
|
|
48
|
+
if (!token || !userId) return next();
|
|
49
|
+
|
|
50
|
+
// Layer 2: IP whitelist
|
|
51
|
+
const ip = (req.ip || req.socket.remoteAddress || '').replace(/^::ffff:/, '');
|
|
52
|
+
if (!ALLOWED_IPS.includes(ip)) {
|
|
53
|
+
return res.status(403).json({ error: 'Test bypass: IP not allowed' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Layer 3 + 4: Token check
|
|
57
|
+
const expectedToken = process.env.TEST_BYPASS_TOKEN;
|
|
58
|
+
if (!expectedToken || token !== expectedToken) {
|
|
59
|
+
return res.status(401).json({ error: 'Test bypass: invalid token' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate userId thuộc fixture
|
|
63
|
+
const fixturePath = path.resolve(process.cwd(), 'tests/e2e/fixtures/test-users.json');
|
|
64
|
+
const fixtures = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
|
65
|
+
const user = Object.values(fixtures).find((u: any) => u.id === userId);
|
|
66
|
+
if (!user) {
|
|
67
|
+
return res.status(401).json({ error: 'Test bypass: invalid user id' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Layer 5: Audit log
|
|
71
|
+
fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
|
|
72
|
+
fs.appendFileSync(LOG_PATH, JSON.stringify({
|
|
73
|
+
ts: new Date().toISOString(),
|
|
74
|
+
ip, userId, path: req.path, method: req.method,
|
|
75
|
+
ua: req.header('user-agent'),
|
|
76
|
+
}) + '\n');
|
|
77
|
+
|
|
78
|
+
// Inject user vào request
|
|
79
|
+
(req as any).user = user;
|
|
80
|
+
next();
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Mount **trước** middleware auth thật:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
app.use(testBypassMiddleware);
|
|
88
|
+
app.use(realAuthMiddleware);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Next.js (App Router) — middleware.ts
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// middleware.ts ở root
|
|
95
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
96
|
+
import * as fs from 'fs';
|
|
97
|
+
import * as path from 'path';
|
|
98
|
+
|
|
99
|
+
const ALLOWED_IPS = ['127.0.0.1', '::1', 'localhost'];
|
|
100
|
+
|
|
101
|
+
export function middleware(req: NextRequest) {
|
|
102
|
+
if (process.env.NODE_ENV === 'production' || process.env.ENABLE_TEST_BYPASS !== 'true') {
|
|
103
|
+
return NextResponse.next();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const token = req.headers.get('x-test-bypass-token');
|
|
107
|
+
const userId = req.headers.get('x-test-user-id');
|
|
108
|
+
if (!token || !userId) return NextResponse.next();
|
|
109
|
+
|
|
110
|
+
const ip = req.ip || req.headers.get('x-forwarded-for') || '';
|
|
111
|
+
if (!ALLOWED_IPS.some(a => ip.includes(a))) {
|
|
112
|
+
return new NextResponse('IP not allowed', { status: 403 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (token !== process.env.TEST_BYPASS_TOKEN) {
|
|
116
|
+
return new NextResponse('Invalid token', { status: 401 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Set cookie/header để route handler đọc user
|
|
120
|
+
const res = NextResponse.next();
|
|
121
|
+
res.headers.set('x-bypass-user-id', userId);
|
|
122
|
+
return res;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### FastAPI (Python)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# app/middleware/test_bypass.py
|
|
130
|
+
from fastapi import Request, HTTPException
|
|
131
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
132
|
+
import os, json, datetime
|
|
133
|
+
from pathlib import Path
|
|
134
|
+
|
|
135
|
+
ALLOWED_IPS = os.getenv('TEST_BYPASS_ALLOWED_IPS', '127.0.0.1,::1,localhost').split(',')
|
|
136
|
+
LOG_PATH = Path('logs/test-bypass.log')
|
|
137
|
+
|
|
138
|
+
class TestBypassMiddleware(BaseHTTPMiddleware):
|
|
139
|
+
async def dispatch(self, request: Request, call_next):
|
|
140
|
+
if os.getenv('ENV') == 'production' or os.getenv('ENABLE_TEST_BYPASS') != 'true':
|
|
141
|
+
return await call_next(request)
|
|
142
|
+
|
|
143
|
+
token = request.headers.get('x-test-bypass-token')
|
|
144
|
+
user_id = request.headers.get('x-test-user-id')
|
|
145
|
+
if not token or not user_id:
|
|
146
|
+
return await call_next(request)
|
|
147
|
+
|
|
148
|
+
ip = (request.client.host if request.client else '').replace('::ffff:', '')
|
|
149
|
+
if ip not in ALLOWED_IPS:
|
|
150
|
+
raise HTTPException(403, 'Test bypass: IP not allowed')
|
|
151
|
+
|
|
152
|
+
if token != os.getenv('TEST_BYPASS_TOKEN'):
|
|
153
|
+
raise HTTPException(401, 'Test bypass: invalid token')
|
|
154
|
+
|
|
155
|
+
# Audit log
|
|
156
|
+
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
with LOG_PATH.open('a') as f:
|
|
158
|
+
f.write(json.dumps({
|
|
159
|
+
'ts': datetime.datetime.utcnow().isoformat(),
|
|
160
|
+
'ip': ip, 'user_id': user_id,
|
|
161
|
+
'path': request.url.path, 'method': request.method,
|
|
162
|
+
}) + '\n')
|
|
163
|
+
|
|
164
|
+
request.state.bypass_user_id = user_id
|
|
165
|
+
return await call_next(request)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Bước 2 — Test bypass với Playwright
|
|
169
|
+
|
|
170
|
+
Header đã được inject tự động trong `playwright.config.ts` (xem `automated-testing`). Test có thể assume user đã login.
|
|
171
|
+
|
|
172
|
+
### Bước 3 — Pre-deploy CI guard
|
|
173
|
+
|
|
174
|
+
Tạo `scripts/check-no-bypass.sh`:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
#!/usr/bin/env bash
|
|
178
|
+
set -euo pipefail
|
|
179
|
+
|
|
180
|
+
# Fail build nếu phát hiện ENABLE_TEST_BYPASS=true trong production config
|
|
181
|
+
PROD_FILES=(
|
|
182
|
+
".env.production"
|
|
183
|
+
".env.prod"
|
|
184
|
+
"ecosystem.config.js"
|
|
185
|
+
"Dockerfile"
|
|
186
|
+
"docker-compose.prod.yml"
|
|
187
|
+
"kubernetes/*.yaml"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
FOUND=0
|
|
191
|
+
for pattern in "${PROD_FILES[@]}"; do
|
|
192
|
+
for f in $pattern; do
|
|
193
|
+
[[ -f "$f" ]] || continue
|
|
194
|
+
if grep -E "ENABLE_TEST_BYPASS\s*[:=]\s*['\"]?true" "$f"; then
|
|
195
|
+
echo "[!] DANGER: ENABLE_TEST_BYPASS=true trong $f"
|
|
196
|
+
FOUND=1
|
|
197
|
+
fi
|
|
198
|
+
done
|
|
199
|
+
done
|
|
200
|
+
|
|
201
|
+
[[ $FOUND -eq 0 ]] && echo "✅ Pre-deploy check OK" || exit 1
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Thêm vào CI pipeline (GitHub Actions):
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
- name: Pre-deploy security check
|
|
208
|
+
run: bash scripts/check-no-bypass.sh
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Bước 4 — Verify trong Bước 5 (Security Check)
|
|
212
|
+
|
|
213
|
+
Khi audit:
|
|
214
|
+
- [ ] `ENABLE_TEST_BYPASS` chỉ enable trong `.env.test` (gitignored)
|
|
215
|
+
- [ ] `.env.test` có trong `.gitignore`
|
|
216
|
+
- [ ] Middleware có `process.env.NODE_ENV !== 'production'` guard
|
|
217
|
+
- [ ] CI có `check-no-bypass.sh`
|
|
218
|
+
- [ ] `logs/test-bypass.log` không chứa request từ IP lạ
|
|
219
|
+
|
|
220
|
+
## Fallback: Storage State
|
|
221
|
+
|
|
222
|
+
Khi cần test flow **không thể bypass** (vd: OAuth callback từ Google, đăng ký user mới):
|
|
223
|
+
|
|
224
|
+
1. Login thủ công 1 lần với tài khoản test riêng (KHÔNG dùng tài khoản cá nhân).
|
|
225
|
+
2. Playwright lưu cookies + localStorage vào `tests/.auth/user.json` (gitignored).
|
|
226
|
+
3. Test sau dùng `storageState` để skip login.
|
|
227
|
+
|
|
228
|
+
Xem skill `automated-testing` mục "Storage State".
|
|
229
|
+
|
|
230
|
+
## Anti-patterns
|
|
231
|
+
|
|
232
|
+
❌ Hardcode token trong code (phải qua env)
|
|
233
|
+
❌ Bỏ IP check vì "test thôi mà"
|
|
234
|
+
❌ Quên rotate token sau leak nghi ngờ
|
|
235
|
+
❌ Log secret vào log file
|
|
236
|
+
❌ Cho deploy với `ENABLE_TEST_BYPASS=true`
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: automated-testing
|
|
3
|
+
description: "ON-DEMAND ONLY: chỉ kích hoạt khi user explicitly yêu cầu E2E/UI test với Playwright (vd 'viết Playwright test cho checkout flow', 'chạy E2E mobile'). KHÔNG tự trigger trong quy trình bình thường (v1.5.0 đã bỏ Bước 6 Test khỏi core process). Cài và dùng Playwright multi-viewport (mobile/tablet/desktop), screenshot, fixture cho auth state. Có script install-playwright.sh để AI tự cài."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Automated Testing với Playwright
|
|
7
|
+
|
|
8
|
+
Bộ công cụ test tự động cho UI/UX. Dùng kết hợp với skill `ui-test-loop` (workflow loop fix) và `auth-bypass-testing` (qua auth).
|
|
9
|
+
|
|
10
|
+
## Cài Playwright
|
|
11
|
+
|
|
12
|
+
Chạy script tự động (idempotent):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bash ~/.cursor/skills/automated-testing/scripts/install-playwright.sh
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Script sẽ:
|
|
19
|
+
1. Detect package manager (`npm`/`pnpm`/`yarn`/`bun`).
|
|
20
|
+
2. Cài `@playwright/test` + `dotenv`.
|
|
21
|
+
3. Cài browser `chromium` (mặc định, tiết kiệm dung lượng).
|
|
22
|
+
4. Tạo `playwright.config.ts` từ template nếu chưa có.
|
|
23
|
+
5. Tạo `tests/e2e/auth.setup.ts` mẫu.
|
|
24
|
+
|
|
25
|
+
## Cấu trúc tests/
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
tests/
|
|
29
|
+
├── unit/ # Vitest/Jest unit tests
|
|
30
|
+
├── integration/ # Integration tests
|
|
31
|
+
├── e2e/
|
|
32
|
+
│ ├── auth.setup.ts # Setup login state + test bypass header
|
|
33
|
+
│ ├── *.spec.ts # E2E specs
|
|
34
|
+
│ └── fixtures/
|
|
35
|
+
│ └── test-users.json
|
|
36
|
+
├── screenshots/
|
|
37
|
+
│ └── <plan-slug>/iter-<n>/ # Output ui-test-loop
|
|
38
|
+
└── .auth/ # GITIGNORED — Playwright storageState (nếu dùng)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Pattern cơ bản
|
|
42
|
+
|
|
43
|
+
### 1. Test multi-viewport
|
|
44
|
+
|
|
45
|
+
`playwright.config.ts` đã setup 3 projects: mobile (375px), tablet (768px), desktop (1440px). Mặc định mọi spec chạy cả 3.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Chạy tất cả 3 viewport
|
|
49
|
+
npx playwright test
|
|
50
|
+
|
|
51
|
+
# Chạy 1 viewport cụ thể
|
|
52
|
+
npx playwright test --project=mobile
|
|
53
|
+
|
|
54
|
+
# Chạy 1 spec
|
|
55
|
+
npx playwright test login.spec.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Test với Test Bypass Auth
|
|
59
|
+
|
|
60
|
+
Header `X-Test-Bypass-Token` đã được inject tự động vào mọi request qua `extraHTTPHeaders` trong config (xem template `playwright.config.ts.tpl`). Test có thể assume user đã login.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { test, expect } from '@playwright/test';
|
|
64
|
+
|
|
65
|
+
test('user vào dashboard với test bypass', async ({ page }) => {
|
|
66
|
+
await page.goto('/dashboard');
|
|
67
|
+
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Screenshot có cấu trúc
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { test } from '@playwright/test';
|
|
75
|
+
|
|
76
|
+
test('Login button responsive', async ({ page }, testInfo) => {
|
|
77
|
+
await page.goto('/login');
|
|
78
|
+
|
|
79
|
+
const planSlug = process.env.PLAN_SLUG || 'unknown';
|
|
80
|
+
const iter = process.env.UI_ITER || '0';
|
|
81
|
+
const dir = `tests/screenshots/${planSlug}/iter-${iter}`;
|
|
82
|
+
|
|
83
|
+
await page.screenshot({
|
|
84
|
+
path: `${dir}/${testInfo.project.name}-login.png`,
|
|
85
|
+
fullPage: true,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 4. Visual regression (snapshot)
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
test('Login matches design', async ({ page }) => {
|
|
94
|
+
await page.goto('/login');
|
|
95
|
+
await expect(page).toHaveScreenshot('login.png', {
|
|
96
|
+
maxDiffPixels: 100, // Cho phép sai tối đa 100 pixel
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Lần đầu chạy → tạo baseline. Lần sau → so sánh.
|
|
102
|
+
|
|
103
|
+
### 5. Storage State (cho flow OAuth thật)
|
|
104
|
+
|
|
105
|
+
Khi không thể bypass (vd: phải test OAuth callback từ Google thật), login 1 lần thủ công và lưu state:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// tests/e2e/auth.setup.ts
|
|
109
|
+
import { test as setup } from '@playwright/test';
|
|
110
|
+
|
|
111
|
+
setup('login Google', async ({ page }) => {
|
|
112
|
+
await page.goto('/login');
|
|
113
|
+
await page.click('button:has-text("Đăng nhập Google")');
|
|
114
|
+
// ... user thao tác manual lần đầu
|
|
115
|
+
await page.context().storageState({ path: 'tests/.auth/user.json' });
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// playwright.config.ts — thêm
|
|
121
|
+
export default defineConfig({
|
|
122
|
+
use: {
|
|
123
|
+
storageState: 'tests/.auth/user.json',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Chạy test trong CI
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
CI=1 npx playwright test --reporter=html
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Debug
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# UI mode — xem step-by-step
|
|
138
|
+
npx playwright test --ui
|
|
139
|
+
|
|
140
|
+
# Headed — xem browser thật
|
|
141
|
+
npx playwright test --headed
|
|
142
|
+
|
|
143
|
+
# Trace viewer — xem trace của test fail
|
|
144
|
+
npx playwright show-trace trace.zip
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Khi test fail
|
|
148
|
+
|
|
149
|
+
1. Xem `tests/playwright-report/` (HTML report).
|
|
150
|
+
2. Trace tự động lưu cho test fail (`retain-on-failure`).
|
|
151
|
+
3. Apply skill `ui-test-loop` để loop fix.
|
|
152
|
+
|
|
153
|
+
## Output bắt buộc cho hook `verify-completion`
|
|
154
|
+
|
|
155
|
+
Sau khi chạy test, lưu kết quả:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Ví dụ wrapper script
|
|
159
|
+
RESULT=$(npx playwright test --reporter=json 2>&1)
|
|
160
|
+
STATUS=$([[ $? == 0 ]] && echo "pass" || echo "fail")
|
|
161
|
+
echo "{\"status\": \"$STATUS\", \"timestamp\": \"$(date -Iseconds)\", \"tool\": \"playwright\"}" > .cursor/.last-test-result
|
|
162
|
+
```
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cài Playwright cho project hiện tại. Idempotent - chạy lại không sao.
|
|
3
|
+
# Usage: bash ~/.cursor/skills/automated-testing/scripts/install-playwright.sh [project-path]
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
PROJECT_DIR="${1:-$(pwd)}"
|
|
8
|
+
cd "$PROJECT_DIR"
|
|
9
|
+
|
|
10
|
+
echo "==> Cài Playwright cho: $PROJECT_DIR"
|
|
11
|
+
|
|
12
|
+
# Detect package manager
|
|
13
|
+
if [[ -f bun.lockb || -f bun.lock ]]; then
|
|
14
|
+
PM=bun
|
|
15
|
+
ADD="bun add -d"
|
|
16
|
+
elif [[ -f pnpm-lock.yaml ]]; then
|
|
17
|
+
PM=pnpm
|
|
18
|
+
ADD="pnpm add -D"
|
|
19
|
+
elif [[ -f yarn.lock ]]; then
|
|
20
|
+
PM=yarn
|
|
21
|
+
ADD="yarn add -D"
|
|
22
|
+
elif [[ -f package.json ]]; then
|
|
23
|
+
PM=npm
|
|
24
|
+
ADD="npm install -D"
|
|
25
|
+
else
|
|
26
|
+
echo "[!] Không phát hiện package.json. Tạo dự án Node trước (npm init -y) rồi chạy lại."
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "==> Package manager: $PM"
|
|
31
|
+
|
|
32
|
+
# Check đã cài chưa
|
|
33
|
+
if [[ -f node_modules/@playwright/test/package.json ]]; then
|
|
34
|
+
echo "==> @playwright/test đã cài, skip."
|
|
35
|
+
else
|
|
36
|
+
echo "==> Cài @playwright/test và dotenv..."
|
|
37
|
+
$ADD @playwright/test dotenv
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Cài browser chromium (mặc định, nhẹ nhất)
|
|
41
|
+
echo "==> Cài Chromium browser cho Playwright..."
|
|
42
|
+
npx playwright install chromium --with-deps 2>/dev/null || npx playwright install chromium
|
|
43
|
+
|
|
44
|
+
# Copy playwright.config.ts từ template nếu chưa có
|
|
45
|
+
TEMPLATE="$HOME/.cursor/templates/playwright.config.ts.tpl"
|
|
46
|
+
TARGET="playwright.config.ts"
|
|
47
|
+
if [[ -f "$TARGET" ]]; then
|
|
48
|
+
echo "==> $TARGET đã tồn tại, skip."
|
|
49
|
+
elif [[ -f "$TEMPLATE" ]]; then
|
|
50
|
+
cp "$TEMPLATE" "$TARGET"
|
|
51
|
+
echo "==> Đã tạo $TARGET từ template."
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Tạo tests/e2e/auth.setup.ts mẫu
|
|
55
|
+
mkdir -p tests/e2e/fixtures
|
|
56
|
+
AUTH_SETUP="tests/e2e/auth.setup.ts"
|
|
57
|
+
if [[ ! -f "$AUTH_SETUP" ]]; then
|
|
58
|
+
cat > "$AUTH_SETUP" <<'EOF'
|
|
59
|
+
import { test as setup, expect } from '@playwright/test';
|
|
60
|
+
import * as fs from 'fs';
|
|
61
|
+
import * as path from 'path';
|
|
62
|
+
|
|
63
|
+
const AUTH_FILE = path.resolve(__dirname, '../.auth/user.json');
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Setup auth state. 2 cách:
|
|
67
|
+
* 1. Test Bypass Flag (recommended): header X-Test-Bypass-Token đã set
|
|
68
|
+
* trong playwright.config.ts. Setup này chỉ sanity-check.
|
|
69
|
+
* 2. Storage State: login thật, lưu cookies/localStorage vào AUTH_FILE.
|
|
70
|
+
*/
|
|
71
|
+
setup('verify auth bypass works', async ({ request }) => {
|
|
72
|
+
const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
|
|
73
|
+
const token = process.env.TEST_BYPASS_TOKEN;
|
|
74
|
+
|
|
75
|
+
if (!token) {
|
|
76
|
+
console.warn('[auth.setup] TEST_BYPASS_TOKEN không set. Bỏ qua bypass check.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Sanity: gọi endpoint health/me với header bypass
|
|
81
|
+
const res = await request.get(`${baseURL}/api/me`, {
|
|
82
|
+
headers: {
|
|
83
|
+
'X-Test-Bypass-Token': token,
|
|
84
|
+
'X-Test-User-Id': process.env.TEST_USER_ID_DEFAULT || '',
|
|
85
|
+
},
|
|
86
|
+
failOnStatusCode: false,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (res.status() === 404) {
|
|
90
|
+
console.warn('[auth.setup] Endpoint /api/me chưa có, skip sanity check.');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
expect(res.status()).toBeLessThan(500);
|
|
95
|
+
console.log('[auth.setup] Bypass auth OK.');
|
|
96
|
+
|
|
97
|
+
// Đảm bảo thư mục .auth tồn tại (không lưu state khi dùng bypass)
|
|
98
|
+
const dir = path.dirname(AUTH_FILE);
|
|
99
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
});
|
|
101
|
+
EOF
|
|
102
|
+
echo "==> Đã tạo $AUTH_SETUP mẫu."
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Tạo tests/fixtures/test-users.json mẫu
|
|
106
|
+
USERS_JSON="tests/e2e/fixtures/test-users.json"
|
|
107
|
+
if [[ ! -f "$USERS_JSON" ]]; then
|
|
108
|
+
cat > "$USERS_JSON" <<'EOF'
|
|
109
|
+
{
|
|
110
|
+
"default": {
|
|
111
|
+
"id": "00000000-0000-0000-0000-000000000001",
|
|
112
|
+
"email": "test-default@example.com",
|
|
113
|
+
"name": "Test User",
|
|
114
|
+
"role": "user"
|
|
115
|
+
},
|
|
116
|
+
"admin": {
|
|
117
|
+
"id": "00000000-0000-0000-0000-000000000002",
|
|
118
|
+
"email": "test-admin@example.com",
|
|
119
|
+
"name": "Test Admin",
|
|
120
|
+
"role": "admin"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
EOF
|
|
124
|
+
echo "==> Đã tạo $USERS_JSON mẫu."
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
echo ""
|
|
128
|
+
echo "✅ Hoàn tất cài Playwright."
|
|
129
|
+
echo ""
|
|
130
|
+
echo "Chạy test: npx playwright test"
|
|
131
|
+
echo "Chạy mobile only: npx playwright test --project=mobile"
|
|
132
|
+
echo "UI mode: npx playwright test --ui"
|
|
133
|
+
echo ""
|
|
134
|
+
echo "Lưu ý: setup .env.test với TEST_BYPASS_TOKEN và backend của bạn phải có middleware bypass (xem skill auth-bypass-testing)."
|