@girardmedia/bootspring 1.2.0 → 2.0.3
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 +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
# Integration Testing Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for end-to-end integration tests.
|
|
4
|
-
|
|
5
|
-
## Database Integration Tests
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// __tests__/integration/setup.ts
|
|
9
|
-
import { PrismaClient } from '@prisma/client'
|
|
10
|
-
import { execSync } from 'child_process'
|
|
11
|
-
|
|
12
|
-
const prisma = new PrismaClient()
|
|
13
|
-
|
|
14
|
-
export async function setupTestDatabase() {
|
|
15
|
-
// Reset database before tests
|
|
16
|
-
await prisma.$executeRawUnsafe(`
|
|
17
|
-
TRUNCATE TABLE "User", "Post", "Comment" CASCADE;
|
|
18
|
-
`)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function teardownTestDatabase() {
|
|
22
|
-
await prisma.$disconnect()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Or use test containers
|
|
26
|
-
import { PostgreSqlContainer } from '@testcontainers/postgresql'
|
|
27
|
-
|
|
28
|
-
let container: PostgreSqlContainer
|
|
29
|
-
|
|
30
|
-
export async function startTestContainer() {
|
|
31
|
-
container = await new PostgreSqlContainer()
|
|
32
|
-
.withDatabase('testdb')
|
|
33
|
-
.withUsername('test')
|
|
34
|
-
.withPassword('test')
|
|
35
|
-
.start()
|
|
36
|
-
|
|
37
|
-
process.env.DATABASE_URL = container.getConnectionUri()
|
|
38
|
-
|
|
39
|
-
// Run migrations
|
|
40
|
-
execSync('npx prisma migrate deploy', {
|
|
41
|
-
env: { ...process.env, DATABASE_URL: container.getConnectionUri() }
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function stopTestContainer() {
|
|
46
|
-
await container?.stop()
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## User Flow Integration Test
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
// __tests__/integration/user-flow.test.ts
|
|
54
|
-
import { prisma } from '@/lib/db'
|
|
55
|
-
import { POST as createUser } from '@/app/api/users/route'
|
|
56
|
-
import { POST as login } from '@/app/api/auth/login/route'
|
|
57
|
-
import { POST as createPost } from '@/app/api/posts/route'
|
|
58
|
-
import { hash } from 'bcryptjs'
|
|
59
|
-
|
|
60
|
-
describe('User Flow Integration', () => {
|
|
61
|
-
beforeEach(async () => {
|
|
62
|
-
await prisma.post.deleteMany()
|
|
63
|
-
await prisma.user.deleteMany()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
afterAll(async () => {
|
|
67
|
-
await prisma.$disconnect()
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('completes full user registration and posting flow', async () => {
|
|
71
|
-
// Step 1: Register user
|
|
72
|
-
const registerRequest = new Request('http://localhost/api/users', {
|
|
73
|
-
method: 'POST',
|
|
74
|
-
headers: { 'Content-Type': 'application/json' },
|
|
75
|
-
body: JSON.stringify({
|
|
76
|
-
email: 'test@example.com',
|
|
77
|
-
password: 'password123',
|
|
78
|
-
name: 'Test User'
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const registerResponse = await createUser(registerRequest)
|
|
83
|
-
expect(registerResponse.status).toBe(201)
|
|
84
|
-
|
|
85
|
-
const user = await registerResponse.json()
|
|
86
|
-
expect(user.email).toBe('test@example.com')
|
|
87
|
-
|
|
88
|
-
// Step 2: Login
|
|
89
|
-
const loginRequest = new Request('http://localhost/api/auth/login', {
|
|
90
|
-
method: 'POST',
|
|
91
|
-
headers: { 'Content-Type': 'application/json' },
|
|
92
|
-
body: JSON.stringify({
|
|
93
|
-
email: 'test@example.com',
|
|
94
|
-
password: 'password123'
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
const loginResponse = await login(loginRequest)
|
|
99
|
-
expect(loginResponse.status).toBe(200)
|
|
100
|
-
|
|
101
|
-
const { token } = await loginResponse.json()
|
|
102
|
-
expect(token).toBeDefined()
|
|
103
|
-
|
|
104
|
-
// Step 3: Create post
|
|
105
|
-
const postRequest = new Request('http://localhost/api/posts', {
|
|
106
|
-
method: 'POST',
|
|
107
|
-
headers: {
|
|
108
|
-
'Content-Type': 'application/json',
|
|
109
|
-
Authorization: `Bearer ${token}`
|
|
110
|
-
},
|
|
111
|
-
body: JSON.stringify({
|
|
112
|
-
title: 'My First Post',
|
|
113
|
-
content: 'Hello world!'
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
const postResponse = await createPost(postRequest)
|
|
118
|
-
expect(postResponse.status).toBe(201)
|
|
119
|
-
|
|
120
|
-
const post = await postResponse.json()
|
|
121
|
-
expect(post.title).toBe('My First Post')
|
|
122
|
-
expect(post.authorId).toBe(user.id)
|
|
123
|
-
|
|
124
|
-
// Verify in database
|
|
125
|
-
const dbPost = await prisma.post.findUnique({
|
|
126
|
-
where: { id: post.id },
|
|
127
|
-
include: { author: true }
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
expect(dbPost?.author.email).toBe('test@example.com')
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## API Integration Tests with Supertest
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
// __tests__/integration/api.test.ts
|
|
139
|
-
import request from 'supertest'
|
|
140
|
-
import { createServer } from 'http'
|
|
141
|
-
import { parse } from 'url'
|
|
142
|
-
import next from 'next'
|
|
143
|
-
|
|
144
|
-
const dev = process.env.NODE_ENV !== 'production'
|
|
145
|
-
const app = next({ dev })
|
|
146
|
-
const handle = app.getRequestHandler()
|
|
147
|
-
|
|
148
|
-
let server: ReturnType<typeof createServer>
|
|
149
|
-
|
|
150
|
-
beforeAll(async () => {
|
|
151
|
-
await app.prepare()
|
|
152
|
-
server = createServer((req, res) => {
|
|
153
|
-
const parsedUrl = parse(req.url!, true)
|
|
154
|
-
handle(req, res, parsedUrl)
|
|
155
|
-
})
|
|
156
|
-
await new Promise<void>(resolve => server.listen(0, resolve))
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
afterAll(async () => {
|
|
160
|
-
await new Promise<void>(resolve => server.close(() => resolve()))
|
|
161
|
-
await app.close()
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
describe('API Integration Tests', () => {
|
|
165
|
-
it('GET /api/health returns healthy status', async () => {
|
|
166
|
-
const res = await request(server)
|
|
167
|
-
.get('/api/health')
|
|
168
|
-
.expect(200)
|
|
169
|
-
|
|
170
|
-
expect(res.body.status).toBe('healthy')
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('POST /api/users creates a new user', async () => {
|
|
174
|
-
const res = await request(server)
|
|
175
|
-
.post('/api/users')
|
|
176
|
-
.send({
|
|
177
|
-
email: 'new@example.com',
|
|
178
|
-
name: 'New User',
|
|
179
|
-
password: 'password123'
|
|
180
|
-
})
|
|
181
|
-
.expect(201)
|
|
182
|
-
|
|
183
|
-
expect(res.body.email).toBe('new@example.com')
|
|
184
|
-
expect(res.body.password).toBeUndefined()
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('GET /api/users requires authentication', async () => {
|
|
188
|
-
await request(server)
|
|
189
|
-
.get('/api/users')
|
|
190
|
-
.expect(401)
|
|
191
|
-
})
|
|
192
|
-
})
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Service Integration Tests
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
// __tests__/integration/services.test.ts
|
|
199
|
-
import { PaymentService } from '@/services/payment'
|
|
200
|
-
import { EmailService } from '@/services/email'
|
|
201
|
-
import { prisma } from '@/lib/db'
|
|
202
|
-
|
|
203
|
-
// Mock external services
|
|
204
|
-
vi.mock('@/services/email', () => ({
|
|
205
|
-
EmailService: {
|
|
206
|
-
sendWelcomeEmail: vi.fn().mockResolvedValue(true),
|
|
207
|
-
sendReceiptEmail: vi.fn().mockResolvedValue(true)
|
|
208
|
-
}
|
|
209
|
-
}))
|
|
210
|
-
|
|
211
|
-
describe('Payment Service Integration', () => {
|
|
212
|
-
beforeEach(async () => {
|
|
213
|
-
await prisma.payment.deleteMany()
|
|
214
|
-
await prisma.subscription.deleteMany()
|
|
215
|
-
vi.clearAllMocks()
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('processes subscription and sends confirmation', async () => {
|
|
219
|
-
const user = await prisma.user.create({
|
|
220
|
-
data: {
|
|
221
|
-
email: 'subscriber@example.com',
|
|
222
|
-
name: 'Subscriber'
|
|
223
|
-
}
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
// Process subscription
|
|
227
|
-
const subscription = await PaymentService.createSubscription({
|
|
228
|
-
userId: user.id,
|
|
229
|
-
planId: 'pro',
|
|
230
|
-
paymentMethodId: 'pm_test_123'
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
expect(subscription.status).toBe('active')
|
|
234
|
-
expect(subscription.planId).toBe('pro')
|
|
235
|
-
|
|
236
|
-
// Verify database state
|
|
237
|
-
const dbSubscription = await prisma.subscription.findUnique({
|
|
238
|
-
where: { id: subscription.id }
|
|
239
|
-
})
|
|
240
|
-
expect(dbSubscription?.userId).toBe(user.id)
|
|
241
|
-
|
|
242
|
-
// Verify email was sent
|
|
243
|
-
expect(EmailService.sendReceiptEmail).toHaveBeenCalledWith(
|
|
244
|
-
expect.objectContaining({
|
|
245
|
-
email: 'subscriber@example.com',
|
|
246
|
-
subscriptionId: subscription.id
|
|
247
|
-
})
|
|
248
|
-
)
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('handles failed payment gracefully', async () => {
|
|
252
|
-
const user = await prisma.user.create({
|
|
253
|
-
data: {
|
|
254
|
-
email: 'fail@example.com',
|
|
255
|
-
name: 'Failed User'
|
|
256
|
-
}
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
await expect(
|
|
260
|
-
PaymentService.createSubscription({
|
|
261
|
-
userId: user.id,
|
|
262
|
-
planId: 'pro',
|
|
263
|
-
paymentMethodId: 'pm_card_declined'
|
|
264
|
-
})
|
|
265
|
-
).rejects.toThrow('Payment declined')
|
|
266
|
-
|
|
267
|
-
// Verify no subscription was created
|
|
268
|
-
const subscriptions = await prisma.subscription.findMany({
|
|
269
|
-
where: { userId: user.id }
|
|
270
|
-
})
|
|
271
|
-
expect(subscriptions).toHaveLength(0)
|
|
272
|
-
})
|
|
273
|
-
})
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Webhook Integration Tests
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
279
|
-
// __tests__/integration/webhooks.test.ts
|
|
280
|
-
import crypto from 'crypto'
|
|
281
|
-
import { POST } from '@/app/api/webhooks/stripe/route'
|
|
282
|
-
import { prisma } from '@/lib/db'
|
|
283
|
-
|
|
284
|
-
function createStripeSignature(payload: string, secret: string): string {
|
|
285
|
-
const timestamp = Math.floor(Date.now() / 1000)
|
|
286
|
-
const signedPayload = `${timestamp}.${payload}`
|
|
287
|
-
const signature = crypto
|
|
288
|
-
.createHmac('sha256', secret)
|
|
289
|
-
.update(signedPayload)
|
|
290
|
-
.digest('hex')
|
|
291
|
-
|
|
292
|
-
return `t=${timestamp},v1=${signature}`
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
describe('Stripe Webhook Integration', () => {
|
|
296
|
-
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
|
|
297
|
-
|
|
298
|
-
beforeEach(async () => {
|
|
299
|
-
await prisma.payment.deleteMany()
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
it('handles payment_intent.succeeded event', async () => {
|
|
303
|
-
const user = await prisma.user.create({
|
|
304
|
-
data: {
|
|
305
|
-
email: 'payer@example.com',
|
|
306
|
-
name: 'Payer',
|
|
307
|
-
stripeCustomerId: 'cus_test_123'
|
|
308
|
-
}
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
const event = {
|
|
312
|
-
id: 'evt_test_123',
|
|
313
|
-
type: 'payment_intent.succeeded',
|
|
314
|
-
data: {
|
|
315
|
-
object: {
|
|
316
|
-
id: 'pi_test_123',
|
|
317
|
-
amount: 2000,
|
|
318
|
-
currency: 'usd',
|
|
319
|
-
customer: 'cus_test_123',
|
|
320
|
-
metadata: { userId: user.id }
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const payload = JSON.stringify(event)
|
|
326
|
-
const signature = createStripeSignature(payload, webhookSecret)
|
|
327
|
-
|
|
328
|
-
const request = new Request('http://localhost/api/webhooks/stripe', {
|
|
329
|
-
method: 'POST',
|
|
330
|
-
headers: {
|
|
331
|
-
'Content-Type': 'application/json',
|
|
332
|
-
'stripe-signature': signature
|
|
333
|
-
},
|
|
334
|
-
body: payload
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
const response = await POST(request)
|
|
338
|
-
expect(response.status).toBe(200)
|
|
339
|
-
|
|
340
|
-
// Verify payment was recorded
|
|
341
|
-
const payment = await prisma.payment.findFirst({
|
|
342
|
-
where: { stripePaymentIntentId: 'pi_test_123' }
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
expect(payment).toBeDefined()
|
|
346
|
-
expect(payment?.amount).toBe(2000)
|
|
347
|
-
expect(payment?.status).toBe('succeeded')
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
it('rejects invalid signatures', async () => {
|
|
351
|
-
const event = {
|
|
352
|
-
id: 'evt_test_456',
|
|
353
|
-
type: 'payment_intent.succeeded',
|
|
354
|
-
data: { object: {} }
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const request = new Request('http://localhost/api/webhooks/stripe', {
|
|
358
|
-
method: 'POST',
|
|
359
|
-
headers: {
|
|
360
|
-
'Content-Type': 'application/json',
|
|
361
|
-
'stripe-signature': 'invalid_signature'
|
|
362
|
-
},
|
|
363
|
-
body: JSON.stringify(event)
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
const response = await POST(request)
|
|
367
|
-
expect(response.status).toBe(400)
|
|
368
|
-
})
|
|
369
|
-
})
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
## Full Stack Integration Test
|
|
373
|
-
|
|
374
|
-
```typescript
|
|
375
|
-
// __tests__/integration/full-stack.test.ts
|
|
376
|
-
import { test, expect } from '@playwright/test'
|
|
377
|
-
import { prisma } from '@/lib/db'
|
|
378
|
-
|
|
379
|
-
test.describe('Full Stack Integration', () => {
|
|
380
|
-
test.beforeEach(async () => {
|
|
381
|
-
await prisma.user.deleteMany()
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
test('complete user journey from signup to purchase', async ({ page }) => {
|
|
385
|
-
// Step 1: Visit homepage
|
|
386
|
-
await page.goto('/')
|
|
387
|
-
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible()
|
|
388
|
-
|
|
389
|
-
// Step 2: Sign up
|
|
390
|
-
await page.click('text=Sign Up')
|
|
391
|
-
await page.fill('input[name="email"]', 'e2e@example.com')
|
|
392
|
-
await page.fill('input[name="password"]', 'password123')
|
|
393
|
-
await page.fill('input[name="name"]', 'E2E User')
|
|
394
|
-
await page.click('button[type="submit"]')
|
|
395
|
-
|
|
396
|
-
// Step 3: Verify redirect to dashboard
|
|
397
|
-
await expect(page).toHaveURL('/dashboard')
|
|
398
|
-
await expect(page.getByText('Welcome, E2E User')).toBeVisible()
|
|
399
|
-
|
|
400
|
-
// Step 4: Navigate to pricing
|
|
401
|
-
await page.click('text=Upgrade')
|
|
402
|
-
await expect(page).toHaveURL('/pricing')
|
|
403
|
-
|
|
404
|
-
// Step 5: Select a plan
|
|
405
|
-
await page.click('text=Pro Plan')
|
|
406
|
-
await expect(page).toHaveURL('/checkout/pro')
|
|
407
|
-
|
|
408
|
-
// Step 6: Fill payment details (test mode)
|
|
409
|
-
const stripeFrame = page.frameLocator('iframe[name*="stripe"]')
|
|
410
|
-
await stripeFrame.locator('input[name="cardnumber"]').fill('4242424242424242')
|
|
411
|
-
await stripeFrame.locator('input[name="exp-date"]').fill('12/30')
|
|
412
|
-
await stripeFrame.locator('input[name="cvc"]').fill('123')
|
|
413
|
-
|
|
414
|
-
await page.click('text=Subscribe')
|
|
415
|
-
|
|
416
|
-
// Step 7: Verify success
|
|
417
|
-
await expect(page.getByText('Subscription Active')).toBeVisible()
|
|
418
|
-
|
|
419
|
-
// Verify in database
|
|
420
|
-
const user = await prisma.user.findUnique({
|
|
421
|
-
where: { email: 'e2e@example.com' },
|
|
422
|
-
include: { subscription: true }
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
expect(user?.subscription?.planId).toBe('pro')
|
|
426
|
-
expect(user?.subscription?.status).toBe('active')
|
|
427
|
-
})
|
|
428
|
-
})
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
## When to Use
|
|
432
|
-
|
|
433
|
-
- Critical user flows
|
|
434
|
-
- Payment processing
|
|
435
|
-
- Multi-service interactions
|
|
436
|
-
- Database transactions
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
# Mocking Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for mocking dependencies in tests.
|
|
4
|
-
|
|
5
|
-
## Mock Functions
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// Basic mock function
|
|
9
|
-
const mockFn = vi.fn()
|
|
10
|
-
mockFn('arg1', 'arg2')
|
|
11
|
-
|
|
12
|
-
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
|
|
13
|
-
expect(mockFn).toHaveBeenCalledTimes(1)
|
|
14
|
-
|
|
15
|
-
// Mock with return value
|
|
16
|
-
const mockGet = vi.fn().mockReturnValue('result')
|
|
17
|
-
const mockAsync = vi.fn().mockResolvedValue({ data: 'test' })
|
|
18
|
-
|
|
19
|
-
// Mock implementation
|
|
20
|
-
const mockImpl = vi.fn().mockImplementation((x) => x * 2)
|
|
21
|
-
expect(mockImpl(5)).toBe(10)
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Module Mocking
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
// __tests__/service.test.ts
|
|
28
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
29
|
-
import { sendEmail } from '@/lib/email'
|
|
30
|
-
import { createUser } from '@/services/user'
|
|
31
|
-
|
|
32
|
-
// Mock entire module
|
|
33
|
-
vi.mock('@/lib/email', () => ({
|
|
34
|
-
sendEmail: vi.fn().mockResolvedValue({ success: true })
|
|
35
|
-
}))
|
|
36
|
-
|
|
37
|
-
describe('createUser', () => {
|
|
38
|
-
it('sends welcome email', async () => {
|
|
39
|
-
await createUser({ email: 'test@example.com', name: 'Test' })
|
|
40
|
-
|
|
41
|
-
expect(sendEmail).toHaveBeenCalledWith(
|
|
42
|
-
expect.objectContaining({
|
|
43
|
-
to: 'test@example.com',
|
|
44
|
-
template: 'welcome'
|
|
45
|
-
})
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Partial Mocking
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
// Mock specific exports
|
|
55
|
-
vi.mock('@/lib/utils', async () => {
|
|
56
|
-
const actual = await vi.importActual('@/lib/utils')
|
|
57
|
-
return {
|
|
58
|
-
...actual,
|
|
59
|
-
generateId: vi.fn().mockReturnValue('test-id')
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Spy on Methods
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
// Spy without replacing
|
|
68
|
-
const consoleSpy = vi.spyOn(console, 'log')
|
|
69
|
-
myFunction()
|
|
70
|
-
expect(consoleSpy).toHaveBeenCalledWith('expected message')
|
|
71
|
-
consoleSpy.mockRestore()
|
|
72
|
-
|
|
73
|
-
// Spy and mock
|
|
74
|
-
const dateSpy = vi.spyOn(Date, 'now').mockReturnValue(1234567890)
|
|
75
|
-
expect(Date.now()).toBe(1234567890)
|
|
76
|
-
dateSpy.mockRestore()
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Mock Timers
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
describe('debounce', () => {
|
|
83
|
-
beforeEach(() => {
|
|
84
|
-
vi.useFakeTimers()
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
afterEach(() => {
|
|
88
|
-
vi.useRealTimers()
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('debounces function calls', () => {
|
|
92
|
-
const fn = vi.fn()
|
|
93
|
-
const debounced = debounce(fn, 100)
|
|
94
|
-
|
|
95
|
-
debounced()
|
|
96
|
-
debounced()
|
|
97
|
-
debounced()
|
|
98
|
-
|
|
99
|
-
expect(fn).not.toHaveBeenCalled()
|
|
100
|
-
|
|
101
|
-
vi.advanceTimersByTime(100)
|
|
102
|
-
|
|
103
|
-
expect(fn).toHaveBeenCalledTimes(1)
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Mock Fetch/HTTP
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// Using msw (Mock Service Worker)
|
|
112
|
-
import { http, HttpResponse } from 'msw'
|
|
113
|
-
import { setupServer } from 'msw/node'
|
|
114
|
-
|
|
115
|
-
const server = setupServer(
|
|
116
|
-
http.get('/api/users', () => {
|
|
117
|
-
return HttpResponse.json([
|
|
118
|
-
{ id: '1', name: 'Test User' }
|
|
119
|
-
])
|
|
120
|
-
}),
|
|
121
|
-
|
|
122
|
-
http.post('/api/users', async ({ request }) => {
|
|
123
|
-
const body = await request.json()
|
|
124
|
-
return HttpResponse.json({ id: '2', ...body }, { status: 201 })
|
|
125
|
-
})
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
beforeAll(() => server.listen())
|
|
129
|
-
afterEach(() => server.resetHandlers())
|
|
130
|
-
afterAll(() => server.close())
|
|
131
|
-
|
|
132
|
-
// Override for specific test
|
|
133
|
-
it('handles error', async () => {
|
|
134
|
-
server.use(
|
|
135
|
-
http.get('/api/users', () => {
|
|
136
|
-
return HttpResponse.json({ error: 'Not found' }, { status: 404 })
|
|
137
|
-
})
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
// Test error handling...
|
|
141
|
-
})
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## Mock Prisma
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
// __mocks__/prisma.ts
|
|
148
|
-
import { PrismaClient } from '@prisma/client'
|
|
149
|
-
import { mockDeep, DeepMockProxy } from 'vitest-mock-extended'
|
|
150
|
-
|
|
151
|
-
export const prismaMock = mockDeep<PrismaClient>()
|
|
152
|
-
|
|
153
|
-
vi.mock('@/lib/db', () => ({
|
|
154
|
-
prisma: prismaMock
|
|
155
|
-
}))
|
|
156
|
-
|
|
157
|
-
// In test
|
|
158
|
-
import { prismaMock } from '../__mocks__/prisma'
|
|
159
|
-
|
|
160
|
-
it('finds user', async () => {
|
|
161
|
-
prismaMock.user.findUnique.mockResolvedValue({
|
|
162
|
-
id: '1',
|
|
163
|
-
email: 'test@example.com',
|
|
164
|
-
name: 'Test'
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
const user = await getUserById('1')
|
|
168
|
-
expect(user.email).toBe('test@example.com')
|
|
169
|
-
})
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## When to Use
|
|
173
|
-
|
|
174
|
-
- External services
|
|
175
|
-
- Database calls
|
|
176
|
-
- Time-dependent code
|
|
177
|
-
- Random values
|