geneseed 4.9.4 → 4.14.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 +106 -0
- package/README.md +16 -2
- package/SHIPPED.md +1 -1
- package/docs/README.md +1 -1
- package/docs/concepts/collaboration.md +1 -1
- package/docs/concepts/enforcement-table.md +1 -1
- package/docs/concepts/foreman-mode.md +1 -1
- package/docs/concepts/kits.md +91 -0
- package/docs/concepts/loop-branch.md +1 -1
- package/docs/concepts/loop-bricks.md +1 -1
- package/docs/concepts/loop-contracts.md +1 -1
- package/docs/concepts/loop-gates.md +1 -1
- package/docs/concepts/loop-notes.md +1 -1
- package/docs/concepts/loop-risk.md +1 -1
- package/docs/concepts/loop-templates.md +1 -1
- package/docs/concepts/loops.md +1 -1
- package/docs/concepts/lsp.md +1 -1
- package/docs/concepts/skills.md +1 -1
- package/docs/extending.md +107 -4
- package/docs/guides/install-scripted.md +1 -1
- package/docs/guides/run-anywhere.md +2 -2
- package/docs/guides/upgrade.md +2 -2
- package/docs/reference/glossary.md +1 -0
- package/docs/reference/troubleshoot-install.md +1 -1
- package/docs/web-ui.md +9 -0
- package/harness.config.json +1 -1
- package/js/build/args.mjs +27 -6
- package/js/build/driver.mjs +29 -8
- package/js/build/emit-claude.mjs +3 -1
- package/js/build/generate.mjs +10 -5
- package/js/build/render.mjs +94 -11
- package/js/build/source.mjs +117 -4
- package/js/cli-table.json +15 -0
- package/js/hosts/installs.mjs +35 -2
- package/js/hosts/link.mjs +88 -1
- package/js/hosts/native.mjs +14 -4
- package/js/inspect/checks-authoring.mjs +6 -3
- package/js/inspect/checks-build.mjs +15 -3
- package/js/inspect/checks-counts.mjs +153 -6
- package/js/inspect/checks-repo.mjs +95 -20
- package/js/inspect/cost.mjs +1 -1
- package/js/inspect/diff.mjs +56 -8
- package/js/inspect/inventory.mjs +2 -0
- package/js/inspect/status.mjs +44 -3
- package/js/inspect/validate.mjs +3 -3
- package/js/lib/prompt.mjs +19 -2
- package/js/maintain/setup.mjs +111 -23
- package/js/maintain/update.mjs +3 -3
- package/js/web/actions.mjs +38 -6
- package/js/web/api.mjs +76 -17
- package/js/web/catalog.mjs +17 -2
- package/js/web/handler.mjs +1 -0
- package/js/web/routes.mjs +2 -1
- package/js/web/server.mjs +11 -5
- package/js/web/state.mjs +38 -3
- package/package.json +1 -1
- package/registry.json +91 -0
- package/src/AGENT.md.tmpl +7 -7
- package/src/agents/_self-improvement.md +11 -0
- package/src/agents/_template.md +1 -8
- package/src/agents/advocate.md +1 -8
- package/src/agents/architect.md +1 -8
- package/src/agents/developer.md +1 -8
- package/src/agents/devops-engineer.md +37 -0
- package/src/agents/docker-expert.md +37 -0
- package/src/agents/docs.md +1 -8
- package/src/agents/empiricist.md +1 -8
- package/src/agents/explorer.md +1 -8
- package/src/agents/framer.md +1 -8
- package/src/agents/historian.md +1 -8
- package/src/agents/kubernetes-specialist.md +37 -0
- package/src/agents/memory-curator.md +1 -8
- package/src/agents/operator.md +1 -8
- package/src/agents/performance.md +1 -8
- package/src/agents/pragmatist.md +1 -8
- package/src/agents/react-engineer.md +38 -0
- package/src/agents/react-reviewer.md +42 -0
- package/src/agents/researcher.md +5 -10
- package/src/agents/reviewer.md +3 -8
- package/src/agents/security.md +1 -8
- package/src/agents/skeptic.md +1 -8
- package/src/agents/spring-boot-engineer.md +40 -0
- package/src/agents/spring-reviewer.md +41 -0
- package/src/agents/spring-security.md +42 -0
- package/src/agents/steward.md +1 -8
- package/src/agents/tester.md +1 -8
- package/src/agents/user-advocate.md +1 -8
- package/src/agents/visionary.md +1 -8
- package/src/kits/react/rules.md +13 -0
- package/src/kits/react.json +18 -0
- package/src/kits/spring-boot/rules.md +10 -0
- package/src/kits/spring-boot.json +23 -0
- package/src/licenses/everything-claude-code.txt +21 -0
- package/src/licenses/mattpocock-skills.txt +21 -0
- package/src/licenses/piomin-spring-boot.txt +190 -0
- package/src/licenses/superpowers.txt +21 -0
- package/src/licenses/vibe-wise.txt +21 -0
- package/src/skills/_self-improvement.md +22 -5
- package/src/skills/brainstorm/builder-check.md +2 -0
- package/src/skills/brainstorm.md +11 -2
- package/src/skills/codebase-design.md +10 -2
- package/src/skills/daydream/VENDOR.md +7 -7
- package/src/skills/debug.md +6 -2
- package/src/skills/develop.md +6 -2
- package/src/skills/geneseed-code-review.md +6 -2
- package/src/skills/git-history.md +2 -0
- package/src/skills/handoff.md +3 -1
- package/src/skills/learn-mode.md +3 -2
- package/src/skills/parallel-agents.md +5 -1
- package/src/skills/pipeline.md +2 -0
- package/src/skills/plan.md +9 -2
- package/src/skills/prototype.md +6 -2
- package/src/skills/react/a11y.md +33 -0
- package/src/skills/react/vite.md +35 -0
- package/src/skills/react/webpack.md +40 -0
- package/src/skills/react-testing.md +68 -0
- package/src/skills/react-view-transitions/AGENTS.md +646 -221
- package/src/skills/react-view-transitions/SKILL.md +28 -16
- package/src/skills/react-view-transitions/VENDOR.md +10 -8
- package/src/skills/react-view-transitions/references/css-recipes.md +72 -2
- package/src/skills/react-view-transitions/references/implementation.md +14 -26
- package/src/skills/react-view-transitions/references/nextjs.md +94 -37
- package/src/skills/react-view-transitions/references/patterns.md +60 -48
- package/src/skills/react-view-transitions/references/troubleshooting.md +47 -0
- package/src/skills/react.md +63 -0
- package/src/skills/research.md +6 -2
- package/src/skills/review-response.md +2 -0
- package/src/skills/rule.md +6 -2
- package/src/skills/ship.md +8 -4
- package/src/skills/skill-forge.md +11 -5
- package/src/skills/spring-boot/cloud.md +103 -0
- package/src/skills/spring-boot/java-modern.md +71 -0
- package/src/skills/spring-boot/logging.md +89 -0
- package/src/skills/spring-boot/security.md +112 -0
- package/src/skills/spring-boot/web.md +112 -0
- package/src/skills/spring-boot.md +57 -0
- package/src/skills/spring-data-jpa/patterns.md +68 -0
- package/src/skills/spring-data-jpa.md +81 -0
- package/src/skills/spring-testing.md +93 -0
- package/src/skills/wayfinder.md +8 -2
- package/src/skills/wiki.md +18 -5
- package/src/upstreams.json +66 -0
- package/themes/_TEMPLATE.json +13 -0
- package/themes/biker.json +13 -0
- package/themes/commentator.json +13 -0
- package/themes/cyberpunk.json +13 -0
- package/themes/gamer.json +13 -0
- package/themes/imperial.json +13 -0
- package/themes/joker.json +13 -0
- package/themes/marvin.json +13 -0
- package/themes/mean.json +13 -0
- package/themes/military.json +13 -0
- package/themes/neutral.json +13 -0
- package/themes/pirate.json +13 -0
- package/themes/sports.json +13 -0
- package/themes/verstappen.json +13 -0
- package/themes/wizard.json +13 -0
- package/web/dist/assets/Activity-44rbWUQI.js +1 -0
- package/web/dist/assets/ActivityDetail-Dk9k893a.js +1 -0
- package/web/dist/assets/Dashboard-DCoUN60W.js +2 -0
- package/web/dist/assets/{Docs-tDW7i7mo.js → Docs-C_je2lRG.js} +3 -3
- package/web/dist/assets/FilterInput-CZREvrjX.js +1 -0
- package/web/dist/assets/GreenhouseView-BXAOIznW.js +2 -0
- package/web/dist/assets/Installs-DAXF403D.js +3 -0
- package/web/dist/assets/JournalView-DZ184wKB.js +1 -0
- package/web/dist/assets/KitApply-pjhvGha0.js +1 -0
- package/web/dist/assets/Kits-BCCKjSIY.js +1 -0
- package/web/dist/assets/{Laws-BA3CzUf5.js → Laws-BgBUpRkE.js} +1 -1
- package/web/dist/assets/Library-CW0Zl6RF.js +1 -0
- package/web/dist/assets/Loops-BvFOzjEw.js +2 -0
- package/web/dist/assets/{Markdown-iHvBrpRN.js → Markdown-zaXXXHz9.js} +13 -13
- package/web/dist/assets/OperatorHudView-Bs4unPNi.js +2 -0
- package/web/dist/assets/Personal-C23XaT-a.js +1 -0
- package/web/dist/assets/Tabs-b2XFu4OS.js +1 -0
- package/web/dist/assets/activity-Jw8XcPiP.js +1 -0
- package/web/dist/assets/api-BfpZmUMI.js +1 -0
- package/web/dist/assets/{drawable-CyLe1gtW.js → drawable-BERkKhx_.js} +1 -1
- package/web/dist/assets/index-C9R_bRw_.js +2 -0
- package/web/dist/assets/{index-BCJz4_8C.css → index-jYlWihN-.css} +1 -1
- package/web/dist/assets/{react-BOF6yL40.js → react-DlYQp10C.js} +1 -1
- package/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/web/dist/assets/sections-ChwaobfW.js +1 -0
- package/web/dist/assets/usePoll-bDFN-Hr6.js +1 -0
- package/web/dist/index.html +6 -5
- package/web/dist/assets/Activity-C8sZVIUU.js +0 -1
- package/web/dist/assets/ActivityDetail-Cc6vWUS4.js +0 -1
- package/web/dist/assets/Dashboard-C5ATa3ow.js +0 -2
- package/web/dist/assets/FilterInput-zLFXoSt6.js +0 -1
- package/web/dist/assets/GreenhouseView-zk2Uzc3A.js +0 -2
- package/web/dist/assets/Installs-CB_rvbBP.js +0 -3
- package/web/dist/assets/JournalView-Bfj27XHC.js +0 -1
- package/web/dist/assets/Library-BWwiy9Vb.js +0 -1
- package/web/dist/assets/Loops-DaPk-ckL.js +0 -2
- package/web/dist/assets/OperatorHudView-CyiGhiHk.js +0 -2
- package/web/dist/assets/Personal-Bwd27CmT.js +0 -1
- package/web/dist/assets/Tabs-BOD7Kaz1.js +0 -1
- package/web/dist/assets/activity-DvPxoS4Y.js +0 -1
- package/web/dist/assets/api-CiXlkbe0.js +0 -1
- package/web/dist/assets/index-DQXG_mqo.js +0 -2
- package/web/dist/assets/sections-BET3c1WT.js +0 -1
- package/web/dist/assets/usePoll-NSTzLOF4.js +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# {{SKILL}}: spring-boot — logging
|
|
2
|
+
|
|
3
|
+
{{CREDIT:piomin-spring-boot:skills/logging-patterns/SKILL.md}}
|
|
4
|
+
|
|
5
|
+
SLF4J through Spring Boot's logging, with structured (JSON) output where logs are machine-read. Structured output is a good default for deployed environments and CI because fields parse directly (`jq`, log platforms, an AI assistant reading logs); keep readable text for local development.
|
|
6
|
+
|
|
7
|
+
## Structured logging (built in)
|
|
8
|
+
No extra dependency. Pick a format per target:
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
logging:
|
|
12
|
+
structured:
|
|
13
|
+
format:
|
|
14
|
+
console: logstash # also: ecs, gelf
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`logging.structured.format.file` does the same for file output. A custom Logback or Log4j2 config must use Boot's structured encoder or layout (`StructuredLogEncoder` / `StructuredLogLayout` fed by `CONSOLE_LOG_STRUCTURED_FORMAT`), or the property has no effect.
|
|
18
|
+
|
|
19
|
+
Profile split so developers keep readable logs:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
---
|
|
23
|
+
spring:
|
|
24
|
+
config:
|
|
25
|
+
activate:
|
|
26
|
+
on-profile: "!local"
|
|
27
|
+
logging:
|
|
28
|
+
structured:
|
|
29
|
+
format:
|
|
30
|
+
console: logstash
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run locally with the `local` profile for plain text. With Micrometer Tracing present, Boot adds `[traceId-spanId]` to every log line by default; change the format with `logging.pattern.correlation`. Whether they appear as JSON fields depends on the format; confirm in a real log line.
|
|
34
|
+
|
|
35
|
+
## Writing log statements
|
|
36
|
+
```java
|
|
37
|
+
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
|
|
38
|
+
|
|
39
|
+
log.info("Order created id={} customer={}", order.id(), order.customerId()); // parameterized
|
|
40
|
+
log.error("Payment failed for order {}", order.id(), e); // exception last, stack trace kept
|
|
41
|
+
```
|
|
42
|
+
- Placeholders, never string concatenation. Guard only genuinely expensive arguments with `isDebugEnabled()`.
|
|
43
|
+
- Pass the exception as the final argument; do not log `e.getMessage()` alone.
|
|
44
|
+
- Log an exception once, where it is handled, not at every layer it passes through.
|
|
45
|
+
- Never log secrets, tokens, passwords or personal data; log identifiers.
|
|
46
|
+
- Lombok's `@Slf4j` is fine where the project uses Lombok; otherwise declare the logger as above.
|
|
47
|
+
|
|
48
|
+
| Level | Meaning |
|
|
49
|
+
|-------|---------|
|
|
50
|
+
| ERROR | Something failed and needs attention |
|
|
51
|
+
| WARN | Unexpected but handled (fallback used, retry succeeded) |
|
|
52
|
+
| INFO | Business events (order created, job finished) |
|
|
53
|
+
| DEBUG | Technical detail (queries, outbound calls) |
|
|
54
|
+
| TRACE | Verbose internals |
|
|
55
|
+
|
|
56
|
+
## MDC for correlation
|
|
57
|
+
MDC attaches key-value context to every log line written by the current thread. Set it at the edge in a filter and always clear it in `finally`.
|
|
58
|
+
|
|
59
|
+
```java
|
|
60
|
+
@Component
|
|
61
|
+
@Order(Ordered.HIGHEST_PRECEDENCE)
|
|
62
|
+
class MdcFilter extends OncePerRequestFilter {
|
|
63
|
+
@Override
|
|
64
|
+
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
|
|
65
|
+
throws ServletException, IOException {
|
|
66
|
+
String header = req.getHeader("X-Request-Id");
|
|
67
|
+
// client-supplied: accept only a short, safe value
|
|
68
|
+
String id = header != null && header.matches("[A-Za-z0-9-]{1,64}") ? header : UUID.randomUUID().toString();
|
|
69
|
+
MDC.put("requestId", id);
|
|
70
|
+
try {
|
|
71
|
+
chain.doFilter(req, res);
|
|
72
|
+
} finally {
|
|
73
|
+
MDC.remove("requestId");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
MDC is thread-local, so it does not follow work handed to another thread (virtual or platform). For executor tasks, copy the context with a `TaskDecorator` (`MDC.getCopyOfContextMap()` before, `MDC.setContextMap(...)` inside the task, `MDC.clear()` in `finally`) and register it on the executor you use; check that Boot applies it to your executor in the version you run.
|
|
80
|
+
|
|
81
|
+
Useful fields for incident analysis: `requestId`, user or tenant id, and a duration on slow-path logs. Keep them to identifiers, not payloads.
|
|
82
|
+
|
|
83
|
+
## Quick reference
|
|
84
|
+
| Need | Use |
|
|
85
|
+
|------|-----|
|
|
86
|
+
| JSON console | `logging.structured.format.console: logstash` |
|
|
87
|
+
| Per-package level | `logging.level.com.example.orders: DEBUG` |
|
|
88
|
+
| Correlate a request | MDC `requestId` set in a filter |
|
|
89
|
+
| Follow one request in JSON logs | `jq 'select(.requestId == "abc")' app.log` (field name depends on MDC key and format) |
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# {{SKILL}}: spring-boot — security
|
|
2
|
+
|
|
3
|
+
{{CREDIT:piomin-spring-boot:skills/spring-boot/references/security.md}}
|
|
4
|
+
|
|
5
|
+
Spring Security 7. Default posture for an API: a stateless OAuth2 resource server that validates JWTs issued by an identity provider. Do not write your own token issuer, JWT filter or password store unless the task is explicitly to build an authorization server; that code is where the security bugs live.
|
|
6
|
+
|
|
7
|
+
## Resource server
|
|
8
|
+
Properties first (Boot builds the decoder from them):
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
spring:
|
|
12
|
+
security:
|
|
13
|
+
oauth2:
|
|
14
|
+
resourceserver:
|
|
15
|
+
jwt:
|
|
16
|
+
issuer-uri: https://auth.example.com
|
|
17
|
+
audiences:
|
|
18
|
+
- orders-api
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```java
|
|
22
|
+
@Configuration
|
|
23
|
+
@EnableWebSecurity
|
|
24
|
+
@EnableMethodSecurity
|
|
25
|
+
class SecurityConfig {
|
|
26
|
+
|
|
27
|
+
@Bean
|
|
28
|
+
SecurityFilterChain api(HttpSecurity http) throws Exception {
|
|
29
|
+
http
|
|
30
|
+
.cors(Customizer.withDefaults())
|
|
31
|
+
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
32
|
+
.authorizeHttpRequests(auth -> auth
|
|
33
|
+
.requestMatchers("/actuator/health/**").permitAll()
|
|
34
|
+
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
35
|
+
.anyRequest().authenticated())
|
|
36
|
+
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
|
|
37
|
+
return http.build();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Lambda DSL only; `.and()` chaining and `authorizeRequests` are gone in Security 7.
|
|
43
|
+
- Disabling CSRF is acceptable only for a stateless, bearer-token API with no cookie-based session. If any endpoint authenticates with a cookie, keep CSRF on.
|
|
44
|
+
- `cors(Customizer.withDefaults())` picks up a `CorsConfigurationSource` bean; define that bean with explicit origins.
|
|
45
|
+
- Default JWT algorithm trust is RS256 only; widen it deliberately if the provider signs otherwise.
|
|
46
|
+
|
|
47
|
+
## Mapping claims to authorities
|
|
48
|
+
The default converter maps the `scope` claim to `SCOPE_` authorities. For roles in another claim:
|
|
49
|
+
|
|
50
|
+
```java
|
|
51
|
+
@Bean
|
|
52
|
+
JwtAuthenticationConverter jwtAuthenticationConverter() {
|
|
53
|
+
var authorities = new JwtGrantedAuthoritiesConverter();
|
|
54
|
+
authorities.setAuthoritiesClaimName("roles");
|
|
55
|
+
authorities.setAuthorityPrefix("ROLE_");
|
|
56
|
+
var converter = new JwtAuthenticationConverter();
|
|
57
|
+
converter.setJwtGrantedAuthoritiesConverter(authorities);
|
|
58
|
+
return converter;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Wire it with `oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))`.
|
|
63
|
+
|
|
64
|
+
## Validate audience
|
|
65
|
+
An issuer check alone accepts any token that issuer minted for any API. The `audiences` property in the YAML above makes Boot require the `aud` claim. Define your own `JwtDecoder` bean only when you need custom validators; it replaces the property-driven decoder, audiences included.
|
|
66
|
+
|
|
67
|
+
## Method security
|
|
68
|
+
`@EnableMethodSecurity` turns on `@PreAuthorize` and `@PostAuthorize`. Authorize in the service layer, next to the data, not only on URL patterns. The legacy Access API (`AccessDecisionManager`, voters) moved to the separate `spring-security-access` module; `@Secured` still works via `@EnableMethodSecurity(securedEnabled = true)`, but prefer `@PreAuthorize`.
|
|
69
|
+
|
|
70
|
+
```java
|
|
71
|
+
@PreAuthorize("hasRole('ADMIN') or #ownerId == authentication.name")
|
|
72
|
+
public Order find(String ownerId, Long id) { ... }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Testing
|
|
76
|
+
Use the `jwt()` request post processor with MockMvc (`SecurityMockMvcRequestPostProcessors.jwt()`; default token has `scope=read`), or `@WithMockUser` for method security. Always test the 401 (no token) and 403 (wrong authority) paths, not only success.
|
|
77
|
+
|
|
78
|
+
## TLS with SSL bundles
|
|
79
|
+
Configure server and client TLS with SSL bundles (named key material). Reloading works only when the consuming component supports it (Tomcat and Netty servers) and only through the file watcher, so the material must be on the file system with a `file:` location; a `classpath:` bundle cannot reload.
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
spring:
|
|
83
|
+
ssl:
|
|
84
|
+
bundle:
|
|
85
|
+
pem:
|
|
86
|
+
server:
|
|
87
|
+
reload-on-update: true
|
|
88
|
+
keystore:
|
|
89
|
+
certificate: "file:/etc/ssl/app/tls.crt"
|
|
90
|
+
private-key: "file:/etc/ssl/app/tls.key"
|
|
91
|
+
server:
|
|
92
|
+
ssl:
|
|
93
|
+
bundle: server
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Tune detection with `spring.ssl.bundle.watch.file.quiet-period` (default 10s). Apply a bundle to an outbound `RestClient` with the `RestClientSsl` helper (it replaces any request factory you set earlier; use a separate `client` bundle with a `truststore` for the upstream, not the server's keystore bundle), or build a factory when you need more than SSL:
|
|
97
|
+
|
|
98
|
+
```java
|
|
99
|
+
RestClient client(RestClient.Builder builder, RestClientSsl ssl) {
|
|
100
|
+
return builder.baseUrl("https://internal-service").apply(ssl.fromBundle("client")).build();
|
|
101
|
+
}
|
|
102
|
+
// more than SSL:
|
|
103
|
+
// var settings = HttpClientSettings.ofSslBundle(bundles.getBundle("client")).withReadTimeout(Duration.ofSeconds(5));
|
|
104
|
+
// builder.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Checklist
|
|
108
|
+
- HTTPS everywhere outside local dev; secrets from env or mounted files ([spring-boot/cloud.md](cloud.md) covers config trees).
|
|
109
|
+
- Validate issuer and audience; keep token lifetimes short; never log tokens.
|
|
110
|
+
- Passwords (if you must store any): `PasswordEncoder` from `PasswordEncoderFactories`/BCrypt, never a plain hash.
|
|
111
|
+
- Rate-limit authentication endpoints at the gateway or proxy.
|
|
112
|
+
- Ask the `spring-security` agent for review (it hands the generic audit to `security`) when the change touches authentication or authorization.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# {{SKILL}}: spring-boot — web layer
|
|
2
|
+
|
|
3
|
+
{{CREDIT:piomin-spring-boot:skills/spring-boot/references/web.md}}
|
|
4
|
+
|
|
5
|
+
Controllers, validation, error handling and outbound HTTP for a Spring MVC service.
|
|
6
|
+
|
|
7
|
+
## Controller
|
|
8
|
+
Thin: bind, validate, delegate, shape the response. Create returns `201` with a `Location` header; delete returns `204`.
|
|
9
|
+
|
|
10
|
+
```java
|
|
11
|
+
@RestController
|
|
12
|
+
@RequestMapping("/api/v1/users")
|
|
13
|
+
class UserController {
|
|
14
|
+
private final UserService service;
|
|
15
|
+
|
|
16
|
+
UserController(UserService service) { this.service = service; }
|
|
17
|
+
|
|
18
|
+
@GetMapping
|
|
19
|
+
PagedModel<UserResponse> list(@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
|
|
20
|
+
return new PagedModel<>(service.findAll(pageable));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@PostMapping
|
|
24
|
+
ResponseEntity<UserResponse> create(@Valid @RequestBody UserCreateRequest request) {
|
|
25
|
+
UserResponse user = service.create(request);
|
|
26
|
+
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
|
|
27
|
+
.path("/{id}").buildAndExpand(user.id()).toUri();
|
|
28
|
+
return ResponseEntity.created(location).body(user);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@DeleteMapping("/{id}")
|
|
32
|
+
@ResponseStatus(HttpStatus.NO_CONTENT)
|
|
33
|
+
void delete(@PathVariable Long id) { service.delete(id); }
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Version the path (`/api/v1`). Use GET for reads, POST/PUT/PATCH/DELETE for state changes; never change state on GET.
|
|
38
|
+
|
|
39
|
+
## DTOs and validation
|
|
40
|
+
Records for request and response. Constraints sit on the record components (`jakarta.validation.constraints`); `@Valid` on the controller parameter triggers them. Put the entity-to-DTO mapping in a static factory or a mapper, not in the controller.
|
|
41
|
+
|
|
42
|
+
```java
|
|
43
|
+
public record UserCreateRequest(
|
|
44
|
+
@NotBlank @Email String email,
|
|
45
|
+
@NotBlank @Size(min = 8, max = 100) String password,
|
|
46
|
+
@Min(18) Integer age) {}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A custom constraint is an annotation with `@Constraint(validatedBy = ...)` plus a `ConstraintValidator`; the validator may be a Spring bean and inject a repository. A uniqueness check in a validator is advisory only; the database constraint is the truth, so also handle `DataIntegrityViolationException` (409).
|
|
50
|
+
|
|
51
|
+
## Errors: Problem Details
|
|
52
|
+
Enable RFC 9457 responses for Spring's own exceptions:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
spring:
|
|
56
|
+
mvc:
|
|
57
|
+
problemdetails:
|
|
58
|
+
enabled: true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Return `ProblemDetail` from your own handlers so every error shares one shape. Extending `ResponseEntityExceptionHandler` keeps the framework exceptions on that shape too.
|
|
62
|
+
|
|
63
|
+
```java
|
|
64
|
+
@RestControllerAdvice
|
|
65
|
+
class ApiExceptionHandler extends ResponseEntityExceptionHandler {
|
|
66
|
+
|
|
67
|
+
@ExceptionHandler(ResourceNotFoundException.class)
|
|
68
|
+
ProblemDetail notFound(ResourceNotFoundException ex) {
|
|
69
|
+
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@ExceptionHandler(DataIntegrityViolationException.class)
|
|
73
|
+
ProblemDetail conflict(DataIntegrityViolationException ex) {
|
|
74
|
+
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, "Resource already exists");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A domain exception can carry its own problem by extending `ErrorResponseException`. Never put stack traces or SQL text in `detail`; log the exception server-side and return a generic message for the catch-all 500.
|
|
80
|
+
|
|
81
|
+
## Outbound HTTP
|
|
82
|
+
Blocking calls use `RestClient`; build it from the injected `RestClient.Builder` so Boot's customizers (timeouts, SSL, observability) apply.
|
|
83
|
+
|
|
84
|
+
```java
|
|
85
|
+
@Bean
|
|
86
|
+
RestClient pricingClient(RestClient.Builder builder) {
|
|
87
|
+
return builder.baseUrl("https://api.example.com").build();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Quote fetch(String id) {
|
|
91
|
+
return pricingClient.get().uri("/quotes/{id}", id)
|
|
92
|
+
.retrieve()
|
|
93
|
+
.onStatus(status -> status.value() == 404, (req, res) -> {
|
|
94
|
+
throw new ResourceNotFoundException("Quote " + id);
|
|
95
|
+
})
|
|
96
|
+
.body(Quote.class);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Prefer an `@HttpExchange` interface when the remote API has several operations (see [spring-boot/cloud.md](cloud.md)). `WebClient` is for reactive or streaming code only.
|
|
101
|
+
|
|
102
|
+
## CORS
|
|
103
|
+
Prefer one bean over scattered annotations. With Spring Security on the classpath configure CORS in the security chain ([spring-boot/security.md](security.md)); otherwise `WebMvcConfigurer.addCorsMappings`. List explicit origins; `allowedOrigins("*")` cannot be combined with credentials.
|
|
104
|
+
|
|
105
|
+
## Quick reference
|
|
106
|
+
| Need | Use |
|
|
107
|
+
|------|-----|
|
|
108
|
+
| Page and sort | `Pageable` + `@PageableDefault` |
|
|
109
|
+
| Bad input | `@Valid` -> 400 Problem Details |
|
|
110
|
+
| Missing resource | 404 via your exception handler |
|
|
111
|
+
| Duplicate | 409 from the DB constraint |
|
|
112
|
+
| Slow upstream | `spring.http.clients.connect-timeout` / `read-timeout` |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# {{SKILL}}: spring-boot
|
|
2
|
+
|
|
3
|
+
> {{DESC_SPRING_BOOT}}
|
|
4
|
+
|
|
5
|
+
{{CREDIT:piomin-spring-boot:skills/spring-boot/SKILL.md}}
|
|
6
|
+
|
|
7
|
+
{{CREDIT:piomin-spring-boot:skills/conventions/SKILL.md}}
|
|
8
|
+
|
|
9
|
+
**Trigger:** writing or changing a Spring Boot application (Spring Boot 4, Spring Framework 7, Java 21+) — controllers, services, configuration, security, HTTP clients, logging, cloud-native deployment. Load it for any Spring Boot task, then read the side file the task needs. It does not replace the core review or refactor skills; for a Spring review or refactor, load those and use this one for the framework facts.
|
|
10
|
+
|
|
11
|
+
Target stack: Spring Boot 4 / Spring Framework 7 / Spring Security 7 / Hibernate 7 / Jakarta EE 11 / Java 21+. Check the project's actual versions first (`pom.xml` or `build.gradle`); if it is still on Boot 3, say so and do not apply the Boot 4 rows below blindly.
|
|
12
|
+
|
|
13
|
+
## Working loop
|
|
14
|
+
1. Read the build file, `application.yml` and the existing package layout. Match what the project already does (Maven or Gradle, its CI, its test style) before adding anything.
|
|
15
|
+
2. Confirm the design for anything beyond a small change: endpoints, DTOs, transaction boundaries, who authenticates.
|
|
16
|
+
3. Implement in layers, then run the project's own test command and read the output.
|
|
17
|
+
|
|
18
|
+
## Conventions
|
|
19
|
+
- Layering: controller (HTTP, validation) -> service (business logic, transactions) -> repository. Keep the package layout the project already has; for a new app a flat feature-or-layer split under one root package is fine (`com.example.<app>`).
|
|
20
|
+
- Constructor injection only; one constructor needs no `@Autowired`. Lombok is allowed where the project uses it.
|
|
21
|
+
- DTOs are records. JPA entities are not records. Never return an entity from a controller.
|
|
22
|
+
- `@Valid` on every request body; `@Transactional(readOnly = true)` on read services, `@Transactional` on mutating methods.
|
|
23
|
+
- Typed config: `@ConfigurationProperties` records instead of scattered `@Value`.
|
|
24
|
+
- Errors: one `@RestControllerAdvice`, Problem Details responses.
|
|
25
|
+
- Blocking outbound HTTP: `RestClient`, or an `@HttpExchange` interface. `WebClient` only for reactive or streaming code. Do not mix blocking and reactive in one call path.
|
|
26
|
+
- Jakarta namespaces only (`jakarta.persistence`, `jakarta.validation`, `jakarta.servlet`).
|
|
27
|
+
- Secrets from the environment or mounted files, never committed config.
|
|
28
|
+
- Tests: positive and negative cases; Testcontainers with `@ServiceConnection`; `@MockitoBean` for slice-test collaborators.
|
|
29
|
+
- Baseline properties worth enabling: `spring.threads.virtual.enabled: true`, `spring.mvc.problemdetails.enabled: true`, `server.shutdown: graceful`.
|
|
30
|
+
|
|
31
|
+
## Boot 4 pitfalls
|
|
32
|
+
Removed or renamed in Boot 4 (do not write them): `@MockBean` and `@SpyBean`, `ClientHttpRequestFactories`, `javax.persistence` (now `jakarta.persistence`), Spring Security 6 idioms such as `authorizeRequests` and `.and()` chains. <!-- banned: ok -->
|
|
33
|
+
|
|
34
|
+
| Old habit | Boot 4 / Framework 7 reality |
|
|
35
|
+
|-----------|------------------------------|
|
|
36
|
+
| Mock beans in slice tests | `@MockitoBean`, `@MockitoSpyBean` (`org.springframework.test.context.bean.override.mockito`) |
|
|
37
|
+
| Hand-built request factories | `ClientHttpRequestFactoryBuilder` with `HttpClientSettings` |
|
|
38
|
+
| Security configuration | Spring Security 7: lambda DSL, `authorizeHttpRequests` |
|
|
39
|
+
| `org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest` | `org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest`: slices moved to per-technology modules; import from the new packages |
|
|
40
|
+
| Jackson 2 (`com.fasterxml.jackson`) | Jackson 3 is the default (`tools.jackson`); Jackson 2 support is deprecated, `spring.jackson.use-jackson2-defaults` eases migration |
|
|
41
|
+
| `@DynamicPropertySource` for container URLs | `@ServiceConnection` on the container |
|
|
42
|
+
| MockMvc status assertions via Hamcrest | `MockMvcTester` (AssertJ) or `RestTestClient` with `@AutoConfigureRestTestClient` |
|
|
43
|
+
| OpenFeign for declarative clients | `@HttpExchange` interfaces, registered with `@ImportHttpServices` |
|
|
44
|
+
| Zipkin-only tracing setup | `spring-boot-starter-opentelemetry`, `management.opentelemetry.tracing.export.otlp.*` |
|
|
45
|
+
| Starters assumed to pull everything | Boot 4 starters are modular; add the matching starter or test starter for each technology, and read the migration guide when a class vanishes |
|
|
46
|
+
|
|
47
|
+
## Side files
|
|
48
|
+
- **Web/REST** -> read [spring-boot/web.md](spring-boot/web.md) and follow it.
|
|
49
|
+
- **Security (OAuth2 resource server, method security, SSL bundles)** -> read [spring-boot/security.md](spring-boot/security.md) and follow it.
|
|
50
|
+
- **Cloud-native (config, gateway, resilience, tracing, Kubernetes, images)** -> read [spring-boot/cloud.md](spring-boot/cloud.md) and follow it.
|
|
51
|
+
- **Modern Java in Spring code (records, sealed types, pattern matching, null safety)** -> read [spring-boot/java-modern.md](spring-boot/java-modern.md) and follow it.
|
|
52
|
+
- **Logging and structured logs** -> read [spring-boot/logging.md](spring-boot/logging.md) and follow it.
|
|
53
|
+
|
|
54
|
+
## Done when
|
|
55
|
+
- The project's build and tests pass, read from real output; no deprecated Boot 2/3 idiom was introduced; every new endpoint validates input and has a negative test.
|
|
56
|
+
|
|
57
|
+
<!-- INCLUDE: skills/_self-improvement.md -->
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# {{SKILL}}: spring-data-jpa — outbox, saga, CQRS
|
|
2
|
+
|
|
3
|
+
{{CREDIT:piomin-spring-boot:skills/design-patterns/SKILL.md}}
|
|
4
|
+
|
|
5
|
+
Use when a write must also publish an event, a business process spans services, or reads and writes need separate models. If a plain `@Transactional` method does the job, do not add these.
|
|
6
|
+
|
|
7
|
+
## Transactional outbox
|
|
8
|
+
|
|
9
|
+
Dual write (save, then publish to a broker) can lose or duplicate the event. Write the event row in the same transaction as the state change; a separate relay publishes it.
|
|
10
|
+
|
|
11
|
+
```java
|
|
12
|
+
@Entity
|
|
13
|
+
public class OutboxEvent {
|
|
14
|
+
@Id @GeneratedValue(strategy = GenerationType.UUID) UUID id;
|
|
15
|
+
String aggregateId;
|
|
16
|
+
String type;
|
|
17
|
+
@Column(columnDefinition = "text") String payload;
|
|
18
|
+
boolean published;
|
|
19
|
+
// constructor, getters
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Service
|
|
23
|
+
class OrderService {
|
|
24
|
+
@Transactional
|
|
25
|
+
public Order place(OrderRequest req) {
|
|
26
|
+
Order order = orders.save(Order.from(req));
|
|
27
|
+
outbox.save(new OutboxEvent(order.getId().toString(), "OrderPlaced", json(order)));
|
|
28
|
+
return order; // both rows commit or neither does
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Component
|
|
33
|
+
class OutboxRelay {
|
|
34
|
+
@Scheduled(fixedDelay = 1000) // needs @EnableScheduling on a config class
|
|
35
|
+
@Transactional
|
|
36
|
+
void relay() {
|
|
37
|
+
for (OutboxEvent e : outbox.findTop100ByPublishedFalseOrderByIdAsc()) {
|
|
38
|
+
publisher.send(e); // broker client; may throw, row stays unpublished
|
|
39
|
+
e.setPublished(true);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- Delivery is at-least-once: consumers dedupe on the event id.
|
|
46
|
+
- Several relay instances will pick the same rows. Use a single relay, or a row-claiming query verified for your database (not covered here), or a lock.
|
|
47
|
+
- Delete or archive published rows on a schedule.
|
|
48
|
+
- In-process alternative: publish a Spring `ApplicationEvent` and handle it with `@TransactionalEventListener` (default phase `AFTER_COMMIT`). The handler runs only if the transaction commits, and it is not durable: a crash after commit loses the event. Use it for in-JVM side effects, the outbox table for anything that must survive a crash. If the listener must write to the database again, annotate it `@Transactional(propagation = Propagation.REQUIRES_NEW)`.
|
|
49
|
+
|
|
50
|
+
## Saga
|
|
51
|
+
|
|
52
|
+
| Style | Mechanism | Pick when |
|
|
53
|
+
|-------|-----------|-----------|
|
|
54
|
+
| Choreography | each service reacts to the previous service's event and emits its own (events through the outbox) | few steps, no central owner |
|
|
55
|
+
| Orchestration | one `OrderSaga` component sends commands, tracks state in its own table, issues compensations | many steps, branching, need to see where a flow is stuck |
|
|
56
|
+
|
|
57
|
+
- Every step has a compensating action (cancel, refund, release); compensations must be idempotent.
|
|
58
|
+
- Persist saga state (`STARTED`, `PAYMENT_DONE`, `FAILED`, ...) with `@Version` so concurrent events cannot both advance it.
|
|
59
|
+
- Timeouts are events too: a scheduled job moves stale sagas to compensation.
|
|
60
|
+
- Messaging infrastructure (Spring Cloud Stream, plain Kafka or AMQP clients) is project-specific and not verified here; keep the saga logic behind a small publisher interface.
|
|
61
|
+
|
|
62
|
+
## CQRS
|
|
63
|
+
|
|
64
|
+
Split only when read and write shapes diverge. Smallest useful form, same database:
|
|
65
|
+
|
|
66
|
+
- Write side: aggregate entities and `@Transactional` command services.
|
|
67
|
+
- Read side: projection or DTO-record repository methods (or a view) behind a `@Transactional(readOnly = true)` query service; never return entities.
|
|
68
|
+
- A separate read store fed by outbox events adds eventual consistency: show it in the API (version or timestamp) and accept stale reads.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# {{SKILL}}: spring-data-jpa
|
|
2
|
+
|
|
3
|
+
> {{DESC_SPRING_DATA_JPA}}
|
|
4
|
+
|
|
5
|
+
{{CREDIT:piomin-spring-boot:skills/jpa-patterns/SKILL.md}}
|
|
6
|
+
|
|
7
|
+
{{CREDIT:piomin-spring-boot:skills/spring-boot/references/data.md}}
|
|
8
|
+
|
|
9
|
+
**Trigger:** a Spring Data JPA / Hibernate task in a Spring Boot project: entities, repositories, N+1 queries, `LazyInitializationException`, transactions, locking, projections, Flyway or Liquibase migrations, soft delete, or a transactional outbox. Spring-specific; for generic query or schema work outside Spring, do not load it.
|
|
10
|
+
|
|
11
|
+
Target stack: Spring Boot 4, Hibernate 7, Jakarta Persistence (`jakarta.persistence.*` only).
|
|
12
|
+
|
|
13
|
+
## Symptom to fix
|
|
14
|
+
|
|
15
|
+
| Symptom | Fix |
|
|
16
|
+
|---------|-----|
|
|
17
|
+
| N+1 SELECTs | `@EntityGraph(attributePaths = "books")` or `JOIN FETCH`; as a mitigation only, `hibernate.default_batch_fetch_size` (batch fetching mitigates N+1, it does not solve it) |
|
|
18
|
+
| `LazyInitializationException` | fetch what the use case needs inside the transaction (graph / `JOIN FETCH`) or return a DTO; do not flip associations to `EAGER` |
|
|
19
|
+
| Slow list endpoints | `Pageable` (`Slice` when no total is needed), interface or record projections, indexes |
|
|
20
|
+
| Lost updates | `@Version` (optimistic); `@Lock(LockModeType.PESSIMISTIC_WRITE)` only for hot rows |
|
|
21
|
+
| Slow read-only service | `@Transactional(readOnly = true)` on the class, override on writes |
|
|
22
|
+
| Transaction ignored | self-invocation bypasses the proxy: call through another bean |
|
|
23
|
+
|
|
24
|
+
## Entities
|
|
25
|
+
|
|
26
|
+
```java
|
|
27
|
+
@Entity
|
|
28
|
+
@SoftDelete // Hibernate: adds a deleted-flag column, filters reads
|
|
29
|
+
public class Author {
|
|
30
|
+
@Id @GeneratedValue(strategy = GenerationType.UUID)
|
|
31
|
+
private UUID id;
|
|
32
|
+
@Version private long version;
|
|
33
|
+
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
|
|
34
|
+
private List<Book> books = new ArrayList<>();
|
|
35
|
+
// getters/setters, or Lombok if the project uses it
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- Every to-one and to-many is `LAZY`; decide fetching per query, not on the mapping.
|
|
40
|
+
- `@SoftDelete(strategy = ...)` takes `ACTIVE`, `DELETED`, or `TIMESTAMP`; the default stores a deleted flag.
|
|
41
|
+
- `GenerationType.UUID` generates the id in Java; `IDENTITY` and `SEQUENCE` need the database.
|
|
42
|
+
- Equality on entities: id-based with a stable hash, or business key. Never include lazy collections in `equals`/`toString`.
|
|
43
|
+
|
|
44
|
+
## Repositories and projections
|
|
45
|
+
|
|
46
|
+
```java
|
|
47
|
+
public interface AuthorRepository extends JpaRepository<Author, UUID> {
|
|
48
|
+
@EntityGraph(attributePaths = "books")
|
|
49
|
+
List<Author> findByNameStartingWith(String prefix);
|
|
50
|
+
|
|
51
|
+
record AuthorName(UUID id, String name) {}
|
|
52
|
+
List<AuthorName> findAllBy(Pageable pageable); // DTO projection
|
|
53
|
+
|
|
54
|
+
@Modifying(clearAutomatically = true)
|
|
55
|
+
@Query("update Author a set a.name = :name where a.id = :id")
|
|
56
|
+
int rename(UUID id, String name);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Interface projections are built from tuple queries; DTO records need an all-args constructor and no column aliases in `@Query` JPQL.
|
|
61
|
+
- Bulk `@Modifying` queries bypass the persistence context: `clearAutomatically = true` or reload.
|
|
62
|
+
- Return DTOs from the web layer, never entities.
|
|
63
|
+
|
|
64
|
+
## Transactions and open-in-view
|
|
65
|
+
|
|
66
|
+
- Put `@Transactional` on service methods, not repositories or controllers.
|
|
67
|
+
- `spring.jpa.open-in-view` defaults to `true` in a web app (the EntityManager stays open through view rendering). Set it to `false` and fetch explicitly; it hides N+1 otherwise.
|
|
68
|
+
- Detect N+1: `spring.jpa.show-sql=true` is enough for a quick look; assert query counts in a `@DataJpaTest` for regressions.
|
|
69
|
+
|
|
70
|
+
## Migrations
|
|
71
|
+
|
|
72
|
+
Schema is owned by migrations, not `ddl-auto`: `spring.jpa.hibernate.ddl-auto=validate` (or `none`) with Flyway or Liquibase, whichever the project already uses. In Boot 4 add the starter (`spring-boot-starter-flyway`), not just the Flyway core jar, or auto-configuration does not apply; Postgres and MySQL also need `flyway-database-postgresql` / `flyway-mysql`. Migration files are immutable once merged; add a new one.
|
|
73
|
+
|
|
74
|
+
## Outbox, saga, CQRS
|
|
75
|
+
|
|
76
|
+
Event-publishing alongside a DB write (dual-write problem), distributed workflows, read/write model split: read [spring-data-jpa/patterns.md](spring-data-jpa/patterns.md).
|
|
77
|
+
|
|
78
|
+
## Done when
|
|
79
|
+
- No lazy collection is touched outside a transaction; hot queries show one SELECT per use case in the SQL log; schema changes ship as a migration; imports are `jakarta.persistence`.
|
|
80
|
+
|
|
81
|
+
<!-- INCLUDE: skills/_self-improvement.md -->
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# {{SKILL}}: spring-testing
|
|
2
|
+
|
|
3
|
+
> {{DESC_SPRING_TESTING}}
|
|
4
|
+
|
|
5
|
+
{{CREDIT:piomin-spring-boot:skills/testing-patterns/SKILL.md}}
|
|
6
|
+
|
|
7
|
+
{{CREDIT:piomin-spring-boot:skills/spring-boot/references/testing.md}}
|
|
8
|
+
|
|
9
|
+
**Trigger:** writing or fixing tests in a Spring Boot project: unit tests of services, `@WebMvcTest` / `@DataJpaTest` slices, `@SpringBootTest` integration tests, Testcontainers, mocking beans. Spring-specific; core test discipline lives in the test-gates and develop skills.
|
|
10
|
+
|
|
11
|
+
Target stack: Spring Boot 4, JUnit 5, Mockito, AssertJ.
|
|
12
|
+
|
|
13
|
+
## Pick the narrowest test
|
|
14
|
+
|
|
15
|
+
| Goal | Tool |
|
|
16
|
+
|------|------|
|
|
17
|
+
| Service logic | plain JUnit + `@ExtendWith(MockitoExtension.class)`, no Spring context |
|
|
18
|
+
| Controller mapping, validation, serialization | `@WebMvcTest(X.class)` + `MockMvcTester` + `@MockitoBean` |
|
|
19
|
+
| Repository queries, mappings | `@DataJpaTest` against the real database engine |
|
|
20
|
+
| Whole app over HTTP | `@SpringBootTest(webEnvironment = RANDOM_PORT)` + `@AutoConfigureRestTestClient` |
|
|
21
|
+
| Whole app, no server | `@SpringBootTest` + `@AutoConfigureMockMvc` |
|
|
22
|
+
|
|
23
|
+
Boot 4 split the test support into modules: slice annotations live in `org.springframework.boot.<module>.test.autoconfigure` (e.g. `org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest`); `@AutoConfigureTestDatabase` is in `org.springframework.boot.jdbc.test.autoconfigure`. Let the IDE import them. Use the matching test starter (`spring-boot-starter-data-jpa-test`, and so on) so each slice is on the classpath.
|
|
24
|
+
|
|
25
|
+
## Mocking beans
|
|
26
|
+
|
|
27
|
+
The Boot-era mock-bean annotations are gone in Boot 4. Use `@MockitoBean` and `@MockitoSpyBean` (from `org.springframework.test.context.bean.override.mockito`).
|
|
28
|
+
|
|
29
|
+
```java
|
|
30
|
+
@WebMvcTest(UserController.class)
|
|
31
|
+
class UserControllerTests {
|
|
32
|
+
@Autowired MockMvcTester mvc;
|
|
33
|
+
@MockitoBean UserService users;
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
void unknownUserIs404() {
|
|
37
|
+
given(users.find("x")).willThrow(new UserNotFoundException("x"));
|
|
38
|
+
assertThat(mvc.get().uri("/users/x")).hasStatus(HttpStatus.NOT_FOUND);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`MockMvcTester` is AssertJ-based (`assertThat(mvc.get().uri(...)).hasStatusOk()`); do not mix it with the `expectStatus()` chain, which belongs to the other clients. Security: `@WithMockUser` still works on slices; `@Import` your security config.
|
|
44
|
+
|
|
45
|
+
## Full-server test
|
|
46
|
+
|
|
47
|
+
```java
|
|
48
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
49
|
+
@AutoConfigureRestTestClient
|
|
50
|
+
class UserApiTests {
|
|
51
|
+
@Autowired RestTestClient client;
|
|
52
|
+
|
|
53
|
+
@Test
|
|
54
|
+
void createReturns201() {
|
|
55
|
+
client.post().uri("/users").contentType(MediaType.APPLICATION_JSON)
|
|
56
|
+
.body(new UserRequest("a@example.com"))
|
|
57
|
+
.exchange()
|
|
58
|
+
.expectStatus().isCreated();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`RestTestClient` resolves relative URIs against the running server; it also supports AssertJ-style assertions through its response type. Customize through a `RestTestClientBuilderCustomizer` bean.
|
|
64
|
+
|
|
65
|
+
## Real dependencies with Testcontainers
|
|
66
|
+
|
|
67
|
+
Prefer `@ServiceConnection`: Boot derives connection details, no property wiring.
|
|
68
|
+
|
|
69
|
+
```java
|
|
70
|
+
@TestConfiguration(proxyBeanMethods = false)
|
|
71
|
+
class ContainersConfig {
|
|
72
|
+
@Bean @ServiceConnection
|
|
73
|
+
PostgreSQLContainer postgres() { return new PostgreSQLContainer("postgres:16"); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@DataJpaTest
|
|
77
|
+
@Import(ContainersConfig.class)
|
|
78
|
+
class AuthorRepositoryTests { /* ... */ }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- Testcontainers 2 containers are non-generic and live in per-module packages (`org.testcontainers.postgresql.PostgreSQLContainer`, artifact `testcontainers-postgresql`).
|
|
82
|
+
- The default `replace = NON_TEST` already keeps a `@ServiceConnection` data source; `replace = NONE` is only needed for a non-test `DataSource` you defined yourself.
|
|
83
|
+
- Where `@ServiceConnection` has no support for the container, use `@Testcontainers` + `@Container static` + `@DynamicPropertySource` to register the properties.
|
|
84
|
+
- Share one container configuration across tests so the context cache is reused.
|
|
85
|
+
|
|
86
|
+
## Rules
|
|
87
|
+
|
|
88
|
+
- Test behaviour through the public seam; do not assert on mock call counts unless the call is the behaviour.
|
|
89
|
+
- Slice tests load only what they need; reaching for `@SpringBootTest` to test one controller is a smell.
|
|
90
|
+
- Never replace the database engine with H2 when production runs another one; dialect differences hide bugs.
|
|
91
|
+
- No `Thread.sleep`: wait on the condition with a bounded timeout.
|
|
92
|
+
|
|
93
|
+
<!-- INCLUDE: skills/_self-improvement.md -->
|
package/src/skills/wayfinder.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> {{DESC_WAYFINDER}}
|
|
4
4
|
|
|
5
|
+
{{CREDIT:mattpocock-skills:skills/engineering/wayfinder/SKILL.md}}
|
|
6
|
+
|
|
5
7
|
**Trigger:** a piece of work too large for one session and still in the fog —
|
|
6
8
|
the destination can be named but the route to it cannot; the user says
|
|
7
9
|
"wayfinder" or asks to chart a big effort before building anything.
|
|
@@ -27,7 +29,8 @@ the destination can be named but the route to it cannot; the user says
|
|
|
27
29
|
`wayfinder:<type>` label (or a `Type:` line), which names how it resolves:
|
|
28
30
|
- **research** (agent alone) — a fact the decision waits on, from docs, an API or
|
|
29
31
|
the web: a subagent runs the [research {{SKILL}}](research.md). Fire every research
|
|
30
|
-
ticket in parallel as soon as the map is charted.
|
|
32
|
+
ticket in parallel as soon as the map is charted. Then stop: charting is one session's work and
|
|
33
|
+
resolves no ticket itself.
|
|
31
34
|
- **prototype** (with the user) — a rough artifact to react to when "how should it
|
|
32
35
|
look or behave" is the question: the [prototype {{SKILL}}](prototype.md).
|
|
33
36
|
- **grilling** (with the user, the default) — an interview through the
|
|
@@ -45,7 +48,10 @@ the destination can be named but the route to it cannot; the user says
|
|
|
45
48
|
`Status: claimed`) so concurrent sessions skip it. Resolve it as its type says — read
|
|
46
49
|
the label, the body never states it — write the decision into the ticket,
|
|
47
50
|
gist-and-link it on the map, close it. At most one ticket per session (research
|
|
48
|
-
tickets excepted). New questions become tickets
|
|
51
|
+
tickets excepted). New questions become tickets; fog the answer made specifiable
|
|
52
|
+
graduates to tickets and leaves **Not yet specified**; a ticket the answer invalidates
|
|
53
|
+
is updated or closed, and one now past the destination is closed with an **Out of
|
|
54
|
+
scope** line.
|
|
49
55
|
5. In everything the user reads, refer to the map and tickets **by name**,
|
|
50
56
|
never by bare issue number — the id rides inside the name's link.
|
|
51
57
|
6. Stop when the frontier is empty: nothing left to decide means the way is
|