@su-record/vibe 2.7.13 → 2.7.15
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/.env.example +37 -37
- package/CLAUDE.md +134 -126
- package/LICENSE +21 -21
- package/README.md +449 -449
- package/agents/architect-low.md +41 -41
- package/agents/architect-medium.md +59 -59
- package/agents/architect.md +80 -80
- package/agents/build-error-resolver.md +115 -115
- package/agents/compounder.md +261 -261
- package/agents/diagrammer.md +178 -178
- package/agents/docs/api-documenter.md +99 -99
- package/agents/docs/changelog-writer.md +93 -93
- package/agents/e2e-tester.md +294 -294
- package/agents/explorer-low.md +42 -42
- package/agents/explorer-medium.md +59 -59
- package/agents/explorer.md +48 -48
- package/agents/implementer-low.md +43 -43
- package/agents/implementer-medium.md +52 -52
- package/agents/implementer.md +54 -54
- package/agents/junior-mentor.md +141 -141
- package/agents/planning/requirements-analyst.md +84 -84
- package/agents/planning/ux-advisor.md +83 -83
- package/agents/qa/acceptance-tester.md +86 -86
- package/agents/qa/edge-case-finder.md +93 -93
- package/agents/refactor-cleaner.md +143 -143
- package/agents/research/best-practices-agent.md +199 -199
- package/agents/research/codebase-patterns-agent.md +157 -157
- package/agents/research/framework-docs-agent.md +188 -188
- package/agents/research/security-advisory-agent.md +213 -213
- package/agents/review/architecture-reviewer.md +107 -107
- package/agents/review/complexity-reviewer.md +116 -116
- package/agents/review/data-integrity-reviewer.md +88 -88
- package/agents/review/git-history-reviewer.md +103 -103
- package/agents/review/performance-reviewer.md +86 -86
- package/agents/review/python-reviewer.md +150 -150
- package/agents/review/rails-reviewer.md +139 -139
- package/agents/review/react-reviewer.md +144 -144
- package/agents/review/security-reviewer.md +80 -80
- package/agents/review/simplicity-reviewer.md +140 -140
- package/agents/review/test-coverage-reviewer.md +116 -116
- package/agents/review/typescript-reviewer.md +127 -127
- package/agents/searcher.md +54 -54
- package/agents/simplifier.md +120 -120
- package/agents/tester.md +49 -49
- package/agents/ui/ui-a11y-auditor.md +93 -93
- package/agents/ui/ui-antipattern-detector.md +94 -94
- package/agents/ui/ui-dataviz-advisor.md +69 -69
- package/agents/ui/ui-design-system-gen.md +57 -57
- package/agents/ui/ui-industry-analyzer.md +49 -49
- package/agents/ui/ui-layout-architect.md +65 -65
- package/agents/ui/ui-stack-implementer.md +68 -68
- package/agents/ui/ux-compliance-reviewer.md +81 -81
- package/agents/ui-previewer.md +258 -260
- package/commands/vibe.analyze.md +11 -13
- package/commands/vibe.review.md +43 -1
- package/commands/vibe.run.md +2124 -2078
- package/commands/vibe.spec.md +9 -4
- package/commands/vibe.spec.review.md +569 -565
- package/commands/vibe.utils.md +413 -413
- package/commands/vibe.verify.md +33 -8
- package/dist/cli/collaborator.js +52 -52
- package/dist/cli/commands/evolution.js +12 -12
- package/dist/cli/commands/info.js +54 -54
- package/dist/cli/commands/init.js +5 -5
- package/dist/cli/commands/remove.js +14 -14
- package/dist/cli/commands/sentinel.js +27 -27
- package/dist/cli/commands/skills.js +5 -5
- package/dist/cli/commands/slack.js +10 -10
- package/dist/cli/commands/telegram.js +12 -12
- package/dist/cli/detect.js +32 -32
- package/dist/cli/index.js +51 -51
- package/dist/cli/llm/claude-commands.js +16 -16
- package/dist/cli/llm/config.js +19 -19
- package/dist/cli/llm/config.js.map +1 -1
- package/dist/cli/llm/gemini-commands.js +16 -16
- package/dist/cli/llm/gpt-commands.js +19 -19
- package/dist/cli/llm/help.js +21 -21
- package/dist/cli/postinstall/cursor-agents.js +32 -32
- package/dist/cli/postinstall/cursor-rules.js +83 -83
- package/dist/cli/postinstall/cursor-skills.js +743 -743
- package/dist/cli/setup/Provisioner.js +42 -42
- package/dist/cli/types.d.ts +0 -2
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/infra/lib/DeepInit.js +24 -24
- package/dist/infra/lib/IterationTracker.js +11 -11
- package/dist/infra/lib/PythonParser.js +108 -108
- package/dist/infra/lib/ReviewRace.js +96 -96
- package/dist/infra/lib/SkillFrontmatter.js +28 -28
- package/dist/infra/lib/SkillQualityGate.js +9 -9
- package/dist/infra/lib/SkillRepository.js +159 -159
- package/dist/infra/lib/UltraQA.js +99 -99
- package/dist/infra/lib/autonomy/AuditStore.js +41 -41
- package/dist/infra/lib/autonomy/ConfirmationStore.js +30 -30
- package/dist/infra/lib/autonomy/EventOutbox.js +38 -38
- package/dist/infra/lib/autonomy/PolicyEngine.js +18 -18
- package/dist/infra/lib/autonomy/SecuritySentinel.js +1 -1
- package/dist/infra/lib/autonomy/SuggestionStore.js +33 -33
- package/dist/infra/lib/embedding/VectorStore.js +22 -22
- package/dist/infra/lib/evolution/AgentAnalyzer.js +10 -10
- package/dist/infra/lib/evolution/DescriptionOptimizer.js +21 -21
- package/dist/infra/lib/evolution/GenerationRegistry.js +36 -36
- package/dist/infra/lib/evolution/InsightStore.js +90 -90
- package/dist/infra/lib/evolution/RollbackManager.js +5 -5
- package/dist/infra/lib/evolution/SkillBenchmark.js +23 -23
- package/dist/infra/lib/evolution/SkillEvalRunner.js +50 -50
- package/dist/infra/lib/evolution/SkillGapDetector.js +10 -10
- package/dist/infra/lib/evolution/UsageTracker.js +28 -28
- package/dist/infra/lib/gemini/orchestration.js +5 -5
- package/dist/infra/lib/gpt/orchestration.js +4 -4
- package/dist/infra/lib/memory/KnowledgeGraph.js +4 -4
- package/dist/infra/lib/memory/MemorySearch.js +57 -57
- package/dist/infra/lib/memory/MemoryStorage.js +181 -181
- package/dist/infra/lib/memory/ObservationStore.js +28 -28
- package/dist/infra/lib/memory/ReflectionStore.js +30 -30
- package/dist/infra/lib/memory/SessionRAGRetriever.js +7 -7
- package/dist/infra/lib/memory/SessionRAGStore.js +225 -225
- package/dist/infra/lib/memory/SessionSummarizer.js +9 -9
- package/dist/infra/orchestrator/AgentManager.js +12 -12
- package/dist/infra/orchestrator/AgentRegistry.js +65 -65
- package/dist/infra/orchestrator/MultiLlmResearch.js +8 -8
- package/dist/infra/orchestrator/SwarmOrchestrator.test.js +16 -16
- package/dist/infra/orchestrator/parallelResearch.js +24 -24
- package/dist/tools/convention/analyzeComplexity.test.js +115 -115
- package/dist/tools/convention/validateCodeQuality.test.js +104 -104
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +23 -23
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/dist/tools/semantic/astGrep.test.js +6 -6
- package/dist/tools/spec/prdParser.test.js +171 -171
- package/dist/tools/spec/specGenerator.js +169 -169
- package/dist/tools/spec/traceabilityMatrix.js +64 -64
- package/dist/tools/spec/traceabilityMatrix.test.js +28 -28
- package/hooks/gemini-hooks.json +73 -73
- package/hooks/hooks.json +137 -137
- package/hooks/scripts/code-check.js +77 -70
- package/hooks/scripts/context-save.js +212 -212
- package/hooks/scripts/hud-status.js +291 -291
- package/hooks/scripts/keyword-detector.js +214 -214
- package/hooks/scripts/llm-orchestrate.js +475 -475
- package/hooks/scripts/post-edit.js +32 -32
- package/hooks/scripts/pre-tool-guard.js +125 -125
- package/hooks/scripts/prompt-dispatcher.js +185 -185
- package/hooks/scripts/sentinel-guard.js +104 -104
- package/hooks/scripts/session-start.js +106 -106
- package/hooks/scripts/stop-notify.js +209 -209
- package/hooks/scripts/utils.js +100 -100
- package/languages/csharp-unity.md +515 -515
- package/languages/gdscript-godot.md +470 -470
- package/languages/ruby-rails.md +489 -489
- package/languages/typescript-angular.md +433 -433
- package/languages/typescript-astro.md +416 -416
- package/languages/typescript-electron.md +406 -406
- package/languages/typescript-nestjs.md +524 -524
- package/languages/typescript-svelte.md +407 -407
- package/languages/typescript-tauri.md +365 -365
- package/package.json +121 -121
- package/skills/agents-md/SKILL.md +120 -120
- package/skills/arch-guard/SKILL.md +180 -180
- package/skills/brand-assets/SKILL.md +146 -146
- package/skills/capability-loop/SKILL.md +167 -167
- package/skills/characterization-test/SKILL.md +206 -206
- package/skills/commerce-patterns/SKILL.md +59 -59
- package/skills/commit-push-pr/SKILL.md +75 -75
- package/skills/context7-usage/SKILL.md +105 -105
- package/skills/core-capabilities/SKILL.md +48 -48
- package/skills/e2e-commerce/SKILL.md +57 -57
- package/skills/exec-plan/SKILL.md +147 -147
- package/skills/frontend-design/SKILL.md +73 -73
- package/skills/git-worktree/SKILL.md +72 -72
- package/skills/handoff/SKILL.md +109 -109
- package/skills/parallel-research/SKILL.md +87 -87
- package/skills/priority-todos/SKILL.md +63 -63
- package/skills/seo-checklist/SKILL.md +57 -57
- package/skills/techdebt/SKILL.md +122 -122
- package/skills/tool-fallback/SKILL.md +103 -103
- package/skills/typescript-advanced-types/SKILL.md +66 -65
- package/skills/ui-ux-pro-max/SKILL.md +206 -206
- package/skills/vercel-react-best-practices/SKILL.md +59 -59
- package/skills/video-production/SKILL.md +51 -51
- package/vibe/config.json +29 -29
- package/vibe/constitution.md +227 -227
- package/vibe/rules/principles/communication-guide.md +98 -98
- package/vibe/rules/principles/development-philosophy.md +52 -52
- package/vibe/rules/principles/quick-start.md +102 -102
- package/vibe/rules/quality/bdd-contract-testing.md +393 -393
- package/vibe/rules/quality/checklist.md +276 -276
- package/vibe/rules/quality/performance.md +236 -236
- package/vibe/rules/quality/testing-strategy.md +440 -440
- package/vibe/rules/standards/anti-patterns.md +541 -541
- package/vibe/rules/standards/code-structure.md +291 -291
- package/vibe/rules/standards/complexity-metrics.md +313 -313
- package/vibe/rules/standards/git-workflow.md +237 -237
- package/vibe/rules/standards/naming-conventions.md +198 -198
- package/vibe/rules/standards/security.md +305 -305
- package/vibe/rules/writing/document-style.md +74 -74
- package/vibe/setup.sh +31 -31
- package/vibe/templates/constitution-template.md +252 -252
- package/vibe/templates/contract-backend-template.md +526 -526
- package/vibe/templates/contract-frontend-template.md +599 -599
- package/vibe/templates/feature-template.md +96 -96
- package/vibe/templates/spec-template.md +221 -221
- package/vibe/ui-ux-data/charts.csv +26 -26
- package/vibe/ui-ux-data/colors.csv +97 -97
- package/vibe/ui-ux-data/icons.csv +101 -101
- package/vibe/ui-ux-data/landing.csv +31 -31
- package/vibe/ui-ux-data/products.csv +96 -96
- package/vibe/ui-ux-data/react-performance.csv +45 -45
- package/vibe/ui-ux-data/stacks/astro.csv +54 -54
- package/vibe/ui-ux-data/stacks/flutter.csv +53 -53
- package/vibe/ui-ux-data/stacks/html-tailwind.csv +56 -56
- package/vibe/ui-ux-data/stacks/jetpack-compose.csv +53 -53
- package/vibe/ui-ux-data/stacks/nextjs.csv +53 -53
- package/vibe/ui-ux-data/stacks/nuxt-ui.csv +51 -51
- package/vibe/ui-ux-data/stacks/nuxtjs.csv +59 -59
- package/vibe/ui-ux-data/stacks/react-native.csv +52 -52
- package/vibe/ui-ux-data/stacks/react.csv +54 -54
- package/vibe/ui-ux-data/stacks/shadcn.csv +61 -61
- package/vibe/ui-ux-data/stacks/svelte.csv +54 -54
- package/vibe/ui-ux-data/stacks/swiftui.csv +51 -51
- package/vibe/ui-ux-data/stacks/vue.csv +50 -50
- package/vibe/ui-ux-data/styles.csv +68 -68
- package/vibe/ui-ux-data/typography.csv +57 -57
- package/vibe/ui-ux-data/ui-reasoning.csv +101 -101
- package/vibe/ui-ux-data/ux-guidelines.csv +99 -99
- package/vibe/ui-ux-data/version.json +31 -31
- package/vibe/ui-ux-data/web-interface.csv +31 -31
|
@@ -1,526 +1,526 @@
|
|
|
1
|
-
# Backend Contract Tests: {Feature Name}
|
|
2
|
-
|
|
3
|
-
**Generated from**: `specs/{feature-name}.md` (Section 6: API Contract)
|
|
4
|
-
**Framework**: {FastAPI | Django | Express | NestJS}
|
|
5
|
-
**Language**: {Python | TypeScript | JavaScript}
|
|
6
|
-
**Priority**: {HIGH | MEDIUM | LOW}
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Overview
|
|
11
|
-
|
|
12
|
-
Contract Testing **validates API contracts (schemas)**:
|
|
13
|
-
|
|
14
|
-
- ✅ Request/Response schema compliance
|
|
15
|
-
- ✅ Status code matching
|
|
16
|
-
- ✅ Header validation
|
|
17
|
-
- ✅ Data types and required fields verification
|
|
18
|
-
|
|
19
|
-
**Consumer → Provider contract assurance** (Pact pattern)
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## API Contracts
|
|
24
|
-
|
|
25
|
-
### Contract 1: {Endpoint Name}
|
|
26
|
-
|
|
27
|
-
**Endpoint**: `POST /api/v1/{resource}`
|
|
28
|
-
**Mapped to**: REQ-001 in SPEC
|
|
29
|
-
|
|
30
|
-
#### Request Contract
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"method": "POST",
|
|
35
|
-
"path": "/api/v1/{resource}",
|
|
36
|
-
"headers": {
|
|
37
|
-
"Content-Type": "application/json",
|
|
38
|
-
"Authorization": "Bearer {token}"
|
|
39
|
-
},
|
|
40
|
-
"body": {
|
|
41
|
-
"field1": "string (required)",
|
|
42
|
-
"field2": "integer (required)",
|
|
43
|
-
"field3": "boolean (optional)"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
**JSON Schema**:
|
|
49
|
-
|
|
50
|
-
```json
|
|
51
|
-
{
|
|
52
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
53
|
-
"type": "object",
|
|
54
|
-
"required": ["field1", "field2"],
|
|
55
|
-
"properties": {
|
|
56
|
-
"field1": {
|
|
57
|
-
"type": "string",
|
|
58
|
-
"minLength": 1,
|
|
59
|
-
"maxLength": 100
|
|
60
|
-
},
|
|
61
|
-
"field2": {
|
|
62
|
-
"type": "integer",
|
|
63
|
-
"minimum": 0
|
|
64
|
-
},
|
|
65
|
-
"field3": {
|
|
66
|
-
"type": "boolean",
|
|
67
|
-
"default": false
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"additionalProperties": false
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
#### Response Contract (Success)
|
|
75
|
-
|
|
76
|
-
```json
|
|
77
|
-
{
|
|
78
|
-
"status": 201,
|
|
79
|
-
"headers": {
|
|
80
|
-
"Content-Type": "application/json"
|
|
81
|
-
},
|
|
82
|
-
"body": {
|
|
83
|
-
"id": "uuid",
|
|
84
|
-
"field1": "string",
|
|
85
|
-
"field2": "integer",
|
|
86
|
-
"field3": "boolean",
|
|
87
|
-
"created_at": "datetime (ISO 8601)"
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**JSON Schema**:
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
97
|
-
"type": "object",
|
|
98
|
-
"required": ["id", "field1", "field2", "created_at"],
|
|
99
|
-
"properties": {
|
|
100
|
-
"id": {
|
|
101
|
-
"type": "string",
|
|
102
|
-
"format": "uuid"
|
|
103
|
-
},
|
|
104
|
-
"field1": {
|
|
105
|
-
"type": "string"
|
|
106
|
-
},
|
|
107
|
-
"field2": {
|
|
108
|
-
"type": "integer"
|
|
109
|
-
},
|
|
110
|
-
"field3": {
|
|
111
|
-
"type": "boolean"
|
|
112
|
-
},
|
|
113
|
-
"created_at": {
|
|
114
|
-
"type": "string",
|
|
115
|
-
"format": "date-time"
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
#### Response Contract (Error)
|
|
122
|
-
|
|
123
|
-
```json
|
|
124
|
-
{
|
|
125
|
-
"status": 400,
|
|
126
|
-
"body": {
|
|
127
|
-
"error": "string",
|
|
128
|
-
"message": "string",
|
|
129
|
-
"details": ["array of strings (optional)"]
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## Implementation
|
|
137
|
-
|
|
138
|
-
### Python (FastAPI + Pydantic)
|
|
139
|
-
|
|
140
|
-
**File**: `tests/contract/test_{feature_name}_contract.py`
|
|
141
|
-
|
|
142
|
-
```python
|
|
143
|
-
import pytest
|
|
144
|
-
from fastapi.testclient import TestClient
|
|
145
|
-
from jsonschema import validate, ValidationError
|
|
146
|
-
from app.main import app
|
|
147
|
-
|
|
148
|
-
client = TestClient(app)
|
|
149
|
-
|
|
150
|
-
# JSON Schema definitions
|
|
151
|
-
REQUEST_SCHEMA = {
|
|
152
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
153
|
-
"type": "object",
|
|
154
|
-
"required": ["field1", "field2"],
|
|
155
|
-
"properties": {
|
|
156
|
-
"field1": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
157
|
-
"field2": {"type": "integer", "minimum": 0},
|
|
158
|
-
"field3": {"type": "boolean", "default": False}
|
|
159
|
-
},
|
|
160
|
-
"additionalProperties": False
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
RESPONSE_SCHEMA = {
|
|
164
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
165
|
-
"type": "object",
|
|
166
|
-
"required": ["id", "field1", "field2", "created_at"],
|
|
167
|
-
"properties": {
|
|
168
|
-
"id": {"type": "string", "format": "uuid"},
|
|
169
|
-
"field1": {"type": "string"},
|
|
170
|
-
"field2": {"type": "integer"},
|
|
171
|
-
"field3": {"type": "boolean"},
|
|
172
|
-
"created_at": {"type": "string", "format": "date-time"}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
class TestCreateResourceContract:
|
|
177
|
-
"""Contract tests for POST /api/v1/resource"""
|
|
178
|
-
|
|
179
|
-
def test_request_schema_valid(self):
|
|
180
|
-
"""Request body matches contract schema"""
|
|
181
|
-
payload = {
|
|
182
|
-
"field1": "test value",
|
|
183
|
-
"field2": 42,
|
|
184
|
-
"field3": True
|
|
185
|
-
}
|
|
186
|
-
# Should not raise ValidationError
|
|
187
|
-
validate(instance=payload, schema=REQUEST_SCHEMA)
|
|
188
|
-
|
|
189
|
-
def test_request_schema_invalid_missing_required(self):
|
|
190
|
-
"""Request with missing required field is rejected"""
|
|
191
|
-
payload = {
|
|
192
|
-
"field1": "test value"
|
|
193
|
-
# Missing field2
|
|
194
|
-
}
|
|
195
|
-
with pytest.raises(ValidationError):
|
|
196
|
-
validate(instance=payload, schema=REQUEST_SCHEMA)
|
|
197
|
-
|
|
198
|
-
def test_response_schema_success(self):
|
|
199
|
-
"""Response body matches contract schema (201 Created)"""
|
|
200
|
-
payload = {
|
|
201
|
-
"field1": "test value",
|
|
202
|
-
"field2": 42,
|
|
203
|
-
"field3": True
|
|
204
|
-
}
|
|
205
|
-
response = client.post(
|
|
206
|
-
"/api/v1/resource",
|
|
207
|
-
json=payload,
|
|
208
|
-
headers={"Authorization": "Bearer test-token"}
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
# Status code contract
|
|
212
|
-
assert response.status_code == 201
|
|
213
|
-
|
|
214
|
-
# Response schema contract
|
|
215
|
-
response_data = response.json()
|
|
216
|
-
validate(instance=response_data, schema=RESPONSE_SCHEMA)
|
|
217
|
-
|
|
218
|
-
# Data contract
|
|
219
|
-
assert response_data["field1"] == payload["field1"]
|
|
220
|
-
assert response_data["field2"] == payload["field2"]
|
|
221
|
-
assert response_data["field3"] == payload["field3"]
|
|
222
|
-
|
|
223
|
-
def test_response_schema_error(self):
|
|
224
|
-
"""Error response matches contract schema (400 Bad Request)"""
|
|
225
|
-
payload = {
|
|
226
|
-
"field1": "", # Invalid: empty string
|
|
227
|
-
"field2": -1 # Invalid: negative
|
|
228
|
-
}
|
|
229
|
-
response = client.post(
|
|
230
|
-
"/api/v1/resource",
|
|
231
|
-
json=payload,
|
|
232
|
-
headers={"Authorization": "Bearer test-token"}
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
# Status code contract
|
|
236
|
-
assert response.status_code == 400
|
|
237
|
-
|
|
238
|
-
# Error schema contract
|
|
239
|
-
error_data = response.json()
|
|
240
|
-
assert "error" in error_data
|
|
241
|
-
assert "message" in error_data
|
|
242
|
-
assert isinstance(error_data["message"], str)
|
|
243
|
-
|
|
244
|
-
def test_headers_contract(self):
|
|
245
|
-
"""Response headers match contract"""
|
|
246
|
-
payload = {
|
|
247
|
-
"field1": "test value",
|
|
248
|
-
"field2": 42
|
|
249
|
-
}
|
|
250
|
-
response = client.post(
|
|
251
|
-
"/api/v1/resource",
|
|
252
|
-
json=payload,
|
|
253
|
-
headers={"Authorization": "Bearer test-token"}
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
assert response.headers["Content-Type"] == "application/json"
|
|
257
|
-
|
|
258
|
-
@pytest.mark.parametrize("invalid_payload,expected_error", [
|
|
259
|
-
({"field1": "x" * 101, "field2": 42}, "field1 too long"),
|
|
260
|
-
({"field1": "test", "field2": -1}, "field2 must be positive"),
|
|
261
|
-
({"field2": 42}, "field1 is required"),
|
|
262
|
-
])
|
|
263
|
-
def test_validation_errors(self, invalid_payload, expected_error):
|
|
264
|
-
"""Contract validation errors are properly handled"""
|
|
265
|
-
response = client.post(
|
|
266
|
-
"/api/v1/resource",
|
|
267
|
-
json=invalid_payload,
|
|
268
|
-
headers={"Authorization": "Bearer test-token"}
|
|
269
|
-
)
|
|
270
|
-
assert response.status_code == 400
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
**Run**:
|
|
274
|
-
|
|
275
|
-
```bash
|
|
276
|
-
pytest tests/contract/test_{feature_name}_contract.py -v --tb=short
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
---
|
|
280
|
-
|
|
281
|
-
### Python (Pact - Consumer-Driven Contracts)
|
|
282
|
-
|
|
283
|
-
**File**: `tests/pact/consumer_test_{feature_name}.py`
|
|
284
|
-
|
|
285
|
-
```python
|
|
286
|
-
import pytest
|
|
287
|
-
from pact import Consumer, Provider, Like, EachLike, Format
|
|
288
|
-
|
|
289
|
-
pact = Consumer('FrontendApp').has_pact_with(Provider('BackendAPI'))
|
|
290
|
-
|
|
291
|
-
@pytest.fixture(scope='module')
|
|
292
|
-
def setup_pact():
|
|
293
|
-
pact.start_service()
|
|
294
|
-
yield
|
|
295
|
-
pact.stop_service()
|
|
296
|
-
|
|
297
|
-
def test_create_resource_contract(setup_pact):
|
|
298
|
-
"""Consumer expects provider to create resource"""
|
|
299
|
-
expected = {
|
|
300
|
-
'id': Format().uuid,
|
|
301
|
-
'field1': Like('test value'),
|
|
302
|
-
'field2': Like(42),
|
|
303
|
-
'field3': Like(True),
|
|
304
|
-
'created_at': Format().iso_8601_datetime
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
(pact
|
|
308
|
-
.given('user is authenticated')
|
|
309
|
-
.upon_receiving('a request to create resource')
|
|
310
|
-
.with_request('POST', '/api/v1/resource',
|
|
311
|
-
headers={'Authorization': Like('Bearer token')},
|
|
312
|
-
body={
|
|
313
|
-
'field1': 'test value',
|
|
314
|
-
'field2': 42,
|
|
315
|
-
'field3': True
|
|
316
|
-
})
|
|
317
|
-
.will_respond_with(201, body=expected))
|
|
318
|
-
|
|
319
|
-
with pact:
|
|
320
|
-
# Test consumer code
|
|
321
|
-
result = api_client.create_resource(field1='test value', field2=42)
|
|
322
|
-
assert result['id'] is not None
|
|
323
|
-
assert result['field1'] == 'test value'
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
**Generate Pact file**:
|
|
327
|
-
|
|
328
|
-
```bash
|
|
329
|
-
pytest tests/pact/ --pact-broker-url=https://your-pact-broker.com
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
### TypeScript (NestJS + Jest)
|
|
335
|
-
|
|
336
|
-
**File**: `test/contract/{feature-name}.contract.spec.ts`
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
import { Test } from '@nestjs/testing';
|
|
340
|
-
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
341
|
-
import * as request from 'supertest';
|
|
342
|
-
import { AppModule } from '../src/app.module';
|
|
343
|
-
import Ajv from 'ajv';
|
|
344
|
-
import addFormats from 'ajv-formats';
|
|
345
|
-
|
|
346
|
-
describe('Create Resource Contract (e2e)', () => {
|
|
347
|
-
let app: INestApplication;
|
|
348
|
-
const ajv = new Ajv();
|
|
349
|
-
addFormats(ajv);
|
|
350
|
-
|
|
351
|
-
const requestSchema = {
|
|
352
|
-
type: 'object',
|
|
353
|
-
required: ['field1', 'field2'],
|
|
354
|
-
properties: {
|
|
355
|
-
field1: { type: 'string', minLength: 1, maxLength: 100 },
|
|
356
|
-
field2: { type: 'integer', minimum: 0 },
|
|
357
|
-
field3: { type: 'boolean' }
|
|
358
|
-
},
|
|
359
|
-
additionalProperties: false
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
const responseSchema = {
|
|
363
|
-
type: 'object',
|
|
364
|
-
required: ['id', 'field1', 'field2', 'createdAt'],
|
|
365
|
-
properties: {
|
|
366
|
-
id: { type: 'string', format: 'uuid' },
|
|
367
|
-
field1: { type: 'string' },
|
|
368
|
-
field2: { type: 'integer' },
|
|
369
|
-
field3: { type: 'boolean' },
|
|
370
|
-
createdAt: { type: 'string', format: 'date-time' }
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
beforeAll(async () => {
|
|
375
|
-
const moduleFixture = await Test.createTestingModule({
|
|
376
|
-
imports: [AppModule],
|
|
377
|
-
}).compile();
|
|
378
|
-
|
|
379
|
-
app = moduleFixture.createNestApplication();
|
|
380
|
-
app.useGlobalPipes(new ValidationPipe());
|
|
381
|
-
await app.init();
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
afterAll(async () => {
|
|
385
|
-
await app.close();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('POST /api/v1/resource - validates request schema', () => {
|
|
389
|
-
const payload = {
|
|
390
|
-
field1: 'test value',
|
|
391
|
-
field2: 42,
|
|
392
|
-
field3: true
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
const validate = ajv.compile(requestSchema);
|
|
396
|
-
expect(validate(payload)).toBe(true);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('POST /api/v1/resource - validates response schema (201)', async () => {
|
|
400
|
-
const response = await request(app.getHttpServer())
|
|
401
|
-
.post('/api/v1/resource')
|
|
402
|
-
.set('Authorization', 'Bearer test-token')
|
|
403
|
-
.send({
|
|
404
|
-
field1: 'test value',
|
|
405
|
-
field2: 42,
|
|
406
|
-
field3: true
|
|
407
|
-
})
|
|
408
|
-
.expect(201)
|
|
409
|
-
.expect('Content-Type', /json/);
|
|
410
|
-
|
|
411
|
-
const validate = ajv.compile(responseSchema);
|
|
412
|
-
expect(validate(response.body)).toBe(true);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('POST /api/v1/resource - returns 400 for invalid request', async () => {
|
|
416
|
-
await request(app.getHttpServer())
|
|
417
|
-
.post('/api/v1/resource')
|
|
418
|
-
.set('Authorization', 'Bearer test-token')
|
|
419
|
-
.send({
|
|
420
|
-
field1: '', // Invalid
|
|
421
|
-
field2: -1 // Invalid
|
|
422
|
-
})
|
|
423
|
-
.expect(400);
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
**Run**:
|
|
429
|
-
|
|
430
|
-
```bash
|
|
431
|
-
npm test -- test/contract/{feature-name}.contract.spec.ts
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
---
|
|
435
|
-
|
|
436
|
-
## Contract Testing Strategy
|
|
437
|
-
|
|
438
|
-
### 1. Provider Tests (Backend)
|
|
439
|
-
|
|
440
|
-
```bash
|
|
441
|
-
# Run all contract tests
|
|
442
|
-
pytest tests/contract/ -v
|
|
443
|
-
|
|
444
|
-
# Run specific contract
|
|
445
|
-
pytest tests/contract/test_{feature_name}_contract.py
|
|
446
|
-
|
|
447
|
-
# Generate Pact file for consumer
|
|
448
|
-
pytest tests/pact/ --pact-broker-url=...
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### 2. Consumer Tests (Frontend)
|
|
452
|
-
|
|
453
|
-
```bash
|
|
454
|
-
# Verify against provider contract
|
|
455
|
-
npm run test:contract -- --pact-broker-url=...
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
### 3. CI/CD Integration
|
|
459
|
-
|
|
460
|
-
```yaml
|
|
461
|
-
# .github/workflows/contract-tests.yml
|
|
462
|
-
name: Contract Tests
|
|
463
|
-
|
|
464
|
-
on: [pull_request]
|
|
465
|
-
|
|
466
|
-
jobs:
|
|
467
|
-
contract-tests:
|
|
468
|
-
runs-on: ubuntu-latest
|
|
469
|
-
steps:
|
|
470
|
-
- uses: actions/checkout@v2
|
|
471
|
-
- name: Run provider contract tests
|
|
472
|
-
run: pytest tests/contract/ -v
|
|
473
|
-
- name: Publish Pact
|
|
474
|
-
run: pytest tests/pact/ --pact-broker-url=${{ secrets.PACT_BROKER_URL }}
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
---
|
|
478
|
-
|
|
479
|
-
## Coverage Mapping
|
|
480
|
-
|
|
481
|
-
| Contract | SPEC REQ | Endpoints | Status |
|
|
482
|
-
|----------|----------|-----------|--------|
|
|
483
|
-
| Create Resource | REQ-001 | POST /api/v1/resource | ⬜ |
|
|
484
|
-
| Get Resource | REQ-002 | GET /api/v1/resource/:id | ⬜ |
|
|
485
|
-
| Update Resource | REQ-003 | PATCH /api/v1/resource/:id | ⬜ |
|
|
486
|
-
|
|
487
|
-
**Coverage**: 0 / {Total contracts} (0%)
|
|
488
|
-
|
|
489
|
-
---
|
|
490
|
-
|
|
491
|
-
## Best Practices
|
|
492
|
-
|
|
493
|
-
1. **Test Contract, Not Implementation**
|
|
494
|
-
- ✅ Verify schema compliance
|
|
495
|
-
- ❌ Do not test business logic
|
|
496
|
-
|
|
497
|
-
2. **Provider-First vs Consumer-First**
|
|
498
|
-
- Provider-First: Define API first → Write contract tests
|
|
499
|
-
- Consumer-First: Frontend requirements → Write Pact → Implement provider
|
|
500
|
-
|
|
501
|
-
3. **Version Control**
|
|
502
|
-
- Manage contract files per API version
|
|
503
|
-
- Detect breaking changes
|
|
504
|
-
|
|
505
|
-
4. **Pact Broker Usage**
|
|
506
|
-
- Central contract management
|
|
507
|
-
- Consumer-Provider matching
|
|
508
|
-
- CI/CD automation
|
|
509
|
-
|
|
510
|
-
---
|
|
511
|
-
|
|
512
|
-
## Next Steps
|
|
513
|
-
|
|
514
|
-
```bash
|
|
515
|
-
# 1. Write contract tests
|
|
516
|
-
vibe contract "{feature name}"
|
|
517
|
-
|
|
518
|
-
# 2. Implement provider
|
|
519
|
-
vibe run "Task 1-1"
|
|
520
|
-
|
|
521
|
-
# 3. Verify contract
|
|
522
|
-
vibe test "{feature name}" --contract
|
|
523
|
-
|
|
524
|
-
# 4. Publish Pact (optional)
|
|
525
|
-
pytest tests/pact/ --pact-broker-url=...
|
|
526
|
-
```
|
|
1
|
+
# Backend Contract Tests: {Feature Name}
|
|
2
|
+
|
|
3
|
+
**Generated from**: `specs/{feature-name}.md` (Section 6: API Contract)
|
|
4
|
+
**Framework**: {FastAPI | Django | Express | NestJS}
|
|
5
|
+
**Language**: {Python | TypeScript | JavaScript}
|
|
6
|
+
**Priority**: {HIGH | MEDIUM | LOW}
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Contract Testing **validates API contracts (schemas)**:
|
|
13
|
+
|
|
14
|
+
- ✅ Request/Response schema compliance
|
|
15
|
+
- ✅ Status code matching
|
|
16
|
+
- ✅ Header validation
|
|
17
|
+
- ✅ Data types and required fields verification
|
|
18
|
+
|
|
19
|
+
**Consumer → Provider contract assurance** (Pact pattern)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## API Contracts
|
|
24
|
+
|
|
25
|
+
### Contract 1: {Endpoint Name}
|
|
26
|
+
|
|
27
|
+
**Endpoint**: `POST /api/v1/{resource}`
|
|
28
|
+
**Mapped to**: REQ-001 in SPEC
|
|
29
|
+
|
|
30
|
+
#### Request Contract
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"method": "POST",
|
|
35
|
+
"path": "/api/v1/{resource}",
|
|
36
|
+
"headers": {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Authorization": "Bearer {token}"
|
|
39
|
+
},
|
|
40
|
+
"body": {
|
|
41
|
+
"field1": "string (required)",
|
|
42
|
+
"field2": "integer (required)",
|
|
43
|
+
"field3": "boolean (optional)"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**JSON Schema**:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
53
|
+
"type": "object",
|
|
54
|
+
"required": ["field1", "field2"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"field1": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"minLength": 1,
|
|
59
|
+
"maxLength": 100
|
|
60
|
+
},
|
|
61
|
+
"field2": {
|
|
62
|
+
"type": "integer",
|
|
63
|
+
"minimum": 0
|
|
64
|
+
},
|
|
65
|
+
"field3": {
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"default": false
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"additionalProperties": false
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Response Contract (Success)
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"status": 201,
|
|
79
|
+
"headers": {
|
|
80
|
+
"Content-Type": "application/json"
|
|
81
|
+
},
|
|
82
|
+
"body": {
|
|
83
|
+
"id": "uuid",
|
|
84
|
+
"field1": "string",
|
|
85
|
+
"field2": "integer",
|
|
86
|
+
"field3": "boolean",
|
|
87
|
+
"created_at": "datetime (ISO 8601)"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**JSON Schema**:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
97
|
+
"type": "object",
|
|
98
|
+
"required": ["id", "field1", "field2", "created_at"],
|
|
99
|
+
"properties": {
|
|
100
|
+
"id": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"format": "uuid"
|
|
103
|
+
},
|
|
104
|
+
"field1": {
|
|
105
|
+
"type": "string"
|
|
106
|
+
},
|
|
107
|
+
"field2": {
|
|
108
|
+
"type": "integer"
|
|
109
|
+
},
|
|
110
|
+
"field3": {
|
|
111
|
+
"type": "boolean"
|
|
112
|
+
},
|
|
113
|
+
"created_at": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"format": "date-time"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Response Contract (Error)
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"status": 400,
|
|
126
|
+
"body": {
|
|
127
|
+
"error": "string",
|
|
128
|
+
"message": "string",
|
|
129
|
+
"details": ["array of strings (optional)"]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Implementation
|
|
137
|
+
|
|
138
|
+
### Python (FastAPI + Pydantic)
|
|
139
|
+
|
|
140
|
+
**File**: `tests/contract/test_{feature_name}_contract.py`
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import pytest
|
|
144
|
+
from fastapi.testclient import TestClient
|
|
145
|
+
from jsonschema import validate, ValidationError
|
|
146
|
+
from app.main import app
|
|
147
|
+
|
|
148
|
+
client = TestClient(app)
|
|
149
|
+
|
|
150
|
+
# JSON Schema definitions
|
|
151
|
+
REQUEST_SCHEMA = {
|
|
152
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
153
|
+
"type": "object",
|
|
154
|
+
"required": ["field1", "field2"],
|
|
155
|
+
"properties": {
|
|
156
|
+
"field1": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
157
|
+
"field2": {"type": "integer", "minimum": 0},
|
|
158
|
+
"field3": {"type": "boolean", "default": False}
|
|
159
|
+
},
|
|
160
|
+
"additionalProperties": False
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
RESPONSE_SCHEMA = {
|
|
164
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
165
|
+
"type": "object",
|
|
166
|
+
"required": ["id", "field1", "field2", "created_at"],
|
|
167
|
+
"properties": {
|
|
168
|
+
"id": {"type": "string", "format": "uuid"},
|
|
169
|
+
"field1": {"type": "string"},
|
|
170
|
+
"field2": {"type": "integer"},
|
|
171
|
+
"field3": {"type": "boolean"},
|
|
172
|
+
"created_at": {"type": "string", "format": "date-time"}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class TestCreateResourceContract:
|
|
177
|
+
"""Contract tests for POST /api/v1/resource"""
|
|
178
|
+
|
|
179
|
+
def test_request_schema_valid(self):
|
|
180
|
+
"""Request body matches contract schema"""
|
|
181
|
+
payload = {
|
|
182
|
+
"field1": "test value",
|
|
183
|
+
"field2": 42,
|
|
184
|
+
"field3": True
|
|
185
|
+
}
|
|
186
|
+
# Should not raise ValidationError
|
|
187
|
+
validate(instance=payload, schema=REQUEST_SCHEMA)
|
|
188
|
+
|
|
189
|
+
def test_request_schema_invalid_missing_required(self):
|
|
190
|
+
"""Request with missing required field is rejected"""
|
|
191
|
+
payload = {
|
|
192
|
+
"field1": "test value"
|
|
193
|
+
# Missing field2
|
|
194
|
+
}
|
|
195
|
+
with pytest.raises(ValidationError):
|
|
196
|
+
validate(instance=payload, schema=REQUEST_SCHEMA)
|
|
197
|
+
|
|
198
|
+
def test_response_schema_success(self):
|
|
199
|
+
"""Response body matches contract schema (201 Created)"""
|
|
200
|
+
payload = {
|
|
201
|
+
"field1": "test value",
|
|
202
|
+
"field2": 42,
|
|
203
|
+
"field3": True
|
|
204
|
+
}
|
|
205
|
+
response = client.post(
|
|
206
|
+
"/api/v1/resource",
|
|
207
|
+
json=payload,
|
|
208
|
+
headers={"Authorization": "Bearer test-token"}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Status code contract
|
|
212
|
+
assert response.status_code == 201
|
|
213
|
+
|
|
214
|
+
# Response schema contract
|
|
215
|
+
response_data = response.json()
|
|
216
|
+
validate(instance=response_data, schema=RESPONSE_SCHEMA)
|
|
217
|
+
|
|
218
|
+
# Data contract
|
|
219
|
+
assert response_data["field1"] == payload["field1"]
|
|
220
|
+
assert response_data["field2"] == payload["field2"]
|
|
221
|
+
assert response_data["field3"] == payload["field3"]
|
|
222
|
+
|
|
223
|
+
def test_response_schema_error(self):
|
|
224
|
+
"""Error response matches contract schema (400 Bad Request)"""
|
|
225
|
+
payload = {
|
|
226
|
+
"field1": "", # Invalid: empty string
|
|
227
|
+
"field2": -1 # Invalid: negative
|
|
228
|
+
}
|
|
229
|
+
response = client.post(
|
|
230
|
+
"/api/v1/resource",
|
|
231
|
+
json=payload,
|
|
232
|
+
headers={"Authorization": "Bearer test-token"}
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Status code contract
|
|
236
|
+
assert response.status_code == 400
|
|
237
|
+
|
|
238
|
+
# Error schema contract
|
|
239
|
+
error_data = response.json()
|
|
240
|
+
assert "error" in error_data
|
|
241
|
+
assert "message" in error_data
|
|
242
|
+
assert isinstance(error_data["message"], str)
|
|
243
|
+
|
|
244
|
+
def test_headers_contract(self):
|
|
245
|
+
"""Response headers match contract"""
|
|
246
|
+
payload = {
|
|
247
|
+
"field1": "test value",
|
|
248
|
+
"field2": 42
|
|
249
|
+
}
|
|
250
|
+
response = client.post(
|
|
251
|
+
"/api/v1/resource",
|
|
252
|
+
json=payload,
|
|
253
|
+
headers={"Authorization": "Bearer test-token"}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
assert response.headers["Content-Type"] == "application/json"
|
|
257
|
+
|
|
258
|
+
@pytest.mark.parametrize("invalid_payload,expected_error", [
|
|
259
|
+
({"field1": "x" * 101, "field2": 42}, "field1 too long"),
|
|
260
|
+
({"field1": "test", "field2": -1}, "field2 must be positive"),
|
|
261
|
+
({"field2": 42}, "field1 is required"),
|
|
262
|
+
])
|
|
263
|
+
def test_validation_errors(self, invalid_payload, expected_error):
|
|
264
|
+
"""Contract validation errors are properly handled"""
|
|
265
|
+
response = client.post(
|
|
266
|
+
"/api/v1/resource",
|
|
267
|
+
json=invalid_payload,
|
|
268
|
+
headers={"Authorization": "Bearer test-token"}
|
|
269
|
+
)
|
|
270
|
+
assert response.status_code == 400
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Run**:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
pytest tests/contract/test_{feature_name}_contract.py -v --tb=short
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### Python (Pact - Consumer-Driven Contracts)
|
|
282
|
+
|
|
283
|
+
**File**: `tests/pact/consumer_test_{feature_name}.py`
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
import pytest
|
|
287
|
+
from pact import Consumer, Provider, Like, EachLike, Format
|
|
288
|
+
|
|
289
|
+
pact = Consumer('FrontendApp').has_pact_with(Provider('BackendAPI'))
|
|
290
|
+
|
|
291
|
+
@pytest.fixture(scope='module')
|
|
292
|
+
def setup_pact():
|
|
293
|
+
pact.start_service()
|
|
294
|
+
yield
|
|
295
|
+
pact.stop_service()
|
|
296
|
+
|
|
297
|
+
def test_create_resource_contract(setup_pact):
|
|
298
|
+
"""Consumer expects provider to create resource"""
|
|
299
|
+
expected = {
|
|
300
|
+
'id': Format().uuid,
|
|
301
|
+
'field1': Like('test value'),
|
|
302
|
+
'field2': Like(42),
|
|
303
|
+
'field3': Like(True),
|
|
304
|
+
'created_at': Format().iso_8601_datetime
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
(pact
|
|
308
|
+
.given('user is authenticated')
|
|
309
|
+
.upon_receiving('a request to create resource')
|
|
310
|
+
.with_request('POST', '/api/v1/resource',
|
|
311
|
+
headers={'Authorization': Like('Bearer token')},
|
|
312
|
+
body={
|
|
313
|
+
'field1': 'test value',
|
|
314
|
+
'field2': 42,
|
|
315
|
+
'field3': True
|
|
316
|
+
})
|
|
317
|
+
.will_respond_with(201, body=expected))
|
|
318
|
+
|
|
319
|
+
with pact:
|
|
320
|
+
# Test consumer code
|
|
321
|
+
result = api_client.create_resource(field1='test value', field2=42)
|
|
322
|
+
assert result['id'] is not None
|
|
323
|
+
assert result['field1'] == 'test value'
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Generate Pact file**:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
pytest tests/pact/ --pact-broker-url=https://your-pact-broker.com
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### TypeScript (NestJS + Jest)
|
|
335
|
+
|
|
336
|
+
**File**: `test/contract/{feature-name}.contract.spec.ts`
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { Test } from '@nestjs/testing';
|
|
340
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
341
|
+
import * as request from 'supertest';
|
|
342
|
+
import { AppModule } from '../src/app.module';
|
|
343
|
+
import Ajv from 'ajv';
|
|
344
|
+
import addFormats from 'ajv-formats';
|
|
345
|
+
|
|
346
|
+
describe('Create Resource Contract (e2e)', () => {
|
|
347
|
+
let app: INestApplication;
|
|
348
|
+
const ajv = new Ajv();
|
|
349
|
+
addFormats(ajv);
|
|
350
|
+
|
|
351
|
+
const requestSchema = {
|
|
352
|
+
type: 'object',
|
|
353
|
+
required: ['field1', 'field2'],
|
|
354
|
+
properties: {
|
|
355
|
+
field1: { type: 'string', minLength: 1, maxLength: 100 },
|
|
356
|
+
field2: { type: 'integer', minimum: 0 },
|
|
357
|
+
field3: { type: 'boolean' }
|
|
358
|
+
},
|
|
359
|
+
additionalProperties: false
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const responseSchema = {
|
|
363
|
+
type: 'object',
|
|
364
|
+
required: ['id', 'field1', 'field2', 'createdAt'],
|
|
365
|
+
properties: {
|
|
366
|
+
id: { type: 'string', format: 'uuid' },
|
|
367
|
+
field1: { type: 'string' },
|
|
368
|
+
field2: { type: 'integer' },
|
|
369
|
+
field3: { type: 'boolean' },
|
|
370
|
+
createdAt: { type: 'string', format: 'date-time' }
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
beforeAll(async () => {
|
|
375
|
+
const moduleFixture = await Test.createTestingModule({
|
|
376
|
+
imports: [AppModule],
|
|
377
|
+
}).compile();
|
|
378
|
+
|
|
379
|
+
app = moduleFixture.createNestApplication();
|
|
380
|
+
app.useGlobalPipes(new ValidationPipe());
|
|
381
|
+
await app.init();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
afterAll(async () => {
|
|
385
|
+
await app.close();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('POST /api/v1/resource - validates request schema', () => {
|
|
389
|
+
const payload = {
|
|
390
|
+
field1: 'test value',
|
|
391
|
+
field2: 42,
|
|
392
|
+
field3: true
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const validate = ajv.compile(requestSchema);
|
|
396
|
+
expect(validate(payload)).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('POST /api/v1/resource - validates response schema (201)', async () => {
|
|
400
|
+
const response = await request(app.getHttpServer())
|
|
401
|
+
.post('/api/v1/resource')
|
|
402
|
+
.set('Authorization', 'Bearer test-token')
|
|
403
|
+
.send({
|
|
404
|
+
field1: 'test value',
|
|
405
|
+
field2: 42,
|
|
406
|
+
field3: true
|
|
407
|
+
})
|
|
408
|
+
.expect(201)
|
|
409
|
+
.expect('Content-Type', /json/);
|
|
410
|
+
|
|
411
|
+
const validate = ajv.compile(responseSchema);
|
|
412
|
+
expect(validate(response.body)).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('POST /api/v1/resource - returns 400 for invalid request', async () => {
|
|
416
|
+
await request(app.getHttpServer())
|
|
417
|
+
.post('/api/v1/resource')
|
|
418
|
+
.set('Authorization', 'Bearer test-token')
|
|
419
|
+
.send({
|
|
420
|
+
field1: '', // Invalid
|
|
421
|
+
field2: -1 // Invalid
|
|
422
|
+
})
|
|
423
|
+
.expect(400);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Run**:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
npm test -- test/contract/{feature-name}.contract.spec.ts
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Contract Testing Strategy
|
|
437
|
+
|
|
438
|
+
### 1. Provider Tests (Backend)
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
# Run all contract tests
|
|
442
|
+
pytest tests/contract/ -v
|
|
443
|
+
|
|
444
|
+
# Run specific contract
|
|
445
|
+
pytest tests/contract/test_{feature_name}_contract.py
|
|
446
|
+
|
|
447
|
+
# Generate Pact file for consumer
|
|
448
|
+
pytest tests/pact/ --pact-broker-url=...
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### 2. Consumer Tests (Frontend)
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
# Verify against provider contract
|
|
455
|
+
npm run test:contract -- --pact-broker-url=...
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### 3. CI/CD Integration
|
|
459
|
+
|
|
460
|
+
```yaml
|
|
461
|
+
# .github/workflows/contract-tests.yml
|
|
462
|
+
name: Contract Tests
|
|
463
|
+
|
|
464
|
+
on: [pull_request]
|
|
465
|
+
|
|
466
|
+
jobs:
|
|
467
|
+
contract-tests:
|
|
468
|
+
runs-on: ubuntu-latest
|
|
469
|
+
steps:
|
|
470
|
+
- uses: actions/checkout@v2
|
|
471
|
+
- name: Run provider contract tests
|
|
472
|
+
run: pytest tests/contract/ -v
|
|
473
|
+
- name: Publish Pact
|
|
474
|
+
run: pytest tests/pact/ --pact-broker-url=${{ secrets.PACT_BROKER_URL }}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Coverage Mapping
|
|
480
|
+
|
|
481
|
+
| Contract | SPEC REQ | Endpoints | Status |
|
|
482
|
+
|----------|----------|-----------|--------|
|
|
483
|
+
| Create Resource | REQ-001 | POST /api/v1/resource | ⬜ |
|
|
484
|
+
| Get Resource | REQ-002 | GET /api/v1/resource/:id | ⬜ |
|
|
485
|
+
| Update Resource | REQ-003 | PATCH /api/v1/resource/:id | ⬜ |
|
|
486
|
+
|
|
487
|
+
**Coverage**: 0 / {Total contracts} (0%)
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Best Practices
|
|
492
|
+
|
|
493
|
+
1. **Test Contract, Not Implementation**
|
|
494
|
+
- ✅ Verify schema compliance
|
|
495
|
+
- ❌ Do not test business logic
|
|
496
|
+
|
|
497
|
+
2. **Provider-First vs Consumer-First**
|
|
498
|
+
- Provider-First: Define API first → Write contract tests
|
|
499
|
+
- Consumer-First: Frontend requirements → Write Pact → Implement provider
|
|
500
|
+
|
|
501
|
+
3. **Version Control**
|
|
502
|
+
- Manage contract files per API version
|
|
503
|
+
- Detect breaking changes
|
|
504
|
+
|
|
505
|
+
4. **Pact Broker Usage**
|
|
506
|
+
- Central contract management
|
|
507
|
+
- Consumer-Provider matching
|
|
508
|
+
- CI/CD automation
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Next Steps
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
# 1. Write contract tests
|
|
516
|
+
vibe contract "{feature name}"
|
|
517
|
+
|
|
518
|
+
# 2. Implement provider
|
|
519
|
+
vibe run "Task 1-1"
|
|
520
|
+
|
|
521
|
+
# 3. Verify contract
|
|
522
|
+
vibe test "{feature name}" --contract
|
|
523
|
+
|
|
524
|
+
# 4. Publish Pact (optional)
|
|
525
|
+
pytest tests/pact/ --pact-broker-url=...
|
|
526
|
+
```
|