@su-record/vibe 2.7.12 → 2.7.13
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 +126 -126
- package/LICENSE +21 -21
- package/README.md +449 -580
- 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 -266
- 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 +260 -260
- package/commands/vibe.analyze.md +8 -0
- package/commands/vibe.review.md +10 -3
- package/commands/vibe.run.md +2078 -2022
- package/commands/vibe.spec.md +10 -10
- package/commands/vibe.spec.review.md +565 -558
- package/commands/vibe.utils.md +413 -413
- package/commands/vibe.verify.md +45 -0
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +1 -7
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/collaborator.js +52 -52
- package/dist/cli/commands/evolution.js +12 -12
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +55 -70
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +6 -7
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/remove.js +14 -14
- package/dist/cli/commands/sentinel.js +27 -27
- package/dist/cli/commands/setup.js +1 -1
- package/dist/cli/commands/setup.js.map +1 -1
- 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/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +3 -4
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/detect.js +32 -32
- package/dist/cli/index.js +51 -55
- package/dist/cli/index.js.map +1 -1
- 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.d.ts +4 -6
- package/dist/cli/llm/gemini-commands.d.ts.map +1 -1
- package/dist/cli/llm/gemini-commands.js +52 -322
- package/dist/cli/llm/gemini-commands.js.map +1 -1
- package/dist/cli/llm/gpt-commands.js +21 -21
- package/dist/cli/llm/gpt-commands.js.map +1 -1
- package/dist/cli/llm/help.js +21 -21
- package/dist/cli/postinstall/constants.js +1 -1
- package/dist/cli/postinstall/constants.js.map +1 -1
- 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/postinstall/inline-skills.js +1 -1
- package/dist/cli/postinstall/inline-skills.js.map +1 -1
- package/dist/cli/setup/Provisioner.js +42 -42
- package/dist/cli/types.d.ts +2 -16
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/utils.d.ts +0 -9
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +0 -28
- package/dist/cli/utils.js.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/config/GlobalConfigManager.d.ts +0 -2
- package/dist/infra/lib/config/GlobalConfigManager.d.ts.map +1 -1
- package/dist/infra/lib/config/GlobalConfigManager.js +0 -27
- package/dist/infra/lib/config/GlobalConfigManager.js.map +1 -1
- 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/auth.d.ts +4 -16
- package/dist/infra/lib/gemini/auth.d.ts.map +1 -1
- package/dist/infra/lib/gemini/auth.js +10 -405
- package/dist/infra/lib/gemini/auth.js.map +1 -1
- package/dist/infra/lib/gemini/capabilities.d.ts +4 -8
- package/dist/infra/lib/gemini/capabilities.d.ts.map +1 -1
- package/dist/infra/lib/gemini/capabilities.js +8 -166
- package/dist/infra/lib/gemini/capabilities.js.map +1 -1
- package/dist/infra/lib/gemini/chat.d.ts +4 -13
- package/dist/infra/lib/gemini/chat.d.ts.map +1 -1
- package/dist/infra/lib/gemini/chat.js +10 -323
- package/dist/infra/lib/gemini/chat.js.map +1 -1
- package/dist/infra/lib/gemini/completion.d.ts +5 -15
- package/dist/infra/lib/gemini/completion.d.ts.map +1 -1
- package/dist/infra/lib/gemini/completion.js +6 -97
- package/dist/infra/lib/gemini/completion.js.map +1 -1
- package/dist/infra/lib/gemini/constants.d.ts +2 -31
- package/dist/infra/lib/gemini/constants.d.ts.map +1 -1
- package/dist/infra/lib/gemini/constants.js +2 -77
- package/dist/infra/lib/gemini/constants.js.map +1 -1
- package/dist/infra/lib/gemini/index.d.ts +5 -8
- package/dist/infra/lib/gemini/index.d.ts.map +1 -1
- package/dist/infra/lib/gemini/index.js +4 -7
- package/dist/infra/lib/gemini/index.js.map +1 -1
- package/dist/infra/lib/gemini/models.d.ts +3 -4
- package/dist/infra/lib/gemini/models.d.ts.map +1 -1
- package/dist/infra/lib/gemini/models.js +8 -84
- package/dist/infra/lib/gemini/models.js.map +1 -1
- package/dist/infra/lib/gemini/orchestration.js +5 -5
- package/dist/infra/lib/gemini/types.d.ts +16 -44
- package/dist/infra/lib/gemini/types.d.ts.map +1 -1
- package/dist/infra/lib/gemini/types.js +1 -1
- package/dist/infra/lib/gpt/auth.d.ts +2 -5
- package/dist/infra/lib/gpt/auth.d.ts.map +1 -1
- package/dist/infra/lib/gpt/auth.js +8 -38
- package/dist/infra/lib/gpt/auth.js.map +1 -1
- package/dist/infra/lib/gpt/chat.d.ts +3 -3
- package/dist/infra/lib/gpt/chat.d.ts.map +1 -1
- package/dist/infra/lib/gpt/chat.js +37 -53
- package/dist/infra/lib/gpt/chat.js.map +1 -1
- package/dist/infra/lib/gpt/constants.d.ts +2 -5
- package/dist/infra/lib/gpt/constants.d.ts.map +1 -1
- package/dist/infra/lib/gpt/constants.js +4 -9
- package/dist/infra/lib/gpt/constants.js.map +1 -1
- package/dist/infra/lib/gpt/embedding.d.ts +1 -1
- package/dist/infra/lib/gpt/embedding.js +3 -3
- package/dist/infra/lib/gpt/embedding.js.map +1 -1
- package/dist/infra/lib/gpt/oauth.d.ts +6 -39
- package/dist/infra/lib/gpt/oauth.d.ts.map +1 -1
- package/dist/infra/lib/gpt/oauth.js +8 -340
- package/dist/infra/lib/gpt/oauth.js.map +1 -1
- package/dist/infra/lib/gpt/orchestration.js +5 -5
- package/dist/infra/lib/gpt/orchestration.js.map +1 -1
- package/dist/infra/lib/gpt/specializations.d.ts +2 -2
- package/dist/infra/lib/gpt/specializations.js +3 -3
- package/dist/infra/lib/gpt/specializations.js.map +1 -1
- package/dist/infra/lib/gpt/types.d.ts +1 -1
- package/dist/infra/lib/gpt/types.d.ts.map +1 -1
- package/dist/infra/lib/llm/auth/AuthProfileManager.d.ts +2 -2
- package/dist/infra/lib/llm/auth/AuthProfileManager.d.ts.map +1 -1
- package/dist/infra/lib/llm/auth/AuthProfileManager.js.map +1 -1
- package/dist/infra/lib/llm/auth/AuthProfileManager.test.js +1 -1
- package/dist/infra/lib/llm/auth/AuthProfileManager.test.js.map +1 -1
- package/dist/infra/lib/llm/auth/TokenRefresher.d.ts +1 -1
- package/dist/infra/lib/llm/auth/TokenRefresher.js +1 -1
- package/dist/infra/lib/llm/auth/index.d.ts +2 -12
- package/dist/infra/lib/llm/auth/index.d.ts.map +1 -1
- package/dist/infra/lib/llm/auth/index.js +5 -63
- package/dist/infra/lib/llm/auth/index.js.map +1 -1
- package/dist/infra/lib/llm/types.d.ts +1 -1
- package/dist/infra/lib/llm/types.d.ts.map +1 -1
- 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 +70 -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 -646
- 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 +65 -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
- package/commands/vibe.voice.md +0 -79
|
@@ -1,599 +1,599 @@
|
|
|
1
|
-
# Frontend Contract Tests: {Feature Name}
|
|
2
|
-
|
|
3
|
-
**Generated from**: `specs/{feature-name}.md` (Section 6: API Contract)
|
|
4
|
-
**Framework**: {Flutter | React | React Native | Vue}
|
|
5
|
-
**Language**: {Dart | TypeScript | JavaScript}
|
|
6
|
-
**Priority**: {HIGH | MEDIUM | LOW}
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Overview
|
|
11
|
-
|
|
12
|
-
Frontend Contract Testing **validates API contracts from the Consumer perspective**:
|
|
13
|
-
|
|
14
|
-
- ✅ API requests are sent according to contract
|
|
15
|
-
- ✅ API responses follow expected schema
|
|
16
|
-
- ✅ Error handling works as per contract
|
|
17
|
-
- ✅ Independent testing with mock server
|
|
18
|
-
|
|
19
|
-
**Consumer-Driven Contract Testing** (Pact pattern)
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## API Contracts (Consumer View)
|
|
24
|
-
|
|
25
|
-
### Contract 1: Create Resource
|
|
26
|
-
|
|
27
|
-
**Consumer Expectation**:
|
|
28
|
-
|
|
29
|
-
```json
|
|
30
|
-
{
|
|
31
|
-
"request": {
|
|
32
|
-
"method": "POST",
|
|
33
|
-
"path": "/api/v1/resource",
|
|
34
|
-
"headers": {
|
|
35
|
-
"Authorization": "Bearer {token}",
|
|
36
|
-
"Content-Type": "application/json"
|
|
37
|
-
},
|
|
38
|
-
"body": {
|
|
39
|
-
"field1": "string",
|
|
40
|
-
"field2": "integer"
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
"response": {
|
|
44
|
-
"status": 201,
|
|
45
|
-
"body": {
|
|
46
|
-
"id": "uuid",
|
|
47
|
-
"field1": "string",
|
|
48
|
-
"field2": "integer",
|
|
49
|
-
"createdAt": "datetime"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## Implementation
|
|
58
|
-
|
|
59
|
-
### Flutter (Dart + http_mock_adapter)
|
|
60
|
-
|
|
61
|
-
**File**: `test/contract/{feature_name}_contract_test.dart`
|
|
62
|
-
|
|
63
|
-
```dart
|
|
64
|
-
import 'package:flutter_test/flutter_test.dart';
|
|
65
|
-
import 'package:dio/dio.dart';
|
|
66
|
-
import 'package:http_mock_adapter/http_mock_adapter.dart';
|
|
67
|
-
import 'package:your_app/services/api_service.dart';
|
|
68
|
-
import 'package:your_app/models/resource.dart';
|
|
69
|
-
|
|
70
|
-
void main() {
|
|
71
|
-
late Dio dio;
|
|
72
|
-
late DioAdapter dioAdapter;
|
|
73
|
-
late ApiService apiService;
|
|
74
|
-
|
|
75
|
-
setUp(() {
|
|
76
|
-
dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
|
|
77
|
-
dioAdapter = DioAdapter(dio: dio);
|
|
78
|
-
apiService = ApiService(dio: dio);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
group('Create Resource Contract', () {
|
|
82
|
-
test('should match request contract', () async {
|
|
83
|
-
// Arrange: Expected request contract
|
|
84
|
-
final requestBody = {
|
|
85
|
-
'field1': 'test value',
|
|
86
|
-
'field2': 42,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Arrange: Mock response matching contract
|
|
90
|
-
final responseBody = {
|
|
91
|
-
'id': '123e4567-e89b-12d3-a456-426614174000',
|
|
92
|
-
'field1': 'test value',
|
|
93
|
-
'field2': 42,
|
|
94
|
-
'createdAt': '2025-01-17T10:00:00Z',
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
dioAdapter.onPost(
|
|
98
|
-
'/api/v1/resource',
|
|
99
|
-
(server) => server.reply(201, responseBody),
|
|
100
|
-
data: requestBody,
|
|
101
|
-
headers: {
|
|
102
|
-
'Authorization': 'Bearer test-token',
|
|
103
|
-
'Content-Type': 'application/json',
|
|
104
|
-
},
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// Act: Call API service
|
|
108
|
-
final result = await apiService.createResource(
|
|
109
|
-
field1: 'test value',
|
|
110
|
-
field2: 42,
|
|
111
|
-
token: 'test-token',
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Assert: Response matches contract
|
|
115
|
-
expect(result, isA<Resource>());
|
|
116
|
-
expect(result.id, isNotEmpty);
|
|
117
|
-
expect(result.field1, equals('test value'));
|
|
118
|
-
expect(result.field2, equals(42));
|
|
119
|
-
expect(result.createdAt, isA<DateTime>());
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('should handle error response contract', () async {
|
|
123
|
-
// Arrange: Error response contract
|
|
124
|
-
final errorBody = {
|
|
125
|
-
'error': 'ValidationError',
|
|
126
|
-
'message': 'field1 is required',
|
|
127
|
-
'details': ['field1 must not be empty'],
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
dioAdapter.onPost(
|
|
131
|
-
'/api/v1/resource',
|
|
132
|
-
(server) => server.reply(400, errorBody),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// Act & Assert: Error handling matches contract
|
|
136
|
-
expect(
|
|
137
|
-
() async => await apiService.createResource(
|
|
138
|
-
field1: '',
|
|
139
|
-
field2: 42,
|
|
140
|
-
token: 'test-token',
|
|
141
|
-
),
|
|
142
|
-
throwsA(isA<ApiException>().having(
|
|
143
|
-
(e) => e.statusCode,
|
|
144
|
-
'status code',
|
|
145
|
-
equals(400),
|
|
146
|
-
)),
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('should validate response schema', () async {
|
|
151
|
-
// Arrange: Response with invalid schema
|
|
152
|
-
final invalidResponse = {
|
|
153
|
-
'id': 'not-a-uuid', // Invalid UUID format
|
|
154
|
-
'field1': 123, // Wrong type
|
|
155
|
-
// Missing field2
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
dioAdapter.onPost(
|
|
159
|
-
'/api/v1/resource',
|
|
160
|
-
(server) => server.reply(201, invalidResponse),
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
// Act & Assert: Schema validation fails
|
|
164
|
-
expect(
|
|
165
|
-
() async => await apiService.createResource(
|
|
166
|
-
field1: 'test',
|
|
167
|
-
field2: 42,
|
|
168
|
-
token: 'test-token',
|
|
169
|
-
),
|
|
170
|
-
throwsA(isA<SchemaValidationException>()),
|
|
171
|
-
);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
group('Response Schema Validation', () {
|
|
176
|
-
test('validates UUID format', () {
|
|
177
|
-
final validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
178
|
-
expect(isValidUuid(validUuid), isTrue);
|
|
179
|
-
|
|
180
|
-
final invalidUuid = 'not-a-uuid';
|
|
181
|
-
expect(isValidUuid(invalidUuid), isFalse);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('validates DateTime format (ISO 8601)', () {
|
|
185
|
-
final validDateTime = '2025-01-17T10:00:00Z';
|
|
186
|
-
expect(() => DateTime.parse(validDateTime), returnsNormally);
|
|
187
|
-
|
|
188
|
-
final invalidDateTime = '2025-01-17'; // Missing time
|
|
189
|
-
expect(() => DateTime.parse(invalidDateTime), throwsFormatException);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Helper function
|
|
195
|
-
bool isValidUuid(String uuid) {
|
|
196
|
-
final uuidRegex = RegExp(
|
|
197
|
-
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
198
|
-
caseSensitive: false,
|
|
199
|
-
);
|
|
200
|
-
return uuidRegex.hasMatch(uuid);
|
|
201
|
-
}
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
**Run**:
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
flutter test test/contract/{feature_name}_contract_test.dart
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
---
|
|
211
|
-
|
|
212
|
-
### React (TypeScript + MSW + Zod)
|
|
213
|
-
|
|
214
|
-
**File**: `tests/contract/{feature-name}.contract.test.ts`
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
import { rest } from 'msw';
|
|
218
|
-
import { setupServer } from 'msw/node';
|
|
219
|
-
import { z } from 'zod';
|
|
220
|
-
import { createResource, ApiService } from '@/services/api';
|
|
221
|
-
|
|
222
|
-
// Zod schemas for contract validation
|
|
223
|
-
const CreateResourceRequestSchema = z.object({
|
|
224
|
-
field1: z.string().min(1).max(100),
|
|
225
|
-
field2: z.number().int().nonnegative(),
|
|
226
|
-
field3: z.boolean().optional(),
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const CreateResourceResponseSchema = z.object({
|
|
230
|
-
id: z.string().uuid(),
|
|
231
|
-
field1: z.string(),
|
|
232
|
-
field2: z.number().int(),
|
|
233
|
-
field3: z.boolean().optional(),
|
|
234
|
-
createdAt: z.string().datetime(),
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const ErrorResponseSchema = z.object({
|
|
238
|
-
error: z.string(),
|
|
239
|
-
message: z.string(),
|
|
240
|
-
details: z.array(z.string()).optional(),
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Mock server
|
|
244
|
-
const server = setupServer();
|
|
245
|
-
|
|
246
|
-
beforeAll(() => server.listen());
|
|
247
|
-
afterEach(() => server.resetHandlers());
|
|
248
|
-
afterAll(() => server.close());
|
|
249
|
-
|
|
250
|
-
describe('Create Resource Contract', () => {
|
|
251
|
-
it('should send request matching contract', async () => {
|
|
252
|
-
let capturedRequest: any;
|
|
253
|
-
|
|
254
|
-
server.use(
|
|
255
|
-
rest.post('/api/v1/resource', async (req, res, ctx) => {
|
|
256
|
-
capturedRequest = await req.json();
|
|
257
|
-
|
|
258
|
-
// Validate request matches contract
|
|
259
|
-
const result = CreateResourceRequestSchema.safeParse(capturedRequest);
|
|
260
|
-
expect(result.success).toBe(true);
|
|
261
|
-
|
|
262
|
-
return res(
|
|
263
|
-
ctx.status(201),
|
|
264
|
-
ctx.json({
|
|
265
|
-
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
266
|
-
field1: capturedRequest.field1,
|
|
267
|
-
field2: capturedRequest.field2,
|
|
268
|
-
field3: capturedRequest.field3 ?? false,
|
|
269
|
-
createdAt: new Date().toISOString(),
|
|
270
|
-
})
|
|
271
|
-
);
|
|
272
|
-
})
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
const result = await createResource({
|
|
276
|
-
field1: 'test value',
|
|
277
|
-
field2: 42,
|
|
278
|
-
field3: true,
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// Verify request contract
|
|
282
|
-
expect(capturedRequest).toMatchObject({
|
|
283
|
-
field1: 'test value',
|
|
284
|
-
field2: 42,
|
|
285
|
-
field3: true,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Verify response contract
|
|
289
|
-
const responseValidation = CreateResourceResponseSchema.safeParse(result);
|
|
290
|
-
expect(responseValidation.success).toBe(true);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('should handle error response contract', async () => {
|
|
294
|
-
server.use(
|
|
295
|
-
rest.post('/api/v1/resource', (req, res, ctx) => {
|
|
296
|
-
return res(
|
|
297
|
-
ctx.status(400),
|
|
298
|
-
ctx.json({
|
|
299
|
-
error: 'ValidationError',
|
|
300
|
-
message: 'field1 is required',
|
|
301
|
-
details: ['field1 must not be empty'],
|
|
302
|
-
})
|
|
303
|
-
);
|
|
304
|
-
})
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
await expect(
|
|
308
|
-
createResource({
|
|
309
|
-
field1: '',
|
|
310
|
-
field2: 42,
|
|
311
|
-
})
|
|
312
|
-
).rejects.toThrow();
|
|
313
|
-
|
|
314
|
-
// Verify error response matches contract
|
|
315
|
-
try {
|
|
316
|
-
await createResource({ field1: '', field2: 42 });
|
|
317
|
-
} catch (error: any) {
|
|
318
|
-
const errorValidation = ErrorResponseSchema.safeParse(error.response.data);
|
|
319
|
-
expect(errorValidation.success).toBe(true);
|
|
320
|
-
expect(error.response.status).toBe(400);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('should reject response with invalid schema', async () => {
|
|
325
|
-
server.use(
|
|
326
|
-
rest.post('/api/v1/resource', (req, res, ctx) => {
|
|
327
|
-
return res(
|
|
328
|
-
ctx.status(201),
|
|
329
|
-
ctx.json({
|
|
330
|
-
id: 'not-a-uuid', // Invalid UUID
|
|
331
|
-
field1: 123, // Wrong type
|
|
332
|
-
// Missing field2
|
|
333
|
-
})
|
|
334
|
-
);
|
|
335
|
-
})
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
await expect(
|
|
339
|
-
createResource({
|
|
340
|
-
field1: 'test',
|
|
341
|
-
field2: 42,
|
|
342
|
-
})
|
|
343
|
-
).rejects.toThrow('Schema validation failed');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('validates response headers', async () => {
|
|
347
|
-
let responseHeaders: Headers;
|
|
348
|
-
|
|
349
|
-
server.use(
|
|
350
|
-
rest.post('/api/v1/resource', (req, res, ctx) => {
|
|
351
|
-
return res(
|
|
352
|
-
ctx.status(201),
|
|
353
|
-
ctx.set('Content-Type', 'application/json'),
|
|
354
|
-
ctx.json({
|
|
355
|
-
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
356
|
-
field1: 'test',
|
|
357
|
-
field2: 42,
|
|
358
|
-
createdAt: new Date().toISOString(),
|
|
359
|
-
})
|
|
360
|
-
);
|
|
361
|
-
})
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
const response = await fetch('/api/v1/resource', {
|
|
365
|
-
method: 'POST',
|
|
366
|
-
body: JSON.stringify({ field1: 'test', field2: 42 }),
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
describe('Schema Validation Utilities', () => {
|
|
374
|
-
it('validates UUID format', () => {
|
|
375
|
-
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
376
|
-
const result = z.string().uuid().safeParse(validUuid);
|
|
377
|
-
expect(result.success).toBe(true);
|
|
378
|
-
|
|
379
|
-
const invalidUuid = 'not-a-uuid';
|
|
380
|
-
const invalidResult = z.string().uuid().safeParse(invalidUuid);
|
|
381
|
-
expect(invalidResult.success).toBe(false);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('validates ISO 8601 datetime', () => {
|
|
385
|
-
const validDate = '2025-01-17T10:00:00Z';
|
|
386
|
-
const result = z.string().datetime().safeParse(validDate);
|
|
387
|
-
expect(result.success).toBe(true);
|
|
388
|
-
|
|
389
|
-
const invalidDate = '2025-01-17'; // Missing time
|
|
390
|
-
const invalidResult = z.string().datetime().safeParse(invalidDate);
|
|
391
|
-
expect(invalidResult.success).toBe(false);
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
**Run**:
|
|
397
|
-
|
|
398
|
-
```bash
|
|
399
|
-
npm test -- tests/contract/{feature-name}.contract.test.ts
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
---
|
|
403
|
-
|
|
404
|
-
### React Native (TypeScript + Axios + MockAdapter)
|
|
405
|
-
|
|
406
|
-
**File**: `__tests__/contract/{feature-name}.contract.test.ts`
|
|
407
|
-
|
|
408
|
-
```typescript
|
|
409
|
-
import axios from 'axios';
|
|
410
|
-
import MockAdapter from 'axios-mock-adapter';
|
|
411
|
-
import { z } from 'zod';
|
|
412
|
-
import { ApiService } from '@/services/api';
|
|
413
|
-
|
|
414
|
-
const mock = new MockAdapter(axios);
|
|
415
|
-
|
|
416
|
-
const ResponseSchema = z.object({
|
|
417
|
-
id: z.string().uuid(),
|
|
418
|
-
field1: z.string(),
|
|
419
|
-
field2: z.number(),
|
|
420
|
-
createdAt: z.string().datetime(),
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
describe('Create Resource Contract (React Native)', () => {
|
|
424
|
-
beforeEach(() => {
|
|
425
|
-
mock.reset();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('should match API contract', async () => {
|
|
429
|
-
const requestBody = {
|
|
430
|
-
field1: 'test value',
|
|
431
|
-
field2: 42,
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const responseBody = {
|
|
435
|
-
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
436
|
-
field1: 'test value',
|
|
437
|
-
field2: 42,
|
|
438
|
-
createdAt: '2025-01-17T10:00:00Z',
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
mock.onPost('/api/v1/resource', requestBody).reply(201, responseBody);
|
|
442
|
-
|
|
443
|
-
const apiService = new ApiService(axios);
|
|
444
|
-
const result = await apiService.createResource(requestBody);
|
|
445
|
-
|
|
446
|
-
// Validate response schema
|
|
447
|
-
const validation = ResponseSchema.safeParse(result);
|
|
448
|
-
expect(validation.success).toBe(true);
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
**Run**:
|
|
454
|
-
|
|
455
|
-
```bash
|
|
456
|
-
npm test -- __tests__/contract/
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
---
|
|
460
|
-
|
|
461
|
-
## Pact Consumer Tests
|
|
462
|
-
|
|
463
|
-
### Flutter (dart_pact)
|
|
464
|
-
|
|
465
|
-
**File**: `test/pact/{feature_name}_pact_test.dart`
|
|
466
|
-
|
|
467
|
-
```dart
|
|
468
|
-
import 'package:pact_consumer_dart/pact_consumer_dart.dart';
|
|
469
|
-
import 'package:test/test.dart';
|
|
470
|
-
|
|
471
|
-
void main() {
|
|
472
|
-
late PactMockService mockService;
|
|
473
|
-
|
|
474
|
-
setUpAll(() async {
|
|
475
|
-
mockService = PactMockService(
|
|
476
|
-
consumer: 'FrontendApp',
|
|
477
|
-
provider: 'BackendAPI',
|
|
478
|
-
port: 1234,
|
|
479
|
-
);
|
|
480
|
-
await mockService.start();
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
tearDownAll(() async {
|
|
484
|
-
await mockService.stop();
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
test('create resource contract', () async {
|
|
488
|
-
await mockService
|
|
489
|
-
.given('user is authenticated')
|
|
490
|
-
.uponReceiving('a request to create resource')
|
|
491
|
-
.withRequest(
|
|
492
|
-
method: 'POST',
|
|
493
|
-
path: '/api/v1/resource',
|
|
494
|
-
headers: {'Authorization': 'Bearer token'},
|
|
495
|
-
body: {
|
|
496
|
-
'field1': 'test value',
|
|
497
|
-
'field2': 42,
|
|
498
|
-
},
|
|
499
|
-
)
|
|
500
|
-
.willRespondWith(
|
|
501
|
-
status: 201,
|
|
502
|
-
body: {
|
|
503
|
-
'id': Matchers.uuid,
|
|
504
|
-
'field1': Matchers.string('test value'),
|
|
505
|
-
'field2': Matchers.integer(42),
|
|
506
|
-
'createdAt': Matchers.iso8601DateTime,
|
|
507
|
-
},
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
await mockService.run((config) async {
|
|
511
|
-
// Test your API service against mock
|
|
512
|
-
final apiService = ApiService(baseUrl: config.baseUrl);
|
|
513
|
-
final result = await apiService.createResource(
|
|
514
|
-
field1: 'test value',
|
|
515
|
-
field2: 42,
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
expect(result.id, isNotEmpty);
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
// Pact file generated: pacts/FrontendApp-BackendAPI.json
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
## CI/CD Integration
|
|
529
|
-
|
|
530
|
-
```yaml
|
|
531
|
-
# .github/workflows/contract-tests.yml
|
|
532
|
-
name: Frontend Contract Tests
|
|
533
|
-
|
|
534
|
-
on: [pull_request]
|
|
535
|
-
|
|
536
|
-
jobs:
|
|
537
|
-
contract-tests:
|
|
538
|
-
runs-on: ubuntu-latest
|
|
539
|
-
|
|
540
|
-
steps:
|
|
541
|
-
- uses: actions/checkout@v2
|
|
542
|
-
|
|
543
|
-
- name: Setup Flutter
|
|
544
|
-
uses: subosito/flutter-action@v2
|
|
545
|
-
with:
|
|
546
|
-
flutter-version: '3.24.0'
|
|
547
|
-
|
|
548
|
-
- name: Run contract tests
|
|
549
|
-
run: flutter test test/contract/
|
|
550
|
-
|
|
551
|
-
- name: Run Pact tests
|
|
552
|
-
run: flutter test test/pact/
|
|
553
|
-
|
|
554
|
-
- name: Publish Pact
|
|
555
|
-
if: success()
|
|
556
|
-
run: |
|
|
557
|
-
flutter pub global activate pact_broker_cli
|
|
558
|
-
pact-broker publish pacts/ \
|
|
559
|
-
--consumer-app-version=${{ github.sha }} \
|
|
560
|
-
--broker-base-url=${{ secrets.PACT_BROKER_URL }}
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
## Best Practices
|
|
566
|
-
|
|
567
|
-
1. **Use Mock Server**
|
|
568
|
-
- ✅ Independent testing without backend
|
|
569
|
-
- ✅ Immediate detection of contract violations
|
|
570
|
-
|
|
571
|
-
2. **Schema Validation**
|
|
572
|
-
- ✅ Validate responses with Zod, JSON Schema
|
|
573
|
-
- ✅ Ensure type safety
|
|
574
|
-
|
|
575
|
-
3. **Consumer-Driven**
|
|
576
|
-
- ✅ Define frontend requirements first
|
|
577
|
-
- ✅ Share Pact files with backend team
|
|
578
|
-
|
|
579
|
-
4. **CI/CD Automation**
|
|
580
|
-
- ✅ Contract verification on every PR
|
|
581
|
-
- ✅ Central management with Pact Broker
|
|
582
|
-
|
|
583
|
-
---
|
|
584
|
-
|
|
585
|
-
## Next Steps
|
|
586
|
-
|
|
587
|
-
```bash
|
|
588
|
-
# 1. Write contract tests
|
|
589
|
-
vibe contract "{feature name}" --frontend
|
|
590
|
-
|
|
591
|
-
# 2. Develop with mock server
|
|
592
|
-
flutter test test/contract/ --watch
|
|
593
|
-
|
|
594
|
-
# 3. Generate and publish Pact
|
|
595
|
-
flutter test test/pact/
|
|
596
|
-
|
|
597
|
-
# 4. Verify contract with backend
|
|
598
|
-
vibe verify "{feature name}" --contract
|
|
599
|
-
```
|
|
1
|
+
# Frontend Contract Tests: {Feature Name}
|
|
2
|
+
|
|
3
|
+
**Generated from**: `specs/{feature-name}.md` (Section 6: API Contract)
|
|
4
|
+
**Framework**: {Flutter | React | React Native | Vue}
|
|
5
|
+
**Language**: {Dart | TypeScript | JavaScript}
|
|
6
|
+
**Priority**: {HIGH | MEDIUM | LOW}
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Frontend Contract Testing **validates API contracts from the Consumer perspective**:
|
|
13
|
+
|
|
14
|
+
- ✅ API requests are sent according to contract
|
|
15
|
+
- ✅ API responses follow expected schema
|
|
16
|
+
- ✅ Error handling works as per contract
|
|
17
|
+
- ✅ Independent testing with mock server
|
|
18
|
+
|
|
19
|
+
**Consumer-Driven Contract Testing** (Pact pattern)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## API Contracts (Consumer View)
|
|
24
|
+
|
|
25
|
+
### Contract 1: Create Resource
|
|
26
|
+
|
|
27
|
+
**Consumer Expectation**:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"request": {
|
|
32
|
+
"method": "POST",
|
|
33
|
+
"path": "/api/v1/resource",
|
|
34
|
+
"headers": {
|
|
35
|
+
"Authorization": "Bearer {token}",
|
|
36
|
+
"Content-Type": "application/json"
|
|
37
|
+
},
|
|
38
|
+
"body": {
|
|
39
|
+
"field1": "string",
|
|
40
|
+
"field2": "integer"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"response": {
|
|
44
|
+
"status": 201,
|
|
45
|
+
"body": {
|
|
46
|
+
"id": "uuid",
|
|
47
|
+
"field1": "string",
|
|
48
|
+
"field2": "integer",
|
|
49
|
+
"createdAt": "datetime"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Implementation
|
|
58
|
+
|
|
59
|
+
### Flutter (Dart + http_mock_adapter)
|
|
60
|
+
|
|
61
|
+
**File**: `test/contract/{feature_name}_contract_test.dart`
|
|
62
|
+
|
|
63
|
+
```dart
|
|
64
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
65
|
+
import 'package:dio/dio.dart';
|
|
66
|
+
import 'package:http_mock_adapter/http_mock_adapter.dart';
|
|
67
|
+
import 'package:your_app/services/api_service.dart';
|
|
68
|
+
import 'package:your_app/models/resource.dart';
|
|
69
|
+
|
|
70
|
+
void main() {
|
|
71
|
+
late Dio dio;
|
|
72
|
+
late DioAdapter dioAdapter;
|
|
73
|
+
late ApiService apiService;
|
|
74
|
+
|
|
75
|
+
setUp(() {
|
|
76
|
+
dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
|
|
77
|
+
dioAdapter = DioAdapter(dio: dio);
|
|
78
|
+
apiService = ApiService(dio: dio);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
group('Create Resource Contract', () {
|
|
82
|
+
test('should match request contract', () async {
|
|
83
|
+
// Arrange: Expected request contract
|
|
84
|
+
final requestBody = {
|
|
85
|
+
'field1': 'test value',
|
|
86
|
+
'field2': 42,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Arrange: Mock response matching contract
|
|
90
|
+
final responseBody = {
|
|
91
|
+
'id': '123e4567-e89b-12d3-a456-426614174000',
|
|
92
|
+
'field1': 'test value',
|
|
93
|
+
'field2': 42,
|
|
94
|
+
'createdAt': '2025-01-17T10:00:00Z',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
dioAdapter.onPost(
|
|
98
|
+
'/api/v1/resource',
|
|
99
|
+
(server) => server.reply(201, responseBody),
|
|
100
|
+
data: requestBody,
|
|
101
|
+
headers: {
|
|
102
|
+
'Authorization': 'Bearer test-token',
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Act: Call API service
|
|
108
|
+
final result = await apiService.createResource(
|
|
109
|
+
field1: 'test value',
|
|
110
|
+
field2: 42,
|
|
111
|
+
token: 'test-token',
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Assert: Response matches contract
|
|
115
|
+
expect(result, isA<Resource>());
|
|
116
|
+
expect(result.id, isNotEmpty);
|
|
117
|
+
expect(result.field1, equals('test value'));
|
|
118
|
+
expect(result.field2, equals(42));
|
|
119
|
+
expect(result.createdAt, isA<DateTime>());
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should handle error response contract', () async {
|
|
123
|
+
// Arrange: Error response contract
|
|
124
|
+
final errorBody = {
|
|
125
|
+
'error': 'ValidationError',
|
|
126
|
+
'message': 'field1 is required',
|
|
127
|
+
'details': ['field1 must not be empty'],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
dioAdapter.onPost(
|
|
131
|
+
'/api/v1/resource',
|
|
132
|
+
(server) => server.reply(400, errorBody),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Act & Assert: Error handling matches contract
|
|
136
|
+
expect(
|
|
137
|
+
() async => await apiService.createResource(
|
|
138
|
+
field1: '',
|
|
139
|
+
field2: 42,
|
|
140
|
+
token: 'test-token',
|
|
141
|
+
),
|
|
142
|
+
throwsA(isA<ApiException>().having(
|
|
143
|
+
(e) => e.statusCode,
|
|
144
|
+
'status code',
|
|
145
|
+
equals(400),
|
|
146
|
+
)),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should validate response schema', () async {
|
|
151
|
+
// Arrange: Response with invalid schema
|
|
152
|
+
final invalidResponse = {
|
|
153
|
+
'id': 'not-a-uuid', // Invalid UUID format
|
|
154
|
+
'field1': 123, // Wrong type
|
|
155
|
+
// Missing field2
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
dioAdapter.onPost(
|
|
159
|
+
'/api/v1/resource',
|
|
160
|
+
(server) => server.reply(201, invalidResponse),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Act & Assert: Schema validation fails
|
|
164
|
+
expect(
|
|
165
|
+
() async => await apiService.createResource(
|
|
166
|
+
field1: 'test',
|
|
167
|
+
field2: 42,
|
|
168
|
+
token: 'test-token',
|
|
169
|
+
),
|
|
170
|
+
throwsA(isA<SchemaValidationException>()),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
group('Response Schema Validation', () {
|
|
176
|
+
test('validates UUID format', () {
|
|
177
|
+
final validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
178
|
+
expect(isValidUuid(validUuid), isTrue);
|
|
179
|
+
|
|
180
|
+
final invalidUuid = 'not-a-uuid';
|
|
181
|
+
expect(isValidUuid(invalidUuid), isFalse);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('validates DateTime format (ISO 8601)', () {
|
|
185
|
+
final validDateTime = '2025-01-17T10:00:00Z';
|
|
186
|
+
expect(() => DateTime.parse(validDateTime), returnsNormally);
|
|
187
|
+
|
|
188
|
+
final invalidDateTime = '2025-01-17'; // Missing time
|
|
189
|
+
expect(() => DateTime.parse(invalidDateTime), throwsFormatException);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Helper function
|
|
195
|
+
bool isValidUuid(String uuid) {
|
|
196
|
+
final uuidRegex = RegExp(
|
|
197
|
+
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
198
|
+
caseSensitive: false,
|
|
199
|
+
);
|
|
200
|
+
return uuidRegex.hasMatch(uuid);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Run**:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
flutter test test/contract/{feature_name}_contract_test.dart
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### React (TypeScript + MSW + Zod)
|
|
213
|
+
|
|
214
|
+
**File**: `tests/contract/{feature-name}.contract.test.ts`
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { rest } from 'msw';
|
|
218
|
+
import { setupServer } from 'msw/node';
|
|
219
|
+
import { z } from 'zod';
|
|
220
|
+
import { createResource, ApiService } from '@/services/api';
|
|
221
|
+
|
|
222
|
+
// Zod schemas for contract validation
|
|
223
|
+
const CreateResourceRequestSchema = z.object({
|
|
224
|
+
field1: z.string().min(1).max(100),
|
|
225
|
+
field2: z.number().int().nonnegative(),
|
|
226
|
+
field3: z.boolean().optional(),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const CreateResourceResponseSchema = z.object({
|
|
230
|
+
id: z.string().uuid(),
|
|
231
|
+
field1: z.string(),
|
|
232
|
+
field2: z.number().int(),
|
|
233
|
+
field3: z.boolean().optional(),
|
|
234
|
+
createdAt: z.string().datetime(),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const ErrorResponseSchema = z.object({
|
|
238
|
+
error: z.string(),
|
|
239
|
+
message: z.string(),
|
|
240
|
+
details: z.array(z.string()).optional(),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Mock server
|
|
244
|
+
const server = setupServer();
|
|
245
|
+
|
|
246
|
+
beforeAll(() => server.listen());
|
|
247
|
+
afterEach(() => server.resetHandlers());
|
|
248
|
+
afterAll(() => server.close());
|
|
249
|
+
|
|
250
|
+
describe('Create Resource Contract', () => {
|
|
251
|
+
it('should send request matching contract', async () => {
|
|
252
|
+
let capturedRequest: any;
|
|
253
|
+
|
|
254
|
+
server.use(
|
|
255
|
+
rest.post('/api/v1/resource', async (req, res, ctx) => {
|
|
256
|
+
capturedRequest = await req.json();
|
|
257
|
+
|
|
258
|
+
// Validate request matches contract
|
|
259
|
+
const result = CreateResourceRequestSchema.safeParse(capturedRequest);
|
|
260
|
+
expect(result.success).toBe(true);
|
|
261
|
+
|
|
262
|
+
return res(
|
|
263
|
+
ctx.status(201),
|
|
264
|
+
ctx.json({
|
|
265
|
+
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
266
|
+
field1: capturedRequest.field1,
|
|
267
|
+
field2: capturedRequest.field2,
|
|
268
|
+
field3: capturedRequest.field3 ?? false,
|
|
269
|
+
createdAt: new Date().toISOString(),
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const result = await createResource({
|
|
276
|
+
field1: 'test value',
|
|
277
|
+
field2: 42,
|
|
278
|
+
field3: true,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Verify request contract
|
|
282
|
+
expect(capturedRequest).toMatchObject({
|
|
283
|
+
field1: 'test value',
|
|
284
|
+
field2: 42,
|
|
285
|
+
field3: true,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Verify response contract
|
|
289
|
+
const responseValidation = CreateResourceResponseSchema.safeParse(result);
|
|
290
|
+
expect(responseValidation.success).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should handle error response contract', async () => {
|
|
294
|
+
server.use(
|
|
295
|
+
rest.post('/api/v1/resource', (req, res, ctx) => {
|
|
296
|
+
return res(
|
|
297
|
+
ctx.status(400),
|
|
298
|
+
ctx.json({
|
|
299
|
+
error: 'ValidationError',
|
|
300
|
+
message: 'field1 is required',
|
|
301
|
+
details: ['field1 must not be empty'],
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
await expect(
|
|
308
|
+
createResource({
|
|
309
|
+
field1: '',
|
|
310
|
+
field2: 42,
|
|
311
|
+
})
|
|
312
|
+
).rejects.toThrow();
|
|
313
|
+
|
|
314
|
+
// Verify error response matches contract
|
|
315
|
+
try {
|
|
316
|
+
await createResource({ field1: '', field2: 42 });
|
|
317
|
+
} catch (error: any) {
|
|
318
|
+
const errorValidation = ErrorResponseSchema.safeParse(error.response.data);
|
|
319
|
+
expect(errorValidation.success).toBe(true);
|
|
320
|
+
expect(error.response.status).toBe(400);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should reject response with invalid schema', async () => {
|
|
325
|
+
server.use(
|
|
326
|
+
rest.post('/api/v1/resource', (req, res, ctx) => {
|
|
327
|
+
return res(
|
|
328
|
+
ctx.status(201),
|
|
329
|
+
ctx.json({
|
|
330
|
+
id: 'not-a-uuid', // Invalid UUID
|
|
331
|
+
field1: 123, // Wrong type
|
|
332
|
+
// Missing field2
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
})
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
await expect(
|
|
339
|
+
createResource({
|
|
340
|
+
field1: 'test',
|
|
341
|
+
field2: 42,
|
|
342
|
+
})
|
|
343
|
+
).rejects.toThrow('Schema validation failed');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('validates response headers', async () => {
|
|
347
|
+
let responseHeaders: Headers;
|
|
348
|
+
|
|
349
|
+
server.use(
|
|
350
|
+
rest.post('/api/v1/resource', (req, res, ctx) => {
|
|
351
|
+
return res(
|
|
352
|
+
ctx.status(201),
|
|
353
|
+
ctx.set('Content-Type', 'application/json'),
|
|
354
|
+
ctx.json({
|
|
355
|
+
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
356
|
+
field1: 'test',
|
|
357
|
+
field2: 42,
|
|
358
|
+
createdAt: new Date().toISOString(),
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const response = await fetch('/api/v1/resource', {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
body: JSON.stringify({ field1: 'test', field2: 42 }),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('Schema Validation Utilities', () => {
|
|
374
|
+
it('validates UUID format', () => {
|
|
375
|
+
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
376
|
+
const result = z.string().uuid().safeParse(validUuid);
|
|
377
|
+
expect(result.success).toBe(true);
|
|
378
|
+
|
|
379
|
+
const invalidUuid = 'not-a-uuid';
|
|
380
|
+
const invalidResult = z.string().uuid().safeParse(invalidUuid);
|
|
381
|
+
expect(invalidResult.success).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('validates ISO 8601 datetime', () => {
|
|
385
|
+
const validDate = '2025-01-17T10:00:00Z';
|
|
386
|
+
const result = z.string().datetime().safeParse(validDate);
|
|
387
|
+
expect(result.success).toBe(true);
|
|
388
|
+
|
|
389
|
+
const invalidDate = '2025-01-17'; // Missing time
|
|
390
|
+
const invalidResult = z.string().datetime().safeParse(invalidDate);
|
|
391
|
+
expect(invalidResult.success).toBe(false);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Run**:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
npm test -- tests/contract/{feature-name}.contract.test.ts
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
### React Native (TypeScript + Axios + MockAdapter)
|
|
405
|
+
|
|
406
|
+
**File**: `__tests__/contract/{feature-name}.contract.test.ts`
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import axios from 'axios';
|
|
410
|
+
import MockAdapter from 'axios-mock-adapter';
|
|
411
|
+
import { z } from 'zod';
|
|
412
|
+
import { ApiService } from '@/services/api';
|
|
413
|
+
|
|
414
|
+
const mock = new MockAdapter(axios);
|
|
415
|
+
|
|
416
|
+
const ResponseSchema = z.object({
|
|
417
|
+
id: z.string().uuid(),
|
|
418
|
+
field1: z.string(),
|
|
419
|
+
field2: z.number(),
|
|
420
|
+
createdAt: z.string().datetime(),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe('Create Resource Contract (React Native)', () => {
|
|
424
|
+
beforeEach(() => {
|
|
425
|
+
mock.reset();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should match API contract', async () => {
|
|
429
|
+
const requestBody = {
|
|
430
|
+
field1: 'test value',
|
|
431
|
+
field2: 42,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const responseBody = {
|
|
435
|
+
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
436
|
+
field1: 'test value',
|
|
437
|
+
field2: 42,
|
|
438
|
+
createdAt: '2025-01-17T10:00:00Z',
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
mock.onPost('/api/v1/resource', requestBody).reply(201, responseBody);
|
|
442
|
+
|
|
443
|
+
const apiService = new ApiService(axios);
|
|
444
|
+
const result = await apiService.createResource(requestBody);
|
|
445
|
+
|
|
446
|
+
// Validate response schema
|
|
447
|
+
const validation = ResponseSchema.safeParse(result);
|
|
448
|
+
expect(validation.success).toBe(true);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Run**:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
npm test -- __tests__/contract/
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Pact Consumer Tests
|
|
462
|
+
|
|
463
|
+
### Flutter (dart_pact)
|
|
464
|
+
|
|
465
|
+
**File**: `test/pact/{feature_name}_pact_test.dart`
|
|
466
|
+
|
|
467
|
+
```dart
|
|
468
|
+
import 'package:pact_consumer_dart/pact_consumer_dart.dart';
|
|
469
|
+
import 'package:test/test.dart';
|
|
470
|
+
|
|
471
|
+
void main() {
|
|
472
|
+
late PactMockService mockService;
|
|
473
|
+
|
|
474
|
+
setUpAll(() async {
|
|
475
|
+
mockService = PactMockService(
|
|
476
|
+
consumer: 'FrontendApp',
|
|
477
|
+
provider: 'BackendAPI',
|
|
478
|
+
port: 1234,
|
|
479
|
+
);
|
|
480
|
+
await mockService.start();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
tearDownAll(() async {
|
|
484
|
+
await mockService.stop();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('create resource contract', () async {
|
|
488
|
+
await mockService
|
|
489
|
+
.given('user is authenticated')
|
|
490
|
+
.uponReceiving('a request to create resource')
|
|
491
|
+
.withRequest(
|
|
492
|
+
method: 'POST',
|
|
493
|
+
path: '/api/v1/resource',
|
|
494
|
+
headers: {'Authorization': 'Bearer token'},
|
|
495
|
+
body: {
|
|
496
|
+
'field1': 'test value',
|
|
497
|
+
'field2': 42,
|
|
498
|
+
},
|
|
499
|
+
)
|
|
500
|
+
.willRespondWith(
|
|
501
|
+
status: 201,
|
|
502
|
+
body: {
|
|
503
|
+
'id': Matchers.uuid,
|
|
504
|
+
'field1': Matchers.string('test value'),
|
|
505
|
+
'field2': Matchers.integer(42),
|
|
506
|
+
'createdAt': Matchers.iso8601DateTime,
|
|
507
|
+
},
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
await mockService.run((config) async {
|
|
511
|
+
// Test your API service against mock
|
|
512
|
+
final apiService = ApiService(baseUrl: config.baseUrl);
|
|
513
|
+
final result = await apiService.createResource(
|
|
514
|
+
field1: 'test value',
|
|
515
|
+
field2: 42,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
expect(result.id, isNotEmpty);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Pact file generated: pacts/FrontendApp-BackendAPI.json
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## CI/CD Integration
|
|
529
|
+
|
|
530
|
+
```yaml
|
|
531
|
+
# .github/workflows/contract-tests.yml
|
|
532
|
+
name: Frontend Contract Tests
|
|
533
|
+
|
|
534
|
+
on: [pull_request]
|
|
535
|
+
|
|
536
|
+
jobs:
|
|
537
|
+
contract-tests:
|
|
538
|
+
runs-on: ubuntu-latest
|
|
539
|
+
|
|
540
|
+
steps:
|
|
541
|
+
- uses: actions/checkout@v2
|
|
542
|
+
|
|
543
|
+
- name: Setup Flutter
|
|
544
|
+
uses: subosito/flutter-action@v2
|
|
545
|
+
with:
|
|
546
|
+
flutter-version: '3.24.0'
|
|
547
|
+
|
|
548
|
+
- name: Run contract tests
|
|
549
|
+
run: flutter test test/contract/
|
|
550
|
+
|
|
551
|
+
- name: Run Pact tests
|
|
552
|
+
run: flutter test test/pact/
|
|
553
|
+
|
|
554
|
+
- name: Publish Pact
|
|
555
|
+
if: success()
|
|
556
|
+
run: |
|
|
557
|
+
flutter pub global activate pact_broker_cli
|
|
558
|
+
pact-broker publish pacts/ \
|
|
559
|
+
--consumer-app-version=${{ github.sha }} \
|
|
560
|
+
--broker-base-url=${{ secrets.PACT_BROKER_URL }}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Best Practices
|
|
566
|
+
|
|
567
|
+
1. **Use Mock Server**
|
|
568
|
+
- ✅ Independent testing without backend
|
|
569
|
+
- ✅ Immediate detection of contract violations
|
|
570
|
+
|
|
571
|
+
2. **Schema Validation**
|
|
572
|
+
- ✅ Validate responses with Zod, JSON Schema
|
|
573
|
+
- ✅ Ensure type safety
|
|
574
|
+
|
|
575
|
+
3. **Consumer-Driven**
|
|
576
|
+
- ✅ Define frontend requirements first
|
|
577
|
+
- ✅ Share Pact files with backend team
|
|
578
|
+
|
|
579
|
+
4. **CI/CD Automation**
|
|
580
|
+
- ✅ Contract verification on every PR
|
|
581
|
+
- ✅ Central management with Pact Broker
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
## Next Steps
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
# 1. Write contract tests
|
|
589
|
+
vibe contract "{feature name}" --frontend
|
|
590
|
+
|
|
591
|
+
# 2. Develop with mock server
|
|
592
|
+
flutter test test/contract/ --watch
|
|
593
|
+
|
|
594
|
+
# 3. Generate and publish Pact
|
|
595
|
+
flutter test test/pact/
|
|
596
|
+
|
|
597
|
+
# 4. Verify contract with backend
|
|
598
|
+
vibe verify "{feature name}" --contract
|
|
599
|
+
```
|