bros-harness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/SECURITY.md +16 -0
- package/assets/agents.manifest.json +55 -0
- package/assets/commands.manifest.json +35 -0
- package/assets/docs.manifest.json +20 -0
- package/assets/import-report.md +25 -0
- package/assets/manifest.json +799 -0
- package/assets/opencode/agents/README.md +3 -0
- package/assets/opencode/agents/bro-build.md +256 -0
- package/assets/opencode/agents/bro-design.md +77 -0
- package/assets/opencode/agents/bro-docs.md +72 -0
- package/assets/opencode/agents/bro-explore.md +143 -0
- package/assets/opencode/agents/bro-ops.md +195 -0
- package/assets/opencode/agents/bro-shield.md +77 -0
- package/assets/opencode/agents/bro-test.md +204 -0
- package/assets/opencode/agents/bro-ui.md +135 -0
- package/assets/opencode/agents/mighty-bro.md +252 -0
- package/assets/opencode/commands/README.md +3 -0
- package/assets/opencode/commands/bros-assemble.md +32 -0
- package/assets/opencode/commands/bros-build.md +58 -0
- package/assets/opencode/commands/bros-plan.md +83 -0
- package/assets/opencode/commands/bros-review.md +38 -0
- package/assets/opencode/commands/bros-status.md +26 -0
- package/assets/opencode/docs/README.md +3 -0
- package/assets/opencode/docs/bros-builtin-skills.md +63 -0
- package/assets/opencode/docs/bros-harness.md +194 -0
- package/assets/opencode/skills/README.md +3 -0
- package/assets/opencode/skills/agent-architecture-audit/SKILL.md +256 -0
- package/assets/opencode/skills/agent-harness-construction/.openskills.json +7 -0
- package/assets/opencode/skills/agent-harness-construction/SKILL.md +73 -0
- package/assets/opencode/skills/agent-introspection-debugging/.openskills.json +7 -0
- package/assets/opencode/skills/agent-introspection-debugging/SKILL.md +153 -0
- package/assets/opencode/skills/api-design/.openskills.json +7 -0
- package/assets/opencode/skills/api-design/agents/openai.yaml +7 -0
- package/assets/opencode/skills/architecture-decision-records/.openskills.json +7 -0
- package/assets/opencode/skills/architecture-decision-records/SKILL.md +179 -0
- package/assets/opencode/skills/article-writing/.openskills.json +7 -0
- package/assets/opencode/skills/article-writing/SKILL.md +79 -0
- package/assets/opencode/skills/article-writing/agents/openai.yaml +7 -0
- package/assets/opencode/skills/automation-audit-ops/.openskills.json +7 -0
- package/assets/opencode/skills/automation-audit-ops/SKILL.md +142 -0
- package/assets/opencode/skills/backend-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/backend-patterns/SKILL.md +561 -0
- package/assets/opencode/skills/backend-patterns/agents/openai.yaml +7 -0
- package/assets/opencode/skills/benchmark/.openskills.json +7 -0
- package/assets/opencode/skills/benchmark/SKILL.md +93 -0
- package/assets/opencode/skills/bros-orchestrate/SKILL.md +455 -0
- package/assets/opencode/skills/browser-qa/.openskills.json +7 -0
- package/assets/opencode/skills/browser-qa/SKILL.md +87 -0
- package/assets/opencode/skills/canary-watch/.openskills.json +7 -0
- package/assets/opencode/skills/canary-watch/SKILL.md +107 -0
- package/assets/opencode/skills/code-review-expert/SKILL.md +155 -0
- package/assets/opencode/skills/code-review-expert/agents/agent.yaml +7 -0
- package/assets/opencode/skills/code-review-expert/references/code-quality-checklist.md +130 -0
- package/assets/opencode/skills/code-review-expert/references/removal-plan.md +52 -0
- package/assets/opencode/skills/code-review-expert/references/security-checklist.md +118 -0
- package/assets/opencode/skills/code-review-expert/references/solid-checklist.md +65 -0
- package/assets/opencode/skills/code-tour/.openskills.json +7 -0
- package/assets/opencode/skills/code-tour/SKILL.md +236 -0
- package/assets/opencode/skills/coding-standards/.openskills.json +7 -0
- package/assets/opencode/skills/coding-standards/SKILL.md +549 -0
- package/assets/opencode/skills/coding-standards/agents/openai.yaml +7 -0
- package/assets/opencode/skills/context-budget/.openskills.json +7 -0
- package/assets/opencode/skills/context-budget/SKILL.md +135 -0
- package/assets/opencode/skills/database-migrations/.openskills.json +7 -0
- package/assets/opencode/skills/database-migrations/SKILL.md +429 -0
- package/assets/opencode/skills/deployment-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/deployment-patterns/SKILL.md +427 -0
- package/assets/opencode/skills/design-system/.openskills.json +7 -0
- package/assets/opencode/skills/design-system/SKILL.md +82 -0
- package/assets/opencode/skills/docker-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/docker-patterns/SKILL.md +364 -0
- package/assets/opencode/skills/documentation-lookup/.openskills.json +7 -0
- package/assets/opencode/skills/documentation-lookup/SKILL.md +90 -0
- package/assets/opencode/skills/documentation-lookup/agents/openai.yaml +7 -0
- package/assets/opencode/skills/e2e-testing/.openskills.json +7 -0
- package/assets/opencode/skills/e2e-testing/SKILL.md +326 -0
- package/assets/opencode/skills/e2e-testing/agents/openai.yaml +7 -0
- package/assets/opencode/skills/error-handling/SKILL.md +376 -0
- package/assets/opencode/skills/frontend-design/.openskills.json +7 -0
- package/assets/opencode/skills/frontend-design/SKILL.md +145 -0
- package/assets/opencode/skills/frontend-design-direction/SKILL.md +92 -0
- package/assets/opencode/skills/frontend-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/frontend-patterns/SKILL.md +642 -0
- package/assets/opencode/skills/frontend-patterns/agents/openai.yaml +7 -0
- package/assets/opencode/skills/gateguard/.openskills.json +7 -0
- package/assets/opencode/skills/gateguard/SKILL.md +125 -0
- package/assets/opencode/skills/git-master/SKILL.md +60 -0
- package/assets/opencode/skills/golang-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/golang-patterns/SKILL.md +674 -0
- package/assets/opencode/skills/golang-testing/.openskills.json +7 -0
- package/assets/opencode/skills/golang-testing/SKILL.md +720 -0
- package/assets/opencode/skills/grafana-dashboard-design/SKILL.md +65 -0
- package/assets/opencode/skills/hexagonal-architecture/.openskills.json +7 -0
- package/assets/opencode/skills/hexagonal-architecture/SKILL.md +276 -0
- package/assets/opencode/skills/java-coding-standards/.openskills.json +7 -0
- package/assets/opencode/skills/java-coding-standards/SKILL.md +383 -0
- package/assets/opencode/skills/jpa-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/jpa-patterns/SKILL.md +151 -0
- package/assets/opencode/skills/knowledge-ops/.openskills.json +7 -0
- package/assets/opencode/skills/knowledge-ops/SKILL.md +154 -0
- package/assets/opencode/skills/make-interfaces-feel-better/SKILL.md +151 -0
- package/assets/opencode/skills/mysql-patterns/SKILL.md +412 -0
- package/assets/opencode/skills/nestjs-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/nestjs-patterns/SKILL.md +230 -0
- package/assets/opencode/skills/nextjs-turbopack/.openskills.json +7 -0
- package/assets/opencode/skills/nextjs-turbopack/SKILL.md +57 -0
- package/assets/opencode/skills/nextjs-turbopack/agents/openai.yaml +7 -0
- package/assets/opencode/skills/parallel-execution-optimizer/SKILL.md +72 -0
- package/assets/opencode/skills/postgres-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/postgres-patterns/SKILL.md +147 -0
- package/assets/opencode/skills/prisma-patterns/SKILL.md +371 -0
- package/assets/opencode/skills/product-capability/.openskills.json +7 -0
- package/assets/opencode/skills/product-capability/SKILL.md +141 -0
- package/assets/opencode/skills/product-lens/.openskills.json +7 -0
- package/assets/opencode/skills/product-lens/SKILL.md +92 -0
- package/assets/opencode/skills/production-audit/SKILL.md +206 -0
- package/assets/opencode/skills/python-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/python-patterns/SKILL.md +750 -0
- package/assets/opencode/skills/python-testing/.openskills.json +7 -0
- package/assets/opencode/skills/python-testing/SKILL.md +816 -0
- package/assets/opencode/skills/redis-patterns/SKILL.md +403 -0
- package/assets/opencode/skills/requirements-clarity/README.md +260 -0
- package/assets/opencode/skills/requirements-clarity/SKILL.md +324 -0
- package/assets/opencode/skills/rust-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/rust-patterns/SKILL.md +499 -0
- package/assets/opencode/skills/rust-testing/.openskills.json +7 -0
- package/assets/opencode/skills/rust-testing/SKILL.md +500 -0
- package/assets/opencode/skills/safety-guard/.openskills.json +7 -0
- package/assets/opencode/skills/safety-guard/SKILL.md +75 -0
- package/assets/opencode/skills/search-first/.openskills.json +7 -0
- package/assets/opencode/skills/search-first/SKILL.md +181 -0
- package/assets/opencode/skills/security-review/.openskills.json +7 -0
- package/assets/opencode/skills/security-review/agents/openai.yaml +7 -0
- package/assets/opencode/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/assets/opencode/skills/security-scan/.openskills.json +7 -0
- package/assets/opencode/skills/security-scan/SKILL.md +165 -0
- package/assets/opencode/skills/springboot-patterns/.openskills.json +7 -0
- package/assets/opencode/skills/springboot-patterns/SKILL.md +314 -0
- package/assets/opencode/skills/springboot-tdd/.openskills.json +7 -0
- package/assets/opencode/skills/springboot-tdd/SKILL.md +158 -0
- package/assets/opencode/skills/springboot-verification/.openskills.json +7 -0
- package/assets/opencode/skills/springboot-verification/SKILL.md +231 -0
- package/assets/opencode/skills/strategic-compact/.openskills.json +7 -0
- package/assets/opencode/skills/strategic-compact/SKILL.md +131 -0
- package/assets/opencode/skills/strategic-compact/agents/openai.yaml +7 -0
- package/assets/opencode/skills/strategic-compact/suggest-compact.sh +54 -0
- package/assets/opencode/skills/tdd-workflow/.openskills.json +7 -0
- package/assets/opencode/skills/tdd-workflow/SKILL.md +463 -0
- package/assets/opencode/skills/tdd-workflow/agents/openai.yaml +7 -0
- package/assets/opencode/skills/verification-loop/.openskills.json +7 -0
- package/assets/opencode/skills/verification-loop/SKILL.md +126 -0
- package/assets/opencode/skills/verification-loop/agents/openai.yaml +7 -0
- package/assets/opencode/skills/vite-patterns/SKILL.md +449 -0
- package/assets/opencode/skills/web-doc-search/SKILL.md +51 -0
- package/assets/opencode/templates/README.md +3 -0
- package/assets/opencode/templates/bros/adr.md +39 -0
- package/assets/opencode/templates/bros/delivery-report.md +71 -0
- package/assets/opencode/templates/bros/explorer-evidence-packet.md +51 -0
- package/assets/opencode/templates/bros/prd.md +72 -0
- package/assets/opencode/templates/bros/security-review.md +48 -0
- package/assets/opencode/templates/bros/status-board.md +33 -0
- package/assets/opencode/templates/bros/task-packet.md +94 -0
- package/assets/opencode/templates/bros/test-strategy.md +57 -0
- package/assets/opencode/templates/bros/ui-implementation-packet.md +64 -0
- package/assets/skills.manifest.json +650 -0
- package/assets/templates.manifest.json +55 -0
- package/bin/bros.mjs +122 -0
- package/docs/compatibility.md +9 -0
- package/docs/installation.md +66 -0
- package/docs/integrations/claude.md +5 -0
- package/docs/integrations/codex.md +5 -0
- package/docs/integrations/opencode.md +39 -0
- package/docs/migration/from-local-opencode-config.md +10 -0
- package/docs/release-process.md +11 -0
- package/docs/repository-structure.md +15 -0
- package/docs/roadmap.md +20 -0
- package/docs/security.md +18 -0
- package/docs/testing.md +9 -0
- package/examples/opencode/README.md +11 -0
- package/examples/opencode/opencode.example.jsonc +4 -0
- package/package.json +43 -0
- package/scripts/validate-assets.mjs +22 -0
- package/scripts/verify-no-secrets.mjs +38 -0
- package/src/plugin.mjs +98 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-scan
|
|
3
|
+
description: Scan your Claude Code configuration (.claude/ directory) for security vulnerabilities, misconfigurations, and injection risks using AgentShield. Checks CLAUDE.md, settings.json, MCP servers, hooks, and agent definitions.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Security Scan Skill
|
|
8
|
+
|
|
9
|
+
Audit your Claude Code configuration for security issues using [AgentShield](https://github.com/affaan-m/agentshield).
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- Setting up a new Claude Code project
|
|
14
|
+
- After modifying `.claude/settings.json`, `CLAUDE.md`, or MCP configs
|
|
15
|
+
- Before committing configuration changes
|
|
16
|
+
- When onboarding to a new repository with existing Claude Code configs
|
|
17
|
+
- Periodic security hygiene checks
|
|
18
|
+
|
|
19
|
+
## What It Scans
|
|
20
|
+
|
|
21
|
+
| File | Checks |
|
|
22
|
+
|------|--------|
|
|
23
|
+
| `CLAUDE.md` | Hardcoded secrets, auto-run instructions, prompt injection patterns |
|
|
24
|
+
| `settings.json` | Overly permissive allow lists, missing deny lists, dangerous bypass flags |
|
|
25
|
+
| `mcp.json` | Risky MCP servers, hardcoded env secrets, npx supply chain risks |
|
|
26
|
+
| `hooks/` | Command injection via interpolation, data exfiltration, silent error suppression |
|
|
27
|
+
| `agents/*.md` | Unrestricted tool access, prompt injection surface, missing model specs |
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
AgentShield must be installed. Check and install if needed:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Check if installed
|
|
35
|
+
npx ecc-agentshield --version
|
|
36
|
+
|
|
37
|
+
# Install globally (recommended)
|
|
38
|
+
npm install -g ecc-agentshield
|
|
39
|
+
|
|
40
|
+
# Or run directly via npx (no install needed)
|
|
41
|
+
npx ecc-agentshield scan .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Basic Scan
|
|
47
|
+
|
|
48
|
+
Run against the current project's `.claude/` directory:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Scan current project
|
|
52
|
+
npx ecc-agentshield scan
|
|
53
|
+
|
|
54
|
+
# Scan a specific path
|
|
55
|
+
npx ecc-agentshield scan --path /path/to/.claude
|
|
56
|
+
|
|
57
|
+
# Scan with minimum severity filter
|
|
58
|
+
npx ecc-agentshield scan --min-severity medium
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Output Formats
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Terminal output (default) — colored report with grade
|
|
65
|
+
npx ecc-agentshield scan
|
|
66
|
+
|
|
67
|
+
# JSON — for CI/CD integration
|
|
68
|
+
npx ecc-agentshield scan --format json
|
|
69
|
+
|
|
70
|
+
# Markdown — for documentation
|
|
71
|
+
npx ecc-agentshield scan --format markdown
|
|
72
|
+
|
|
73
|
+
# HTML — self-contained dark-theme report
|
|
74
|
+
npx ecc-agentshield scan --format html > security-report.html
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Auto-Fix
|
|
78
|
+
|
|
79
|
+
Apply safe fixes automatically (only fixes marked as auto-fixable):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx ecc-agentshield scan --fix
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This will:
|
|
86
|
+
- Replace hardcoded secrets with environment variable references
|
|
87
|
+
- Tighten wildcard permissions to scoped alternatives
|
|
88
|
+
- Never modify manual-only suggestions
|
|
89
|
+
|
|
90
|
+
### Opus 4.6 Deep Analysis
|
|
91
|
+
|
|
92
|
+
Run the adversarial three-agent pipeline for deeper analysis:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Requires ANTHROPIC_API_KEY
|
|
96
|
+
export ANTHROPIC_API_KEY=your-key
|
|
97
|
+
npx ecc-agentshield scan --opus --stream
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This runs:
|
|
101
|
+
1. **Attacker (Red Team)** — finds attack vectors
|
|
102
|
+
2. **Defender (Blue Team)** — recommends hardening
|
|
103
|
+
3. **Auditor (Final Verdict)** — synthesizes both perspectives
|
|
104
|
+
|
|
105
|
+
### Initialize Secure Config
|
|
106
|
+
|
|
107
|
+
Scaffold a new secure `.claude/` configuration from scratch:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx ecc-agentshield init
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Creates:
|
|
114
|
+
- `settings.json` with scoped permissions and deny list
|
|
115
|
+
- `CLAUDE.md` with security best practices
|
|
116
|
+
- `mcp.json` placeholder
|
|
117
|
+
|
|
118
|
+
### GitHub Action
|
|
119
|
+
|
|
120
|
+
Add to your CI pipeline:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
- uses: affaan-m/agentshield@v1
|
|
124
|
+
with:
|
|
125
|
+
path: '.'
|
|
126
|
+
min-severity: 'medium'
|
|
127
|
+
fail-on-findings: true
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Severity Levels
|
|
131
|
+
|
|
132
|
+
| Grade | Score | Meaning |
|
|
133
|
+
|-------|-------|---------|
|
|
134
|
+
| A | 90-100 | Secure configuration |
|
|
135
|
+
| B | 75-89 | Minor issues |
|
|
136
|
+
| C | 60-74 | Needs attention |
|
|
137
|
+
| D | 40-59 | Significant risks |
|
|
138
|
+
| F | 0-39 | Critical vulnerabilities |
|
|
139
|
+
|
|
140
|
+
## Interpreting Results
|
|
141
|
+
|
|
142
|
+
### Critical Findings (fix immediately)
|
|
143
|
+
- Hardcoded API keys or tokens in config files
|
|
144
|
+
- `Bash(*)` in the allow list (unrestricted shell access)
|
|
145
|
+
- Command injection in hooks via `${file}` interpolation
|
|
146
|
+
- Shell-running MCP servers
|
|
147
|
+
|
|
148
|
+
### High Findings (fix before production)
|
|
149
|
+
- Auto-run instructions in CLAUDE.md (prompt injection vector)
|
|
150
|
+
- Missing deny lists in permissions
|
|
151
|
+
- Agents with unnecessary Bash access
|
|
152
|
+
|
|
153
|
+
### Medium Findings (recommended)
|
|
154
|
+
- Silent error suppression in hooks (`2>/dev/null`, `|| true`)
|
|
155
|
+
- Missing PreToolUse security hooks
|
|
156
|
+
- `npx -y` auto-install in MCP server configs
|
|
157
|
+
|
|
158
|
+
### Info Findings (awareness)
|
|
159
|
+
- Missing descriptions on MCP servers
|
|
160
|
+
- Prohibitive instructions correctly flagged as good practice
|
|
161
|
+
|
|
162
|
+
## Links
|
|
163
|
+
|
|
164
|
+
- **GitHub**: [github.com/affaan-m/agentshield](https://github.com/affaan-m/agentshield)
|
|
165
|
+
- **npm**: [npmjs.com/package/ecc-agentshield](https://www.npmjs.com/package/ecc-agentshield)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: springboot-patterns
|
|
3
|
+
description: Spring Boot architecture patterns, REST API design, layered services, data access, caching, async processing, and logging. Use for Java Spring Boot backend work.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Spring Boot Development Patterns
|
|
8
|
+
|
|
9
|
+
Spring Boot architecture and API patterns for scalable, production-grade services.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- Building REST APIs with Spring MVC or WebFlux
|
|
14
|
+
- Structuring controller → service → repository layers
|
|
15
|
+
- Configuring Spring Data JPA, caching, or async processing
|
|
16
|
+
- Adding validation, exception handling, or pagination
|
|
17
|
+
- Setting up profiles for dev/staging/production environments
|
|
18
|
+
- Implementing event-driven patterns with Spring Events or Kafka
|
|
19
|
+
|
|
20
|
+
## REST API Structure
|
|
21
|
+
|
|
22
|
+
```java
|
|
23
|
+
@RestController
|
|
24
|
+
@RequestMapping("/api/markets")
|
|
25
|
+
@Validated
|
|
26
|
+
class MarketController {
|
|
27
|
+
private final MarketService marketService;
|
|
28
|
+
|
|
29
|
+
MarketController(MarketService marketService) {
|
|
30
|
+
this.marketService = marketService;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@GetMapping
|
|
34
|
+
ResponseEntity<Page<MarketResponse>> list(
|
|
35
|
+
@RequestParam(defaultValue = "0") int page,
|
|
36
|
+
@RequestParam(defaultValue = "20") int size) {
|
|
37
|
+
Page<Market> markets = marketService.list(PageRequest.of(page, size));
|
|
38
|
+
return ResponseEntity.ok(markets.map(MarketResponse::from));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@PostMapping
|
|
42
|
+
ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest request) {
|
|
43
|
+
Market market = marketService.create(request);
|
|
44
|
+
return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse.from(market));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Repository Pattern (Spring Data JPA)
|
|
50
|
+
|
|
51
|
+
```java
|
|
52
|
+
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
|
|
53
|
+
@Query("select m from MarketEntity m where m.status = :status order by m.volume desc")
|
|
54
|
+
List<MarketEntity> findActive(@Param("status") MarketStatus status, Pageable pageable);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Service Layer with Transactions
|
|
59
|
+
|
|
60
|
+
```java
|
|
61
|
+
@Service
|
|
62
|
+
public class MarketService {
|
|
63
|
+
private final MarketRepository repo;
|
|
64
|
+
|
|
65
|
+
public MarketService(MarketRepository repo) {
|
|
66
|
+
this.repo = repo;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Transactional
|
|
70
|
+
public Market create(CreateMarketRequest request) {
|
|
71
|
+
MarketEntity entity = MarketEntity.from(request);
|
|
72
|
+
MarketEntity saved = repo.save(entity);
|
|
73
|
+
return Market.from(saved);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## DTOs and Validation
|
|
79
|
+
|
|
80
|
+
```java
|
|
81
|
+
public record CreateMarketRequest(
|
|
82
|
+
@NotBlank @Size(max = 200) String name,
|
|
83
|
+
@NotBlank @Size(max = 2000) String description,
|
|
84
|
+
@NotNull @FutureOrPresent Instant endDate,
|
|
85
|
+
@NotEmpty List<@NotBlank String> categories) {}
|
|
86
|
+
|
|
87
|
+
public record MarketResponse(Long id, String name, MarketStatus status) {
|
|
88
|
+
static MarketResponse from(Market market) {
|
|
89
|
+
return new MarketResponse(market.id(), market.name(), market.status());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Exception Handling
|
|
95
|
+
|
|
96
|
+
```java
|
|
97
|
+
@ControllerAdvice
|
|
98
|
+
class GlobalExceptionHandler {
|
|
99
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
100
|
+
ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
|
|
101
|
+
String message = ex.getBindingResult().getFieldErrors().stream()
|
|
102
|
+
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
103
|
+
.collect(Collectors.joining(", "));
|
|
104
|
+
return ResponseEntity.badRequest().body(ApiError.validation(message));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@ExceptionHandler(AccessDeniedException.class)
|
|
108
|
+
ResponseEntity<ApiError> handleAccessDenied() {
|
|
109
|
+
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@ExceptionHandler(Exception.class)
|
|
113
|
+
ResponseEntity<ApiError> handleGeneric(Exception ex) {
|
|
114
|
+
// Log unexpected errors with stack traces
|
|
115
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
116
|
+
.body(ApiError.of("Internal server error"));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Caching
|
|
122
|
+
|
|
123
|
+
Requires `@EnableCaching` on a configuration class.
|
|
124
|
+
|
|
125
|
+
```java
|
|
126
|
+
@Service
|
|
127
|
+
public class MarketCacheService {
|
|
128
|
+
private final MarketRepository repo;
|
|
129
|
+
|
|
130
|
+
public MarketCacheService(MarketRepository repo) {
|
|
131
|
+
this.repo = repo;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Cacheable(value = "market", key = "#id")
|
|
135
|
+
public Market getById(Long id) {
|
|
136
|
+
return repo.findById(id)
|
|
137
|
+
.map(Market::from)
|
|
138
|
+
.orElseThrow(() -> new EntityNotFoundException("Market not found"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@CacheEvict(value = "market", key = "#id")
|
|
142
|
+
public void evict(Long id) {}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Async Processing
|
|
147
|
+
|
|
148
|
+
Requires `@EnableAsync` on a configuration class.
|
|
149
|
+
|
|
150
|
+
```java
|
|
151
|
+
@Service
|
|
152
|
+
public class NotificationService {
|
|
153
|
+
@Async
|
|
154
|
+
public CompletableFuture<Void> sendAsync(Notification notification) {
|
|
155
|
+
// send email/SMS
|
|
156
|
+
return CompletableFuture.completedFuture(null);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Logging (SLF4J)
|
|
162
|
+
|
|
163
|
+
```java
|
|
164
|
+
@Service
|
|
165
|
+
public class ReportService {
|
|
166
|
+
private static final Logger log = LoggerFactory.getLogger(ReportService.class);
|
|
167
|
+
|
|
168
|
+
public Report generate(Long marketId) {
|
|
169
|
+
log.info("generate_report marketId={}", marketId);
|
|
170
|
+
try {
|
|
171
|
+
// logic
|
|
172
|
+
} catch (Exception ex) {
|
|
173
|
+
log.error("generate_report_failed marketId={}", marketId, ex);
|
|
174
|
+
throw ex;
|
|
175
|
+
}
|
|
176
|
+
return new Report();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Middleware / Filters
|
|
182
|
+
|
|
183
|
+
```java
|
|
184
|
+
@Component
|
|
185
|
+
public class RequestLoggingFilter extends OncePerRequestFilter {
|
|
186
|
+
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
|
|
187
|
+
|
|
188
|
+
@Override
|
|
189
|
+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
|
190
|
+
FilterChain filterChain) throws ServletException, IOException {
|
|
191
|
+
long start = System.currentTimeMillis();
|
|
192
|
+
try {
|
|
193
|
+
filterChain.doFilter(request, response);
|
|
194
|
+
} finally {
|
|
195
|
+
long duration = System.currentTimeMillis() - start;
|
|
196
|
+
log.info("req method={} uri={} status={} durationMs={}",
|
|
197
|
+
request.getMethod(), request.getRequestURI(), response.getStatus(), duration);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Pagination and Sorting
|
|
204
|
+
|
|
205
|
+
```java
|
|
206
|
+
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
|
|
207
|
+
Page<Market> results = marketService.list(page);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Error-Resilient External Calls
|
|
211
|
+
|
|
212
|
+
```java
|
|
213
|
+
public <T> T withRetry(Supplier<T> supplier, int maxRetries) {
|
|
214
|
+
int attempts = 0;
|
|
215
|
+
while (true) {
|
|
216
|
+
try {
|
|
217
|
+
return supplier.get();
|
|
218
|
+
} catch (Exception ex) {
|
|
219
|
+
attempts++;
|
|
220
|
+
if (attempts >= maxRetries) {
|
|
221
|
+
throw ex;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
Thread.sleep((long) Math.pow(2, attempts) * 100L);
|
|
225
|
+
} catch (InterruptedException ie) {
|
|
226
|
+
Thread.currentThread().interrupt();
|
|
227
|
+
throw ex;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Rate Limiting (Filter + Bucket4j)
|
|
235
|
+
|
|
236
|
+
**Security Note**: The `X-Forwarded-For` header is untrusted by default because clients can spoof it.
|
|
237
|
+
Only use forwarded headers when:
|
|
238
|
+
1. Your app is behind a trusted reverse proxy (nginx, AWS ALB, etc.)
|
|
239
|
+
2. You have registered `ForwardedHeaderFilter` as a bean
|
|
240
|
+
3. You have configured `server.forward-headers-strategy=NATIVE` or `FRAMEWORK` in application properties
|
|
241
|
+
4. Your proxy is configured to overwrite (not append to) the `X-Forwarded-For` header
|
|
242
|
+
|
|
243
|
+
When `ForwardedHeaderFilter` is properly configured, `request.getRemoteAddr()` will automatically
|
|
244
|
+
return the correct client IP from the forwarded headers. Without this configuration, use
|
|
245
|
+
`request.getRemoteAddr()` directly—it returns the immediate connection IP, which is the only
|
|
246
|
+
trustworthy value.
|
|
247
|
+
|
|
248
|
+
```java
|
|
249
|
+
@Component
|
|
250
|
+
public class RateLimitFilter extends OncePerRequestFilter {
|
|
251
|
+
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
* SECURITY: This filter uses request.getRemoteAddr() to identify clients for rate limiting.
|
|
255
|
+
*
|
|
256
|
+
* If your application is behind a reverse proxy (nginx, AWS ALB, etc.), you MUST configure
|
|
257
|
+
* Spring to handle forwarded headers properly for accurate client IP detection:
|
|
258
|
+
*
|
|
259
|
+
* 1. Set server.forward-headers-strategy=NATIVE (for cloud platforms) or FRAMEWORK in
|
|
260
|
+
* application.properties/yaml
|
|
261
|
+
* 2. If using FRAMEWORK strategy, register ForwardedHeaderFilter:
|
|
262
|
+
*
|
|
263
|
+
* @Bean
|
|
264
|
+
* ForwardedHeaderFilter forwardedHeaderFilter() {
|
|
265
|
+
* return new ForwardedHeaderFilter();
|
|
266
|
+
* }
|
|
267
|
+
*
|
|
268
|
+
* 3. Ensure your proxy overwrites (not appends) the X-Forwarded-For header to prevent spoofing
|
|
269
|
+
* 4. Configure server.tomcat.remoteip.trusted-proxies or equivalent for your container
|
|
270
|
+
*
|
|
271
|
+
* Without this configuration, request.getRemoteAddr() returns the proxy IP, not the client IP.
|
|
272
|
+
* Do NOT read X-Forwarded-For directly—it is trivially spoofable without trusted proxy handling.
|
|
273
|
+
*/
|
|
274
|
+
@Override
|
|
275
|
+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
|
276
|
+
FilterChain filterChain) throws ServletException, IOException {
|
|
277
|
+
// Use getRemoteAddr() which returns the correct client IP when ForwardedHeaderFilter
|
|
278
|
+
// is configured, or the direct connection IP otherwise. Never trust X-Forwarded-For
|
|
279
|
+
// headers directly without proper proxy configuration.
|
|
280
|
+
String clientIp = request.getRemoteAddr();
|
|
281
|
+
|
|
282
|
+
Bucket bucket = buckets.computeIfAbsent(clientIp,
|
|
283
|
+
k -> Bucket.builder()
|
|
284
|
+
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
|
|
285
|
+
.build());
|
|
286
|
+
|
|
287
|
+
if (bucket.tryConsume(1)) {
|
|
288
|
+
filterChain.doFilter(request, response);
|
|
289
|
+
} else {
|
|
290
|
+
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Background Jobs
|
|
297
|
+
|
|
298
|
+
Use Spring’s `@Scheduled` or integrate with queues (e.g., Kafka, SQS, RabbitMQ). Keep handlers idempotent and observable.
|
|
299
|
+
|
|
300
|
+
## Observability
|
|
301
|
+
|
|
302
|
+
- Structured logging (JSON) via Logback encoder
|
|
303
|
+
- Metrics: Micrometer + Prometheus/OTel
|
|
304
|
+
- Tracing: Micrometer Tracing with OpenTelemetry or Brave backend
|
|
305
|
+
|
|
306
|
+
## Production Defaults
|
|
307
|
+
|
|
308
|
+
- Prefer constructor injection, avoid field injection
|
|
309
|
+
- Enable `spring.mvc.problemdetails.enabled=true` for RFC 7807 errors (Spring Boot 3+)
|
|
310
|
+
- Configure HikariCP pool sizes for workload, set timeouts
|
|
311
|
+
- Use `@Transactional(readOnly = true)` for queries
|
|
312
|
+
- Enforce null-safety via `@NonNull` and `Optional` where appropriate
|
|
313
|
+
|
|
314
|
+
**Remember**: Keep controllers thin, services focused, repositories simple, and errors handled centrally. Optimize for maintainability and testability.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: springboot-tdd
|
|
3
|
+
description: Test-driven development for Spring Boot using JUnit 5, Mockito, MockMvc, Testcontainers, and JaCoCo. Use when adding features, fixing bugs, or refactoring.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Spring Boot TDD Workflow
|
|
8
|
+
|
|
9
|
+
TDD guidance for Spring Boot services with 80%+ coverage (unit + integration).
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- New features or endpoints
|
|
14
|
+
- Bug fixes or refactors
|
|
15
|
+
- Adding data access logic or security rules
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
1) Write tests first (they should fail)
|
|
20
|
+
2) Implement minimal code to pass
|
|
21
|
+
3) Refactor with tests green
|
|
22
|
+
4) Enforce coverage (JaCoCo)
|
|
23
|
+
|
|
24
|
+
## Unit Tests (JUnit 5 + Mockito)
|
|
25
|
+
|
|
26
|
+
```java
|
|
27
|
+
@ExtendWith(MockitoExtension.class)
|
|
28
|
+
class MarketServiceTest {
|
|
29
|
+
@Mock MarketRepository repo;
|
|
30
|
+
@InjectMocks MarketService service;
|
|
31
|
+
|
|
32
|
+
@Test
|
|
33
|
+
void createsMarket() {
|
|
34
|
+
CreateMarketRequest req = new CreateMarketRequest("name", "desc", Instant.now(), List.of("cat"));
|
|
35
|
+
when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
36
|
+
|
|
37
|
+
Market result = service.create(req);
|
|
38
|
+
|
|
39
|
+
assertThat(result.name()).isEqualTo("name");
|
|
40
|
+
verify(repo).save(any());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Patterns:
|
|
46
|
+
- Arrange-Act-Assert
|
|
47
|
+
- Avoid partial mocks; prefer explicit stubbing
|
|
48
|
+
- Use `@ParameterizedTest` for variants
|
|
49
|
+
|
|
50
|
+
## Web Layer Tests (MockMvc)
|
|
51
|
+
|
|
52
|
+
```java
|
|
53
|
+
@WebMvcTest(MarketController.class)
|
|
54
|
+
class MarketControllerTest {
|
|
55
|
+
@Autowired MockMvc mockMvc;
|
|
56
|
+
@MockBean MarketService marketService;
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
void returnsMarkets() throws Exception {
|
|
60
|
+
when(marketService.list(any())).thenReturn(Page.empty());
|
|
61
|
+
|
|
62
|
+
mockMvc.perform(get("/api/markets"))
|
|
63
|
+
.andExpect(status().isOk())
|
|
64
|
+
.andExpect(jsonPath("$.content").isArray());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Integration Tests (SpringBootTest)
|
|
70
|
+
|
|
71
|
+
```java
|
|
72
|
+
@SpringBootTest
|
|
73
|
+
@AutoConfigureMockMvc
|
|
74
|
+
@ActiveProfiles("test")
|
|
75
|
+
class MarketIntegrationTest {
|
|
76
|
+
@Autowired MockMvc mockMvc;
|
|
77
|
+
|
|
78
|
+
@Test
|
|
79
|
+
void createsMarket() throws Exception {
|
|
80
|
+
mockMvc.perform(post("/api/markets")
|
|
81
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
82
|
+
.content("""
|
|
83
|
+
{"name":"Test","description":"Desc","endDate":"2030-01-01T00:00:00Z","categories":["general"]}
|
|
84
|
+
"""))
|
|
85
|
+
.andExpect(status().isCreated());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Persistence Tests (DataJpaTest)
|
|
91
|
+
|
|
92
|
+
```java
|
|
93
|
+
@DataJpaTest
|
|
94
|
+
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
|
95
|
+
@Import(TestContainersConfig.class)
|
|
96
|
+
class MarketRepositoryTest {
|
|
97
|
+
@Autowired MarketRepository repo;
|
|
98
|
+
|
|
99
|
+
@Test
|
|
100
|
+
void savesAndFinds() {
|
|
101
|
+
MarketEntity entity = new MarketEntity();
|
|
102
|
+
entity.setName("Test");
|
|
103
|
+
repo.save(entity);
|
|
104
|
+
|
|
105
|
+
Optional<MarketEntity> found = repo.findByName("Test");
|
|
106
|
+
assertThat(found).isPresent();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Testcontainers
|
|
112
|
+
|
|
113
|
+
- Use reusable containers for Postgres/Redis to mirror production
|
|
114
|
+
- Wire via `@DynamicPropertySource` to inject JDBC URLs into Spring context
|
|
115
|
+
|
|
116
|
+
## Coverage (JaCoCo)
|
|
117
|
+
|
|
118
|
+
Maven snippet:
|
|
119
|
+
```xml
|
|
120
|
+
<plugin>
|
|
121
|
+
<groupId>org.jacoco</groupId>
|
|
122
|
+
<artifactId>jacoco-maven-plugin</artifactId>
|
|
123
|
+
<version>0.8.14</version>
|
|
124
|
+
<executions>
|
|
125
|
+
<execution>
|
|
126
|
+
<goals><goal>prepare-agent</goal></goals>
|
|
127
|
+
</execution>
|
|
128
|
+
<execution>
|
|
129
|
+
<id>report</id>
|
|
130
|
+
<phase>verify</phase>
|
|
131
|
+
<goals><goal>report</goal></goals>
|
|
132
|
+
</execution>
|
|
133
|
+
</executions>
|
|
134
|
+
</plugin>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Assertions
|
|
138
|
+
|
|
139
|
+
- Prefer AssertJ (`assertThat`) for readability
|
|
140
|
+
- For JSON responses, use `jsonPath`
|
|
141
|
+
- For exceptions: `assertThatThrownBy(...)`
|
|
142
|
+
|
|
143
|
+
## Test Data Builders
|
|
144
|
+
|
|
145
|
+
```java
|
|
146
|
+
class MarketBuilder {
|
|
147
|
+
private String name = "Test";
|
|
148
|
+
MarketBuilder withName(String name) { this.name = name; return this; }
|
|
149
|
+
Market build() { return new Market(null, name, MarketStatus.ACTIVE); }
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## CI Commands
|
|
154
|
+
|
|
155
|
+
- Maven: `mvn -T 4 test` or `mvn verify`
|
|
156
|
+
- Gradle: `./gradlew test jacocoTestReport`
|
|
157
|
+
|
|
158
|
+
**Remember**: Keep tests fast, isolated, and deterministic. Test behavior, not implementation details.
|