create-rigel 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 +156 -0
- package/cli.js +122 -0
- package/package.json +47 -0
- package/templates/express/.claude/CLAUDE.md +114 -0
- package/templates/express/.claude/agents/arch-validator.md +85 -0
- package/templates/express/.claude/agents/db-optimizer.md +72 -0
- package/templates/express/.claude/agents/doc-gardener.md +67 -0
- package/templates/express/.claude/agents/garbage-collector.md +94 -0
- package/templates/express/.claude/agents/gate-checker.md +176 -0
- package/templates/express/.claude/agents/reviewer.md +84 -0
- package/templates/express/.claude/agents/security-auditor.md +82 -0
- package/templates/express/.claude/hooks/post-write.sh +93 -0
- package/templates/express/.claude/rules/api.md +149 -0
- package/templates/express/.claude/rules/architecture.md +78 -0
- package/templates/express/.claude/rules/database.md +104 -0
- package/templates/express/.claude/rules/jobs.md +81 -0
- package/templates/express/.claude/rules/security.md +92 -0
- package/templates/express/.claude/rules/testing.md +97 -0
- package/templates/express/.claude/settings.json +35 -0
- package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
- package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
- package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
- package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
- package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
- package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
- package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
- package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
- package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
- package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
- package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
- package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
- package/templates/express/.dockerignore +43 -0
- package/templates/express/.env.example +42 -0
- package/templates/express/.github/CODEOWNERS +22 -0
- package/templates/express/.github/dependabot.yml +48 -0
- package/templates/express/.github/pull_request_template.md +58 -0
- package/templates/express/.github/workflows/ci.yml +278 -0
- package/templates/express/.github/workflows/load-test.yml +41 -0
- package/templates/express/.gitleaks.toml +18 -0
- package/templates/express/.lintstagedrc.json +3 -0
- package/templates/express/.nvmrc +1 -0
- package/templates/express/.prettierrc +8 -0
- package/templates/express/.sequelizerc +8 -0
- package/templates/express/AGENTS.md +94 -0
- package/templates/express/ARCHITECTURE.md +161 -0
- package/templates/express/Dockerfile +40 -0
- package/templates/express/Makefile +79 -0
- package/templates/express/QUICKSTART.md +131 -0
- package/templates/express/docker-compose.yml +114 -0
- package/templates/express/docs/PLANS.md +111 -0
- package/templates/express/docs/QUALITY_SCORE.md +66 -0
- package/templates/express/docs/design-docs/core-beliefs.md +89 -0
- package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
- package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
- package/templates/express/docs/design-docs/decisions/index.md +23 -0
- package/templates/express/docs/design-docs/index.md +11 -0
- package/templates/express/docs/design-docs/observability.md +194 -0
- package/templates/express/docs/design-docs/team-workflow.md +64 -0
- package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
- package/templates/express/docs/generated/.gitkeep +0 -0
- package/templates/express/docs/product-specs/ROADMAP.md +49 -0
- package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/express/docs/product-specs/index.md +21 -0
- package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/express/eslint.config.mjs +94 -0
- package/templates/express/gitignore +36 -0
- package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
- package/templates/express/jest.config.ts +61 -0
- package/templates/express/package.json +45 -0
- package/templates/express/scripts/openapi.export.ts +77 -0
- package/templates/express/tests/architecture/isolation.test.ts +51 -0
- package/templates/express/tests/architecture/layers.test.ts +83 -0
- package/templates/express/tests/integration/health.test.ts +40 -0
- package/templates/express/tests/integration/isolation.test.template.ts +88 -0
- package/templates/express/tests/load/smoke.js +46 -0
- package/templates/express/tests/load/soak.js +45 -0
- package/templates/express/tests/load/stress.js +44 -0
- package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
- package/templates/express/tsconfig.build.json +8 -0
- package/templates/express/tsconfig.json +20 -0
- package/templates/fastapi/.claude/CLAUDE.md +122 -0
- package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
- package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
- package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
- package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
- package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
- package/templates/fastapi/.claude/agents/reviewer.md +74 -0
- package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
- package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
- package/templates/fastapi/.claude/rules/api.md +119 -0
- package/templates/fastapi/.claude/rules/architecture.md +84 -0
- package/templates/fastapi/.claude/rules/database.md +153 -0
- package/templates/fastapi/.claude/rules/jobs.md +85 -0
- package/templates/fastapi/.claude/rules/security.md +102 -0
- package/templates/fastapi/.claude/rules/testing.md +207 -0
- package/templates/fastapi/.claude/settings.json +50 -0
- package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
- package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
- package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
- package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
- package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
- package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
- package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
- package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
- package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
- package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
- package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
- package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
- package/templates/fastapi/.dockerignore +30 -0
- package/templates/fastapi/.github/dependabot.yml +24 -0
- package/templates/fastapi/AGENTS.md +87 -0
- package/templates/fastapi/ARCHITECTURE.md +141 -0
- package/templates/fastapi/Makefile +42 -0
- package/templates/fastapi/README.md +179 -0
- package/templates/fastapi/docs/PLANS.md +26 -0
- package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
- package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
- package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
- package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
- package/templates/fastapi/docs/design-docs/index.md +9 -0
- package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/fastapi/docs/generated/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
- package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/index.md +18 -0
- package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/fastapi/gitignore +46 -0
- package/templates/fastapi/scripts/gate.sh +68 -0
- package/templates/nestjs/.claude/CLAUDE.md +121 -0
- package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
- package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
- package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
- package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
- package/templates/nestjs/.claude/agents/reviewer.md +68 -0
- package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
- package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
- package/templates/nestjs/.claude/rules/api.md +87 -0
- package/templates/nestjs/.claude/rules/architecture.md +104 -0
- package/templates/nestjs/.claude/rules/database.md +111 -0
- package/templates/nestjs/.claude/rules/security.md +110 -0
- package/templates/nestjs/.claude/rules/testing.md +145 -0
- package/templates/nestjs/.claude/settings.json +33 -0
- package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
- package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
- package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
- package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
- package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
- package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
- package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
- package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
- package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
- package/templates/nestjs/.env.example +11 -0
- package/templates/nestjs/AGENTS.md +88 -0
- package/templates/nestjs/ARCHITECTURE.md +192 -0
- package/templates/nestjs/docs/PLANS.md +22 -0
- package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
- package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
- package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nestjs/docs/design-docs/index.md +7 -0
- package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
- package/templates/nestjs/docs/generated/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/index.md +7 -0
- package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nestjs/gitignore +8 -0
- package/templates/nextjs/.claude/CLAUDE.md +139 -0
- package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
- package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
- package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
- package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
- package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
- package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
- package/templates/nextjs/.claude/agents/reviewer.md +79 -0
- package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
- package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
- package/templates/nextjs/.claude/rules/api-contract.md +166 -0
- package/templates/nextjs/.claude/rules/architecture.md +99 -0
- package/templates/nextjs/.claude/rules/components.md +122 -0
- package/templates/nextjs/.claude/rules/security.md +122 -0
- package/templates/nextjs/.claude/rules/testing.md +187 -0
- package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
- package/templates/nextjs/.claude/settings.json +33 -0
- package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
- package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
- package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
- package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
- package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
- package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
- package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
- package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
- package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
- package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
- package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
- package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
- package/templates/nextjs/.dockerignore +25 -0
- package/templates/nextjs/.gitattributes +10 -0
- package/templates/nextjs/.github/CODEOWNERS +24 -0
- package/templates/nextjs/.github/dependabot.yml +31 -0
- package/templates/nextjs/.github/pull_request_template.md +59 -0
- package/templates/nextjs/.github/workflows/ci.yml +143 -0
- package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
- package/templates/nextjs/.github/workflows/load-test.yml +54 -0
- package/templates/nextjs/.husky/pre-commit +1 -0
- package/templates/nextjs/.lintstagedrc.json +4 -0
- package/templates/nextjs/.mcp.json +8 -0
- package/templates/nextjs/.prettierignore +13 -0
- package/templates/nextjs/.prettierrc +9 -0
- package/templates/nextjs/AGENTS.md +88 -0
- package/templates/nextjs/ARCHITECTURE.md +165 -0
- package/templates/nextjs/Dockerfile +38 -0
- package/templates/nextjs/Makefile +50 -0
- package/templates/nextjs/QUICKSTART.md +128 -0
- package/templates/nextjs/docs/PLANS.md +23 -0
- package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
- package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
- package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nextjs/docs/design-docs/index.md +7 -0
- package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
- package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/nextjs/docs/generated/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
- package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/index.md +18 -0
- package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nextjs/eslint.config.mjs +150 -0
- package/templates/nextjs/gitignore +35 -0
- package/templates/nextjs/lighthouserc.js +37 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/lib/api-client.ts"
|
|
4
|
+
- "app/api/**/*.ts"
|
|
5
|
+
- "src/features/auth/**/*.ts"
|
|
6
|
+
- "src/features/auth/**/*.tsx"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Security Rules — Auto-injected on auth and API client edits
|
|
10
|
+
|
|
11
|
+
## Auth Token Handling
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// ✅ Access token in React context (memory only)
|
|
15
|
+
// src/features/auth/AuthProvider.tsx
|
|
16
|
+
const [accessToken, setAccessToken] = useState<string | null>(null)
|
|
17
|
+
// Store ONLY in React state — wiped on page refresh by design
|
|
18
|
+
|
|
19
|
+
// ✅ Inject access token via api-client middleware
|
|
20
|
+
// src/lib/api-client.ts
|
|
21
|
+
import createClient, { type Middleware } from 'openapi-fetch'
|
|
22
|
+
import type { paths } from '@/types/api.generated'
|
|
23
|
+
|
|
24
|
+
const authMiddleware: Middleware = {
|
|
25
|
+
async onRequest({ request }) {
|
|
26
|
+
const token = getAccessTokenFromContext() // from React context
|
|
27
|
+
if (token) {
|
|
28
|
+
request.headers.set('Authorization', `Bearer ${token}`)
|
|
29
|
+
}
|
|
30
|
+
return request
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const apiClient = createClient<paths>({ baseUrl: env.NEXT_PUBLIC_API_URL })
|
|
35
|
+
apiClient.use(authMiddleware)
|
|
36
|
+
|
|
37
|
+
// ❌ NEVER — token in storage
|
|
38
|
+
localStorage.setItem('accessToken', token)
|
|
39
|
+
sessionStorage.setItem('accessToken', token)
|
|
40
|
+
document.cookie = `accessToken=${token}` // use httpOnly cookies via backend
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Refresh Token Flow
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// src/app/api/auth/refresh/route.ts — Next.js API route
|
|
47
|
+
// The refresh token is in an httpOnly cookie (set by backend)
|
|
48
|
+
// This route proxies the refresh request so the cookie is sent automatically
|
|
49
|
+
|
|
50
|
+
export async function POST() {
|
|
51
|
+
const response = await fetch(`${env.API_URL}/api/v1/auth/refresh`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
credentials: 'include', // sends the httpOnly cookie
|
|
54
|
+
})
|
|
55
|
+
const data = await response.json()
|
|
56
|
+
// Return new access token to client — it goes into React context
|
|
57
|
+
return NextResponse.json({ accessToken: data.accessToken })
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Environment Variables Security
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// ✅ Only NEXT_PUBLIC_* vars are safe to expose to client
|
|
65
|
+
// Non-NEXT_PUBLIC_ vars are server-only — NEVER access in client components
|
|
66
|
+
|
|
67
|
+
// src/lib/env.ts — validate all public env vars with Zod
|
|
68
|
+
import { z } from 'zod'
|
|
69
|
+
const envSchema = z.object({
|
|
70
|
+
NEXT_PUBLIC_API_URL: z.string().url(),
|
|
71
|
+
})
|
|
72
|
+
// Never put secrets (JWT_SECRET, DB passwords) in NEXT_PUBLIC_* vars
|
|
73
|
+
|
|
74
|
+
// ❌ Leaked secret
|
|
75
|
+
NEXT_PUBLIC_JWT_SECRET=my-secret // accessible to all users in browser
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Content Security Policy
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// next.config.ts — CSP via headers
|
|
82
|
+
const ContentSecurityPolicy = `
|
|
83
|
+
default-src 'self';
|
|
84
|
+
script-src 'self' 'nonce-{NONCE}';
|
|
85
|
+
style-src 'self' 'unsafe-inline';
|
|
86
|
+
img-src 'self' data: https:;
|
|
87
|
+
connect-src 'self' ${process.env.NEXT_PUBLIC_API_URL};
|
|
88
|
+
frame-ancestors 'none';
|
|
89
|
+
`.replace(/\n/g, ' ')
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## XSS Prevention
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// ✅ React escapes by default — just use JSX
|
|
96
|
+
<p>{userContent}</p>
|
|
97
|
+
|
|
98
|
+
// ❌ Only with explicit sanitisation
|
|
99
|
+
<div dangerouslySetInnerHTML={{ __html: userContent }} />
|
|
100
|
+
// If required: use DOMPurify.sanitize(userContent) first
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Next.js API Routes (Cookie Proxy Only)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// app/api/ routes ONLY handle:
|
|
107
|
+
// 1. Cookie operations (set/clear httpOnly cookies)
|
|
108
|
+
// 2. Proxying auth requests
|
|
109
|
+
// NOT for business logic — that belongs in the backend
|
|
110
|
+
|
|
111
|
+
// ✅ Legitimate Next.js API route
|
|
112
|
+
export async function POST(request: Request) {
|
|
113
|
+
const body = await request.json()
|
|
114
|
+
const response = await fetch(`${env.API_URL}/api/v1/auth/login`, {
|
|
115
|
+
method: 'POST', body: JSON.stringify(body),
|
|
116
|
+
headers: { 'Content-Type': 'application/json' },
|
|
117
|
+
})
|
|
118
|
+
// Set httpOnly cookie with refresh token from backend response
|
|
119
|
+
const res = NextResponse.json(await response.json())
|
|
120
|
+
return res
|
|
121
|
+
}
|
|
122
|
+
```
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "tests/**/*.ts"
|
|
4
|
+
- "tests/**/*.tsx"
|
|
5
|
+
- "src/**/*.test.ts"
|
|
6
|
+
- "src/**/*.test.tsx"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Testing Rules — Auto-injected on test file edits
|
|
10
|
+
|
|
11
|
+
## Coverage Thresholds (enforced in CI)
|
|
12
|
+
|
|
13
|
+
| Layer | Threshold | What to test |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `src/utils/` | **100%** | Every branch |
|
|
16
|
+
| `src/hooks/` | **80%** | Loading, success, error states; mutations |
|
|
17
|
+
| `src/features/` | **70%** | Render, user interaction, form submission |
|
|
18
|
+
| `src/components/` | **70%** | Render + accessibility |
|
|
19
|
+
|
|
20
|
+
## Utils Tests — Pure Functions, 100% Branches
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// tests/unit/utils/parse-error.test.ts
|
|
24
|
+
import { parseError } from '@/utils/parse-error.util'
|
|
25
|
+
|
|
26
|
+
describe('parseError', () => {
|
|
27
|
+
it('extracts message from Error', () => {
|
|
28
|
+
expect(parseError(new Error('boom'))).toBe('boom')
|
|
29
|
+
})
|
|
30
|
+
it('passes through string errors', () => {
|
|
31
|
+
expect(parseError('nope')).toBe('nope')
|
|
32
|
+
})
|
|
33
|
+
it('falls back for unknown shapes', () => {
|
|
34
|
+
expect(parseError({ weird: true })).toBe('Something went wrong')
|
|
35
|
+
expect(parseError(null)).toBe('Something went wrong')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Vitest + RTL — Hook Tests with MSW
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// tests/unit/hooks/use-applications.test.ts
|
|
44
|
+
import { renderHook, waitFor } from '@testing-library/react'
|
|
45
|
+
import { http, HttpResponse } from 'msw'
|
|
46
|
+
import { server } from '../mocks/server' // MSW server
|
|
47
|
+
import { createWrapper } from '../utils/create-wrapper' // QueryClient wrapper
|
|
48
|
+
import { useApplications } from '@/hooks/use-applications'
|
|
49
|
+
|
|
50
|
+
describe('useApplications', () => {
|
|
51
|
+
it('returns applications on success', async () => {
|
|
52
|
+
server.use(
|
|
53
|
+
http.get('/api/v1/applications', () =>
|
|
54
|
+
HttpResponse.json({ items: [{ id: '1', stage: 'APPLIED', company: 'Acme' }], has_more: false })
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
const { result } = renderHook(() => useApplications(), { wrapper: createWrapper() })
|
|
58
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
|
59
|
+
expect(result.current.data?.items).toHaveLength(1)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns error on failure', async () => {
|
|
63
|
+
server.use(
|
|
64
|
+
http.get('/api/v1/applications', () => HttpResponse.error())
|
|
65
|
+
)
|
|
66
|
+
const { result } = renderHook(() => useApplications(), { wrapper: createWrapper() })
|
|
67
|
+
await waitFor(() => expect(result.current.isError).toBe(true))
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Component Tests — Test Behaviour, Not Implementation
|
|
73
|
+
```typescript
|
|
74
|
+
// tests/unit/features/ApplicationCard.test.tsx
|
|
75
|
+
import { render, screen } from '@testing-library/react'
|
|
76
|
+
import userEvent from '@testing-library/user-event'
|
|
77
|
+
import { ApplicationCard } from '@/features/applications/ApplicationCard'
|
|
78
|
+
|
|
79
|
+
describe('ApplicationCard', () => {
|
|
80
|
+
it('displays company and role', () => {
|
|
81
|
+
render(<ApplicationCard application={mockApplication} />)
|
|
82
|
+
expect(screen.getByText('Acme Corp')).toBeInTheDocument()
|
|
83
|
+
expect(screen.getByText('Software Engineer')).toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('calls onDelete when delete button clicked', async () => {
|
|
87
|
+
const onDelete = vi.fn()
|
|
88
|
+
render(<ApplicationCard application={mockApplication} onDelete={onDelete} />)
|
|
89
|
+
await userEvent.click(screen.getByRole('button', { name: /delete/i }))
|
|
90
|
+
expect(onDelete).toHaveBeenCalledWith(mockApplication.id)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('is accessible — delete button has aria-label', () => {
|
|
94
|
+
render(<ApplicationCard application={mockApplication} onDelete={vi.fn()} />)
|
|
95
|
+
expect(screen.getByRole('button', { name: /delete application/i })).toBeInTheDocument()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Playwright — E2E Critical Paths
|
|
101
|
+
```typescript
|
|
102
|
+
// tests/e2e/applications.spec.ts
|
|
103
|
+
import { test, expect } from '@playwright/test'
|
|
104
|
+
|
|
105
|
+
test.describe('Applications', () => {
|
|
106
|
+
test.beforeEach(async ({ page }) => {
|
|
107
|
+
await page.goto('/login')
|
|
108
|
+
await page.getByLabel('Email').fill('test@example.com')
|
|
109
|
+
await page.getByLabel('Password').fill('password123')
|
|
110
|
+
await page.getByRole('button', { name: 'Sign in' }).click()
|
|
111
|
+
await expect(page).toHaveURL('/applications')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('can create a new application', async ({ page }) => {
|
|
115
|
+
await page.getByRole('button', { name: 'New Application' }).click()
|
|
116
|
+
await page.getByLabel('Company').fill('Acme Corp')
|
|
117
|
+
await page.getByLabel('Role').fill('Software Engineer')
|
|
118
|
+
await page.getByRole('button', { name: 'Create' }).click()
|
|
119
|
+
await expect(page.getByText('Acme Corp')).toBeVisible()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('shows empty state when no applications', async ({ page }) => {
|
|
123
|
+
// With MSW intercepting
|
|
124
|
+
await expect(page.getByText(/no applications yet/i)).toBeVisible()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Cross-User Isolation — REQUIRED security test (one per resource)
|
|
130
|
+
|
|
131
|
+
The single most important security test: confirm User B cannot reach User A's resource,
|
|
132
|
+
and that the app reveals **404, not 403** (a 403 leaks that the resource exists).
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// tests/e2e/isolation.spec.ts
|
|
136
|
+
import { test, expect } from '@playwright/test'
|
|
137
|
+
import { loginAs } from './helpers/auth' // logs in + returns to a known URL
|
|
138
|
+
|
|
139
|
+
test('User B cannot access User A resource (404, not 403, not the data)', async ({ page }) => {
|
|
140
|
+
// 1. User A creates a resource and we capture its id
|
|
141
|
+
await loginAs(page, 'usera@example.com')
|
|
142
|
+
await page.getByRole('button', { name: 'New Application' }).click()
|
|
143
|
+
await page.getByLabel('Company').fill('Acme Corp')
|
|
144
|
+
await page.getByRole('button', { name: 'Create' }).click()
|
|
145
|
+
await expect(page).toHaveURL(/\/applications\/(?<id>[\w-]+)$/)
|
|
146
|
+
const url = page.url()
|
|
147
|
+
|
|
148
|
+
// 2. User B logs in and visits A's resource URL directly
|
|
149
|
+
await loginAs(page, 'userb@example.com')
|
|
150
|
+
const res = await page.goto(url)
|
|
151
|
+
|
|
152
|
+
// 3. Must be denied as not-found — never expose A's data, never a bare 403
|
|
153
|
+
expect(res?.status()).toBe(404)
|
|
154
|
+
await expect(page.getByText('Acme Corp')).toHaveCount(0)
|
|
155
|
+
await expect(page.getByText(/not found/i)).toBeVisible()
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Add one of these per user-owned resource type. The backend enforces isolation via
|
|
160
|
+
`userId in WHERE`; this test proves the frontend surfaces it correctly.
|
|
161
|
+
|
|
162
|
+
## Visual Regression (Playwright Snapshots)
|
|
163
|
+
```typescript
|
|
164
|
+
test('application card snapshot', async ({ page }) => {
|
|
165
|
+
await page.goto('/applications')
|
|
166
|
+
await page.waitForSelector('[data-testid="application-card"]')
|
|
167
|
+
await expect(page.locator('[data-testid="application-card"]').first())
|
|
168
|
+
.toHaveScreenshot('application-card.png')
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## MSW Setup (required)
|
|
173
|
+
```typescript
|
|
174
|
+
// tests/mocks/server.ts
|
|
175
|
+
import { setupServer } from 'msw/node'
|
|
176
|
+
import { handlers } from './handlers'
|
|
177
|
+
export const server = setupServer(...handlers)
|
|
178
|
+
|
|
179
|
+
// tests/mocks/handlers.ts
|
|
180
|
+
import { http, HttpResponse } from 'msw'
|
|
181
|
+
export const handlers = [
|
|
182
|
+
http.get('/api/v1/applications', () =>
|
|
183
|
+
HttpResponse.json({ items: [], has_more: false, next_cursor_id: null })
|
|
184
|
+
),
|
|
185
|
+
// Add per-test overrides via server.use() in individual tests
|
|
186
|
+
]
|
|
187
|
+
```
|