mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-21 20:40:28 +08:00
add quarkus to java part
This commit is contained in:
parent
098b773c11
commit
c44d37e931
@ -120,4 +120,6 @@ Remaining errors: 1
|
|||||||
|
|
||||||
Final: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
Final: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||||
|
|
||||||
For detailed Java and Spring Boot patterns, see `skill: springboot-patterns`.
|
For detailed patterns and examples:
|
||||||
|
- **Spring Boot**: See `skill: springboot-patterns`
|
||||||
|
- **Quarkus**: See `skill: quarkus-patterns`
|
||||||
|
|||||||
@ -94,4 +94,6 @@ grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
|||||||
- **Warning**: MEDIUM issues only
|
- **Warning**: MEDIUM issues only
|
||||||
- **Block**: CRITICAL or HIGH issues found
|
- **Block**: CRITICAL or HIGH issues found
|
||||||
|
|
||||||
For detailed Spring Boot patterns and examples, see `skill: springboot-patterns`.
|
For detailed patterns and examples:
|
||||||
|
- **Spring Boot**: See `skill: springboot-patterns`
|
||||||
|
- **Quarkus**: See `skill: quarkus-patterns`
|
||||||
|
|||||||
@ -374,6 +374,10 @@ everything-claude-code/
|
|||||||
| |-- laravel-verification/ # Laravel verification loops (NEW)
|
| |-- laravel-verification/ # Laravel verification loops (NEW)
|
||||||
| |-- python-patterns/ # Python idioms and best practices (NEW)
|
| |-- python-patterns/ # Python idioms and best practices (NEW)
|
||||||
| |-- python-testing/ # Python testing with pytest (NEW)
|
| |-- python-testing/ # Python testing with pytest (NEW)
|
||||||
|
| |-- quarkus-patterns/ # Java Quarkus patterns (NEW)
|
||||||
|
| |-- quarkus-security/ # Quarkus security (NEW)
|
||||||
|
| |-- quarkus-tdd/ # Quarkus TDD (NEW)
|
||||||
|
| |-- quarkus-verification/ # Quarkus verification (NEW)
|
||||||
| |-- springboot-patterns/ # Java Spring Boot patterns (NEW)
|
| |-- springboot-patterns/ # Java Spring Boot patterns (NEW)
|
||||||
| |-- springboot-security/ # Spring Boot security (NEW)
|
| |-- springboot-security/ # Spring Boot security (NEW)
|
||||||
| |-- springboot-tdd/ # Spring Boot TDD (NEW)
|
| |-- springboot-tdd/ # Spring Boot TDD (NEW)
|
||||||
@ -691,7 +695,7 @@ cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/
|
|||||||
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
||||||
|
|
||||||
# Optional: add niche/framework-specific skills only when needed
|
# Optional: add niche/framework-specific skills only when needed
|
||||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns; do
|
# for s in django-patterns django-tdd laravel-patterns springboot-patterns quarkus-patterns; do
|
||||||
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
||||||
# done
|
# done
|
||||||
|
|
||||||
|
|||||||
@ -289,6 +289,10 @@ everything-claude-code/
|
|||||||
| |-- laravel-verification/ # Laravel 验证循环(新增)
|
| |-- laravel-verification/ # Laravel 验证循环(新增)
|
||||||
| |-- python-patterns/ # Python 惯用写法与最佳实践(新增)
|
| |-- python-patterns/ # Python 惯用写法与最佳实践(新增)
|
||||||
| |-- python-testing/ # 基于 pytest 的 Python 测试(新增)
|
| |-- python-testing/ # 基于 pytest 的 Python 测试(新增)
|
||||||
|
| |-- quarkus-patterns/ # Java Quarkus 模式(新增)
|
||||||
|
| |-- quarkus-security/ # Quarkus 安全(新增)
|
||||||
|
| |-- quarkus-tdd/ # Quarkus TDD(新增)
|
||||||
|
| |-- quarkus-verification/ # Quarkus 验证(新增)
|
||||||
| |-- springboot-patterns/ # Java Spring Boot 模式(新增)
|
| |-- springboot-patterns/ # Java Spring Boot 模式(新增)
|
||||||
| |-- springboot-security/ # Spring Boot 安全(新增)
|
| |-- springboot-security/ # Spring Boot 安全(新增)
|
||||||
| |-- springboot-tdd/ # Spring Boot TDD(新增)
|
| |-- springboot-tdd/ # Spring Boot TDD(新增)
|
||||||
@ -605,7 +609,7 @@ cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/
|
|||||||
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
||||||
|
|
||||||
# 可选:仅在需要时添加细分领域/框架专属技能
|
# 可选:仅在需要时添加细分领域/框架专属技能
|
||||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns; do
|
# for s in django-patterns django-tdd laravel-patterns springboot-patterns quarkus-patterns; do
|
||||||
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
||||||
# done
|
# done
|
||||||
|
|
||||||
|
|||||||
@ -126,6 +126,10 @@ skills:
|
|||||||
- security-scan
|
- security-scan
|
||||||
- skill-comply
|
- skill-comply
|
||||||
- skill-stocktake
|
- skill-stocktake
|
||||||
|
- quarkus-patterns
|
||||||
|
- quarkus-security
|
||||||
|
- quarkus-tdd
|
||||||
|
- quarkus-verification
|
||||||
- springboot-patterns
|
- springboot-patterns
|
||||||
- springboot-security
|
- springboot-security
|
||||||
- springboot-tdd
|
- springboot-tdd
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: java-build-resolver
|
name: java-build-resolver
|
||||||
description: Java/Maven/Gradle build, compilation, and dependency error resolution specialist. Fixes build errors, Java compiler errors, and Maven/Gradle issues with minimal changes. Use when Java or Spring Boot builds fail.
|
description: Java/Maven/Gradle build, compilation, and dependency error resolution specialist. Automatically detects Spring Boot or Quarkus and applies framework-specific fixes. Fixes build errors, Java compiler errors, and Maven/Gradle issues with minimal changes. Use when Java builds fail.
|
||||||
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
|
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
|
||||||
model: sonnet
|
model: sonnet
|
||||||
---
|
---
|
||||||
@ -11,12 +11,25 @@ You are an expert Java/Maven/Gradle build error resolution specialist. Your miss
|
|||||||
|
|
||||||
You DO NOT refactor or rewrite code — you fix the build error only.
|
You DO NOT refactor or rewrite code — you fix the build error only.
|
||||||
|
|
||||||
|
## Framework Detection (run first)
|
||||||
|
|
||||||
|
Before attempting any fix, determine the framework:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat pom.xml 2>/dev/null || cat build.gradle 2>/dev/null || cat build.gradle.kts 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
- If the build file contains `quarkus` → apply **[QUARKUS]** rules
|
||||||
|
- If the build file contains `spring-boot` → apply **[SPRING]** rules
|
||||||
|
- If both are present (unlikely) → flag as a finding and apply both rulesets
|
||||||
|
- If neither is detected → use general Java rules only and note the ambiguity
|
||||||
|
|
||||||
## Core Responsibilities
|
## Core Responsibilities
|
||||||
|
|
||||||
1. Diagnose Java compilation errors
|
1. Diagnose Java compilation errors
|
||||||
2. Fix Maven and Gradle build configuration issues
|
2. Fix Maven and Gradle build configuration issues
|
||||||
3. Resolve dependency conflicts and version mismatches
|
3. Resolve dependency conflicts and version mismatches
|
||||||
4. Handle annotation processor errors (Lombok, MapStruct, Spring)
|
4. Handle annotation processor errors (Lombok, MapStruct, Spring, Quarkus)
|
||||||
5. Fix Checkstyle and SpotBugs violations
|
5. Fix Checkstyle and SpotBugs violations
|
||||||
|
|
||||||
## Diagnostic Commands
|
## Diagnostic Commands
|
||||||
@ -36,15 +49,18 @@ Run these in order:
|
|||||||
## Resolution Workflow
|
## Resolution Workflow
|
||||||
|
|
||||||
```text
|
```text
|
||||||
1. ./mvnw compile OR ./gradlew build -> Parse error message
|
1. Detect framework (Spring Boot / Quarkus)
|
||||||
2. Read affected file -> Understand context
|
2. ./mvnw compile OR ./gradlew build -> Parse error message
|
||||||
3. Apply minimal fix -> Only what's needed
|
3. Read affected file -> Understand context
|
||||||
4. ./mvnw compile OR ./gradlew build -> Verify fix
|
4. Apply minimal fix -> Only what's needed
|
||||||
5. ./mvnw test OR ./gradlew test -> Ensure nothing broke
|
5. ./mvnw compile OR ./gradlew build -> Verify fix
|
||||||
|
6. ./mvnw test OR ./gradlew test -> Ensure nothing broke
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Fix Patterns
|
## Common Fix Patterns
|
||||||
|
|
||||||
|
### General Java
|
||||||
|
|
||||||
| Error | Cause | Fix |
|
| Error | Cause | Fix |
|
||||||
|-------|-------|-----|
|
|-------|-------|-----|
|
||||||
| `cannot find symbol` | Missing import, typo, missing dependency | Add import or dependency |
|
| `cannot find symbol` | Missing import, typo, missing dependency | Add import or dependency |
|
||||||
@ -60,6 +76,34 @@ Run these in order:
|
|||||||
| `The following artifacts could not be resolved` | Private repo or network issue | Check repository credentials or `settings.xml` |
|
| `The following artifacts could not be resolved` | Private repo or network issue | Check repository credentials or `settings.xml` |
|
||||||
| `COMPILATION ERROR: Source option X is no longer supported` | Java version mismatch | Update `maven.compiler.source` / `targetCompatibility` |
|
| `COMPILATION ERROR: Source option X is no longer supported` | Java version mismatch | Update `maven.compiler.source` / `targetCompatibility` |
|
||||||
|
|
||||||
|
### [SPRING] Spring Boot Specific
|
||||||
|
|
||||||
|
| Error | Cause | Fix |
|
||||||
|
|-------|-------|-----|
|
||||||
|
| `No qualifying bean of type X` | Missing `@Component`/`@Service` or component scan | Add annotation or fix scan base package |
|
||||||
|
| `Circular dependency involving X` | Constructor injection cycle | Refactor to break cycle or use `@Lazy` on one leg |
|
||||||
|
| `BeanCreationException: Error creating bean` | Missing config, bad property, or missing dependency | Check `application.yml`, dependency tree |
|
||||||
|
| `HttpMessageNotReadableException` | Malformed JSON or missing Jackson dependency | Check `spring-boot-starter-web` includes Jackson |
|
||||||
|
| `Could not autowire. No beans of type found` | Missing bean or wrong profile active | Check `@Profile`, `@ConditionalOn*`, component scan |
|
||||||
|
| `Failed to configure a DataSource` | Missing DB driver or datasource properties | Add driver dependency or `spring.datasource.*` config |
|
||||||
|
| `spring-boot-starter-* not found` | BOM version mismatch | Check `spring-boot-dependencies` BOM version in parent |
|
||||||
|
|
||||||
|
### [QUARKUS] Quarkus Specific
|
||||||
|
|
||||||
|
| Error | Cause | Fix |
|
||||||
|
|-------|-------|-----|
|
||||||
|
| `UnsatisfiedResolutionException: no bean found` | Missing `@ApplicationScoped`/`@Inject` or missing extension | Add CDI annotation or `quarkus-*` extension |
|
||||||
|
| `AmbiguousResolutionException` | Multiple beans match injection point | Add `@Priority`, `@Alternative`, or qualifier |
|
||||||
|
| `Build step X threw an exception: RuntimeException` | Quarkus build-time augmentation failure | Read full stack trace — usually a missing extension, bad config, or reflection issue |
|
||||||
|
| `Error injecting X: it's a non-proxyable bean type` | `@Singleton` with interceptor or `final` class | Switch to `@ApplicationScoped` or remove `final` |
|
||||||
|
| `ClassNotFoundException at native image build` | Missing `@RegisterForReflection` or reflection config | Add `@RegisterForReflection` or `reflect-config.json` entry |
|
||||||
|
| `BlockingNotAllowedOnIOThread` | Blocking call on Vert.x event loop | Add `@Blocking` to endpoint or use reactive client |
|
||||||
|
| `ConfigurationException: SRCFG*` | Missing or malformed config property | Check `application.properties` for required `quarkus.*` or `mp.*` keys |
|
||||||
|
| `quarkus-extension-* not found` | Wrong BOM version or extension not in BOM | Check `quarkus-bom` version; use `quarkus ext add <name>` |
|
||||||
|
| `DEV mode hot reload failure` | Incompatible change during dev mode | Run `./mvnw quarkus:dev` with clean: `./mvnw clean quarkus:dev` |
|
||||||
|
| `Panache entity not enhanced` | Entity not detected at build time | Ensure entity is in scanned package; check for missing `quarkus-hibernate-orm-panache` or `quarkus-mongodb-panache` extension |
|
||||||
|
| `RESTEASY* deployment failure` | Duplicate JAX-RS paths or missing provider | Check `@Path` uniqueness; ensure `quarkus-resteasy-reactive` vs `quarkus-resteasy` are not mixed |
|
||||||
|
|
||||||
## Maven Troubleshooting
|
## Maven Troubleshooting
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -108,10 +152,10 @@ java -version
|
|||||||
./gradlew -q javaToolchains
|
./gradlew -q javaToolchains
|
||||||
```
|
```
|
||||||
|
|
||||||
## Spring Boot Specific
|
## [SPRING] Spring Boot Specific Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Verify Spring Boot application context loads
|
# Verify application context loads
|
||||||
./mvnw spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=test"
|
./mvnw spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=test"
|
||||||
|
|
||||||
# Check for missing beans or circular dependencies
|
# Check for missing beans or circular dependencies
|
||||||
@ -119,6 +163,40 @@ java -version
|
|||||||
|
|
||||||
# Verify Lombok is configured as annotation processor (not just dependency)
|
# Verify Lombok is configured as annotation processor (not just dependency)
|
||||||
grep -A5 "annotationProcessorPaths\|annotationProcessor" pom.xml build.gradle
|
grep -A5 "annotationProcessorPaths\|annotationProcessor" pom.xml build.gradle
|
||||||
|
|
||||||
|
# Check Spring Boot version alignment
|
||||||
|
./mvnw dependency:tree | grep "org.springframework.boot"
|
||||||
|
```
|
||||||
|
|
||||||
|
## [QUARKUS] Quarkus Specific Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify Quarkus build augmentation
|
||||||
|
./mvnw quarkus:build -q
|
||||||
|
|
||||||
|
# Run in dev mode to surface runtime errors
|
||||||
|
./mvnw quarkus:dev
|
||||||
|
|
||||||
|
# List installed extensions
|
||||||
|
./mvnw quarkus:list-extensions -q 2>&1 | grep "✓\|installed"
|
||||||
|
|
||||||
|
# Add a missing extension
|
||||||
|
./mvnw quarkus:add-extension -Dextensions="<extension-name>"
|
||||||
|
|
||||||
|
# Check Quarkus BOM version alignment
|
||||||
|
./mvnw dependency:tree | grep "io.quarkus"
|
||||||
|
|
||||||
|
# Verify native build prerequisites (GraalVM)
|
||||||
|
./mvnw package -Pnative -DskipTests 2>&1 | head -50
|
||||||
|
|
||||||
|
# Debug build-time augmentation failures
|
||||||
|
./mvnw compile -X 2>&1 | grep -i "augment\|build step\|extension"
|
||||||
|
|
||||||
|
# Check for reflection issues (native image)
|
||||||
|
grep -rn "@RegisterForReflection" src/main/java --include="*.java"
|
||||||
|
|
||||||
|
# Verify CDI bean discovery
|
||||||
|
./mvnw quarkus:dev 2>&1 | grep -i "bean\|unsatisfied\|ambiguous"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Principles
|
## Key Principles
|
||||||
@ -129,6 +207,8 @@ grep -A5 "annotationProcessorPaths\|annotationProcessor" pom.xml build.gradle
|
|||||||
- **Always** run the build after each fix to verify
|
- **Always** run the build after each fix to verify
|
||||||
- Fix root cause over suppressing symptoms
|
- Fix root cause over suppressing symptoms
|
||||||
- Prefer adding missing imports over changing logic
|
- Prefer adding missing imports over changing logic
|
||||||
|
- **[QUARKUS]**: Prefer `quarkus ext add` over manually editing `pom.xml` for extensions
|
||||||
|
- **[QUARKUS]**: Always check if `@RegisterForReflection` is needed before adding reflection config manually
|
||||||
- Check `pom.xml`, `build.gradle`, or `build.gradle.kts` to confirm the build tool before running commands
|
- Check `pom.xml`, `build.gradle`, or `build.gradle.kts` to confirm the build tool before running commands
|
||||||
|
|
||||||
## Stop Conditions
|
## Stop Conditions
|
||||||
@ -138,16 +218,20 @@ Stop and report if:
|
|||||||
- Fix introduces more errors than it resolves
|
- Fix introduces more errors than it resolves
|
||||||
- Error requires architectural changes beyond scope
|
- Error requires architectural changes beyond scope
|
||||||
- Missing external dependencies that need user decision (private repos, licences)
|
- Missing external dependencies that need user decision (private repos, licences)
|
||||||
|
- **[QUARKUS]**: Native image build fails due to GraalVM not being installed — report prerequisite
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
Framework: [SPRING|QUARKUS|UNKNOWN]
|
||||||
[FIXED] src/main/java/com/example/service/PaymentService.java:87
|
[FIXED] src/main/java/com/example/service/PaymentService.java:87
|
||||||
Error: cannot find symbol — symbol: class IdempotencyKey
|
Error: cannot find symbol — symbol: class IdempotencyKey
|
||||||
Fix: Added import com.example.domain.IdempotencyKey
|
Fix: Added import com.example.domain.IdempotencyKey
|
||||||
Remaining errors: 1
|
Remaining errors: 1
|
||||||
```
|
```
|
||||||
|
|
||||||
Final: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
Final: `Framework: X | Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||||
|
|
||||||
For detailed Java and Spring Boot patterns, see `skill: springboot-patterns`.
|
For detailed patterns and examples:
|
||||||
|
- **[SPRING]**: See `skill: springboot-patterns`
|
||||||
|
- **[QUARKUS]**: See `skill: quarkus-patterns`
|
||||||
|
|||||||
@ -1,65 +1,133 @@
|
|||||||
---
|
---
|
||||||
name: java-reviewer
|
name: java-reviewer
|
||||||
description: Expert Java and Spring Boot code reviewer specializing in layered architecture, JPA patterns, security, and concurrency. Use for all Java code changes. MUST BE USED for Spring Boot projects.
|
description: Expert Java code reviewer for Spring Boot and Quarkus projects. Automatically detects the framework and applies the appropriate review rules. Covers layered architecture, JPA/Panache, MongoDB, security, and concurrency. MUST BE USED for all Java code changes.
|
||||||
tools: ["Read", "Grep", "Glob", "Bash"]
|
tools: ["Read", "Grep", "Glob", "Bash"]
|
||||||
model: sonnet
|
model: sonnet
|
||||||
---
|
---
|
||||||
You are a senior Java engineer ensuring high standards of idiomatic Java and Spring Boot best practices.
|
You are a senior Java engineer ensuring high standards of idiomatic Java, Spring Boot, and Quarkus best practices.
|
||||||
When invoked:
|
|
||||||
|
## Framework Detection (run first)
|
||||||
|
|
||||||
|
Before reviewing any code, determine the framework:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Read the build file
|
||||||
|
cat pom.xml 2>/dev/null || cat build.gradle 2>/dev/null || cat build.gradle.kts 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
- If the build file contains `quarkus` → apply **[QUARKUS]** rules
|
||||||
|
- If the build file contains `spring-boot` → apply **[SPRING]** rules
|
||||||
|
- If both are present (unlikely) → flag as a finding and apply both rulesets
|
||||||
|
- If neither is detected → review using general Java rules only and note the ambiguity
|
||||||
|
|
||||||
|
Then proceed:
|
||||||
1. Run `git diff -- '*.java'` to see recent Java file changes
|
1. Run `git diff -- '*.java'` to see recent Java file changes
|
||||||
2. Run `mvn verify -q` or `./gradlew check` if available
|
2. Run the appropriate build check:
|
||||||
|
- **[SPRING]**: `./mvnw verify -q` or `./gradlew check`
|
||||||
|
- **[QUARKUS]**: `./mvnw verify -q` or `./gradlew check`
|
||||||
3. Focus on modified `.java` files
|
3. Focus on modified `.java` files
|
||||||
4. Begin review immediately
|
4. Begin review immediately
|
||||||
|
|
||||||
You DO NOT refactor or rewrite code — you report findings only.
|
You DO NOT refactor or rewrite code — you report findings only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Review Priorities
|
## Review Priorities
|
||||||
|
|
||||||
### CRITICAL -- Security
|
### CRITICAL -- Security
|
||||||
- **SQL injection**: String concatenation in `@Query` or `JdbcTemplate` — use bind parameters (`:param` or `?`)
|
- **SQL injection**: String concatenation in queries — use bind parameters (`:param` or `?`)
|
||||||
|
- **[SPRING]**: Watch for `@Query`, `JdbcTemplate`, `NamedParameterJdbcTemplate`
|
||||||
|
- **[QUARKUS]**: Watch for `@Query`, Panache custom queries, `EntityManager.createNativeQuery()`
|
||||||
- **Command injection**: User-controlled input passed to `ProcessBuilder` or `Runtime.exec()` — validate and sanitise before invocation
|
- **Command injection**: User-controlled input passed to `ProcessBuilder` or `Runtime.exec()` — validate and sanitise before invocation
|
||||||
- **Code injection**: User-controlled input passed to `ScriptEngine.eval(...)` — avoid executing untrusted scripts; prefer safe expression parsers or sandboxing
|
- **Code injection**: User-controlled input passed to `ScriptEngine.eval(...)` — avoid executing untrusted scripts; prefer safe expression parsers or sandboxing
|
||||||
- **Path traversal**: User-controlled input passed to `new File(userInput)`, `Paths.get(userInput)`, or `FileInputStream(userInput)` without `getCanonicalPath()` validation
|
- **Path traversal**: User-controlled input passed to `new File(userInput)`, `Paths.get(userInput)`, or `FileInputStream(userInput)` without `getCanonicalPath()` validation
|
||||||
- **Hardcoded secrets**: API keys, passwords, tokens in source — must come from environment or secrets manager
|
- **Hardcoded secrets**: API keys, passwords, tokens in source
|
||||||
- **PII/token logging**: `log.info(...)` calls near auth code that expose passwords or tokens
|
- **[SPRING]**: Must come from environment, `application.yml`, or secrets manager (Vault, AWS Secrets Manager)
|
||||||
- **Missing `@Valid`**: Raw `@RequestBody` without Bean Validation — never trust unvalidated input
|
- **[QUARKUS]**: Must come from `application.properties`, environment variables, or a secrets manager (e.g. `quarkus-vault`)
|
||||||
- **CSRF disabled without justification**: Stateless JWT APIs may disable it but must document why
|
- **PII/token logging**: Logging calls near auth code that expose passwords or tokens
|
||||||
|
- **[SPRING]**: `log.info(...)` via SLF4J
|
||||||
|
- **[QUARKUS]**: `Log.info(...)` or `@Logged` interceptors
|
||||||
|
- **Missing input validation**: Request bodies accepted without Bean Validation
|
||||||
|
- **[SPRING]**: Raw `@RequestBody` without `@Valid`
|
||||||
|
- **[QUARKUS]**: Raw `@RestForm` / `@BeanParam` / request body without `@Valid` or `@ConvertGroup`
|
||||||
|
- **CSRF disabled without justification**: Stateless JWT APIs may disable/omit it but must document why
|
||||||
|
- **[QUARKUS]**: Form-based endpoints must use `quarkus-csrf-reactive`
|
||||||
|
|
||||||
If any CRITICAL security issue is found, stop and escalate to `security-reviewer`.
|
If any CRITICAL security issue is found, stop and escalate to `security-reviewer`.
|
||||||
|
|
||||||
### CRITICAL -- Error Handling
|
### CRITICAL -- Error Handling
|
||||||
- **Swallowed exceptions**: Empty catch blocks or `catch (Exception e) {}` with no action
|
- **Swallowed exceptions**: Empty catch blocks or `catch (Exception e) {}` with no action
|
||||||
- **`.get()` on Optional**: Calling `repository.findById(id).get()` without `.isPresent()` — use `.orElseThrow()`
|
- **`.get()` on Optional**: Calling `.get()` without `.isPresent()` — use `.orElseThrow()`
|
||||||
- **Missing `@RestControllerAdvice`**: Exception handling scattered across controllers instead of centralised
|
- **[SPRING]**: `repository.findById(id).get()`
|
||||||
|
- **[QUARKUS]**: `repository.findByIdOptional(id).get()`
|
||||||
|
- **Missing centralised exception handling**:
|
||||||
|
- **[SPRING]**: No `@RestControllerAdvice` — exception handling scattered across controllers
|
||||||
|
- **[QUARKUS]**: No `ExceptionMapper<T>` or `@ServerExceptionMapper` — exception handling scattered across resources
|
||||||
- **Wrong HTTP status**: Returning `200 OK` with null body instead of `404`, or missing `201` on creation
|
- **Wrong HTTP status**: Returning `200 OK` with null body instead of `404`, or missing `201` on creation
|
||||||
|
|
||||||
### HIGH -- Spring Boot Architecture
|
### HIGH -- Architecture
|
||||||
- **Field injection**: `@Autowired` on fields is a code smell — constructor injection is required
|
- **Dependency injection style**:
|
||||||
- **Business logic in controllers**: Controllers must delegate to the service layer immediately
|
- **[SPRING]**: `@Autowired` on fields is a code smell — constructor injection is required
|
||||||
- **`@Transactional` on wrong layer**: Must be on service layer, not controller or repository
|
- **[QUARKUS]**: Bare field references expecting CDI — must use `@Inject` or constructor injection
|
||||||
- **Missing `@Transactional(readOnly = true)`**: Read-only service methods must declare this
|
- **[QUARKUS] `@Singleton` vs `@ApplicationScoped`**: `@Singleton` beans are not proxied and break lazy initialization and interception — prefer `@ApplicationScoped` unless explicitly needed
|
||||||
- **Entity exposed in response**: JPA entity returned directly from controller — use DTO or record projection
|
- **Business logic in controllers/resources**: Must delegate to the service layer immediately
|
||||||
|
- **`@Transactional` on wrong layer**: Must be on service layer, not controller/resource or repository
|
||||||
|
- **[SPRING]**: Missing `@Transactional(readOnly = true)` on read-only service methods
|
||||||
|
- **[QUARKUS]**: Missing `@Transactional` on mutating Panache calls — active-record `persist()`, `delete()`, `update()` outside a transactional context will fail
|
||||||
|
- **Entity exposed in response**: JPA/Panache entity returned directly from controller/resource — use DTO or record projection
|
||||||
|
- **[QUARKUS] Blocking call on reactive thread**: Calling blocking I/O (JDBC, file I/O, `Thread.sleep()`) from a `@NonBlocking` endpoint or `Uni`/`Multi` pipeline — use `@Blocking`, `Uni.createFrom().item(() -> ...)` with `.runSubscriptionOn(executor)`, or the reactive client
|
||||||
|
|
||||||
### HIGH -- JPA / Database
|
### HIGH -- JPA / Relational Database
|
||||||
- **N+1 query problem**: `FetchType.EAGER` on collections — use `JOIN FETCH` or `@EntityGraph`
|
- **N+1 query problem**: `FetchType.EAGER` on collections — use `JOIN FETCH` or `@EntityGraph` / `@NamedEntityGraph`
|
||||||
- **Unbounded list endpoints**: Returning `List<T>` from endpoints without `Pageable` and `Page<T>`
|
- **Unbounded list endpoints**:
|
||||||
|
- **[SPRING]**: Returning `List<T>` without `Pageable` and `Page<T>`
|
||||||
|
- **[QUARKUS]**: Returning `List<T>` without `PanacheQuery.page(Page.of(...))`
|
||||||
- **Missing `@Modifying`**: Any `@Query` that mutates data requires `@Modifying` + `@Transactional`
|
- **Missing `@Modifying`**: Any `@Query` that mutates data requires `@Modifying` + `@Transactional`
|
||||||
- **Dangerous cascade**: `CascadeType.ALL` with `orphanRemoval = true` — confirm intent is deliberate
|
- **Dangerous cascade**: `CascadeType.ALL` with `orphanRemoval = true` — confirm intent is deliberate
|
||||||
|
- **[QUARKUS] Active record misuse**: Mixing `PanacheEntity` and `PanacheRepository` in the same bounded context — pick one and stay consistent
|
||||||
|
|
||||||
|
### HIGH -- Panache MongoDB [QUARKUS only]
|
||||||
|
- **Missing codec or serialisation config**: Custom types in documents without a registered `Codec` or proper BSON annotation — causes silent serialisation failures
|
||||||
|
- **Unbounded `listAll()` / `findAll()`**: Using `PanacheMongoEntity.listAll()` or `PanacheMongoRepository.listAll()` without pagination — use `.find(query).page(Page.of(index, size))`
|
||||||
|
- **No index on query fields**: Querying by fields not covered by a MongoDB index — define indexes via `@MongoEntity(collection = "...")` + migration scripts or `createIndex()` at startup
|
||||||
|
- **ObjectId vs custom ID confusion**: Using `String` id fields without explicit `@BsonId` or `@MongoEntity` configuration — leads to `_id` mapping issues; prefer `ObjectId` or document the custom ID strategy
|
||||||
|
- **Blocking MongoDB client on reactive thread**: Using the classic `MongoClient` (blocking) in a reactive pipeline — use `ReactiveMongoClient` and return `Uni<T>` / `Multi<T>`
|
||||||
|
- **Active record misuse**: Mixing `PanacheMongoEntity` and `PanacheMongoRepository` in the same bounded context — pick one and stay consistent
|
||||||
|
- **Missing `@Transactional` awareness**: MongoDB multi-document transactions require an explicit `ClientSession` — Panache MongoDB does not auto-manage transactions like Hibernate ORM; document the consistency guarantees
|
||||||
|
|
||||||
|
### MEDIUM -- NoSQL General
|
||||||
|
- **Schema evolution without migration strategy**: Changing document shapes without a versioned migration plan (e.g. a `schemaVersion` field or migration script) — leads to runtime deserialization failures on old documents
|
||||||
|
- **Storing large blobs in documents**: Embedding large binary data directly in documents instead of using GridFS or external storage — causes memory pressure and hits the 16 MB BSON limit
|
||||||
|
- **Overly nested documents**: Deeply nested document structures that should be modelled as separate collections with references — query and update complexity grows exponentially
|
||||||
|
- **Missing TTL or expiry policy**: Time-sensitive data (sessions, tokens, caches) stored without a TTL index — leads to unbounded collection growth
|
||||||
|
- **No read preference / write concern configuration**: Production deployments using defaults without evaluating consistency requirements
|
||||||
|
|
||||||
### MEDIUM -- Concurrency and State
|
### MEDIUM -- Concurrency and State
|
||||||
- **Mutable singleton fields**: Non-final instance fields in `@Service` / `@Component` are a race condition
|
- **Mutable singleton fields**: Non-final instance fields in singleton-scoped beans are a race condition
|
||||||
- **Unbounded `@Async`**: `CompletableFuture` or `@Async` without a custom `Executor` — default creates unbounded threads
|
- **[SPRING]**: `@Service` / `@Component`
|
||||||
|
- **[QUARKUS]**: `@ApplicationScoped` / `@Singleton`
|
||||||
|
- **Unbounded async execution**:
|
||||||
|
- **[SPRING]**: `CompletableFuture` or `@Async` without a custom `Executor` — default creates unbounded threads
|
||||||
|
- **[QUARKUS]**: `ExecutorService.submit()` or `@ActivateRequestContext` with `@Async` without a managed `ManagedExecutor`
|
||||||
- **Blocking `@Scheduled`**: Long-running scheduled methods that block the scheduler thread
|
- **Blocking `@Scheduled`**: Long-running scheduled methods that block the scheduler thread
|
||||||
|
- **[QUARKUS]**: Use `concurrentExecution = SKIP` or offload to a worker thread
|
||||||
|
- **[QUARKUS] Reactive stream misuse**: Building `Uni`/`Multi` pipelines that subscribe more than once or share mutable state between subscribers
|
||||||
|
|
||||||
### MEDIUM -- Java Idioms and Performance
|
### MEDIUM -- Java Idioms and Performance
|
||||||
- **String concatenation in loops**: Use `StringBuilder` or `String.join`
|
- **String concatenation in loops**: Use `StringBuilder` or `String.join`
|
||||||
- **Raw type usage**: Unparameterised generics (`List` instead of `List<T>`)
|
- **Raw type usage**: Unparameterised generics (`List` instead of `List<T>`)
|
||||||
- **Missed pattern matching**: `instanceof` check followed by explicit cast — use pattern matching (Java 16+)
|
- **Missed pattern matching**: `instanceof` check followed by explicit cast — use pattern matching (Java 16+)
|
||||||
- **Null returns from service layer**: Prefer `Optional<T>` over returning null
|
- **Null returns from service layer**: Prefer `Optional<T>` over returning null
|
||||||
|
- **[QUARKUS] Not leveraging build-time init**: Using runtime reflection or classpath scanning that could be replaced by Quarkus build-time extensions or `@RegisterForReflection`
|
||||||
|
|
||||||
### MEDIUM -- Testing
|
### MEDIUM -- Testing
|
||||||
- **`@SpringBootTest` for unit tests**: Use `@WebMvcTest` for controllers, `@DataJpaTest` for repositories
|
- **Over-scoped test annotations**:
|
||||||
- **Missing Mockito extension**: Service tests must use `@ExtendWith(MockitoExtension.class)`
|
- **[SPRING]**: `@SpringBootTest` for unit tests — use `@WebMvcTest` for controllers, `@DataJpaTest` for repositories
|
||||||
|
- **[QUARKUS]**: `@QuarkusTest` for unit tests — reserve for integration tests; use plain JUnit 5 + Mockito for units
|
||||||
|
- **Missing mock setup**:
|
||||||
|
- **[SPRING]**: Service tests must use `@ExtendWith(MockitoExtension.class)`
|
||||||
|
- **[QUARKUS]**: `@InjectMock` misuse — reserve for CDI integration tests, use plain Mockito for unit tests
|
||||||
|
- **[QUARKUS] Missing `@QuarkusTestResource`**: Integration tests requiring external services should use Dev Services or `@QuarkusTestResource` with Testcontainers
|
||||||
- **`Thread.sleep()` in tests**: Use `Awaitility` for async assertions
|
- **`Thread.sleep()` in tests**: Use `Awaitility` for async assertions
|
||||||
- **Weak test names**: `testFindUser` gives no information — use `should_return_404_when_user_not_found`
|
- **Weak test names**: `testFindUser` gives no information — use `should_return_404_when_user_not_found`
|
||||||
|
|
||||||
@ -68,25 +136,45 @@ If any CRITICAL security issue is found, stop and escalate to `security-reviewer
|
|||||||
- **Illegal state transitions**: No guard on transitions like `CANCELLED → PROCESSING`
|
- **Illegal state transitions**: No guard on transitions like `CANCELLED → PROCESSING`
|
||||||
- **Non-atomic compensation**: Rollback/compensation logic that can partially succeed
|
- **Non-atomic compensation**: Rollback/compensation logic that can partially succeed
|
||||||
- **Missing jitter on retry**: Exponential backoff without jitter causes thundering herd
|
- **Missing jitter on retry**: Exponential backoff without jitter causes thundering herd
|
||||||
|
- **[SPRING]**: Check Spring Retry configuration
|
||||||
|
- **[QUARKUS]**: Check `@Retry` from MicroProfile Fault Tolerance
|
||||||
- **No dead-letter handling**: Failed async events with no fallback or alerting
|
- **No dead-letter handling**: Failed async events with no fallback or alerting
|
||||||
|
- **[SPRING]**: Spring Kafka / AMQP error handlers
|
||||||
|
- **[QUARKUS]**: SmallRye Reactive Messaging `@Incoming` dead-letter or `nack` strategy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Diagnostic Commands
|
## Diagnostic Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Common
|
||||||
git diff -- '*.java'
|
git diff -- '*.java'
|
||||||
mvn verify -q
|
|
||||||
./gradlew check # Gradle equivalent
|
# Build & verify
|
||||||
./mvnw checkstyle:check # style
|
./mvnw verify -q # Maven
|
||||||
./mvnw spotbugs:check # static analysis
|
./gradlew check # Gradle
|
||||||
./mvnw test # unit tests
|
|
||||||
|
# Static analysis
|
||||||
|
./mvnw checkstyle:check
|
||||||
|
./mvnw spotbugs:check
|
||||||
./mvnw dependency-check:check # CVE scan (OWASP plugin)
|
./mvnw dependency-check:check # CVE scan (OWASP plugin)
|
||||||
grep -rn "@Autowired" src/main/java --include="*.java"
|
|
||||||
|
# Framework detection greps
|
||||||
|
grep -rn "@Autowired" src/main/java --include="*.java" # [SPRING]
|
||||||
|
grep -rn "@Inject" src/main/java --include="*.java" # [QUARKUS]
|
||||||
grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
||||||
|
grep -rn "@Singleton" src/main/java --include="*.java" # [QUARKUS]
|
||||||
|
grep -rn "listAll\|findAll" src/main/java --include="*.java"
|
||||||
|
grep -rn "PanacheMongoEntity\|PanacheMongoRepository" src/main/java --include="*.java" # [QUARKUS]
|
||||||
```
|
```
|
||||||
Read `pom.xml`, `build.gradle`, or `build.gradle.kts` to determine the build tool and Spring Boot version before reviewing.
|
|
||||||
|
Read `pom.xml`, `build.gradle`, or `build.gradle.kts` to determine the build tool and framework version before reviewing.
|
||||||
|
|
||||||
## Approval Criteria
|
## Approval Criteria
|
||||||
- **Approve**: No CRITICAL or HIGH issues
|
- **Approve**: No CRITICAL or HIGH issues
|
||||||
- **Warning**: MEDIUM issues only
|
- **Warning**: MEDIUM issues only
|
||||||
- **Block**: CRITICAL or HIGH issues found
|
- **Block**: CRITICAL or HIGH issues found
|
||||||
|
|
||||||
For detailed Spring Boot patterns and examples, see `skill: springboot-patterns`.
|
For detailed patterns and examples:
|
||||||
|
- **[SPRING]**: See `skill: springboot-patterns`
|
||||||
|
- **[QUARKUS]**: See `skill: quarkus-patterns`
|
||||||
|
|||||||
@ -228,6 +228,10 @@ everything-claude-code/
|
|||||||
| |-- django-verification/ # Django 検証ループ(新規)
|
| |-- django-verification/ # Django 検証ループ(新規)
|
||||||
| |-- python-patterns/ # Python イディオムとベストプラクティス(新規)
|
| |-- python-patterns/ # Python イディオムとベストプラクティス(新規)
|
||||||
| |-- python-testing/ # pytest を使った Python テスト(新規)
|
| |-- python-testing/ # pytest を使った Python テスト(新規)
|
||||||
|
| |-- quarkus-patterns/ # Quarkus アーキテクチャ、Camel、CDI、Panache パターン(新規)
|
||||||
|
| |-- quarkus-security/ # Quarkus セキュリティ: JWT/OIDC、RBAC、バリデーション(新規)
|
||||||
|
| |-- quarkus-tdd/ # Quarkus TDD: JUnit 5、Mockito、REST Assured(新規)
|
||||||
|
| |-- quarkus-verification/ # Quarkus 検証: ビルド、テスト、ネイティブコンパイル(新規)
|
||||||
| |-- springboot-patterns/ # Java Spring Boot パターン(新規)
|
| |-- springboot-patterns/ # Java Spring Boot パターン(新規)
|
||||||
| |-- springboot-security/ # Spring Boot セキュリティ(新規)
|
| |-- springboot-security/ # Spring Boot セキュリティ(新規)
|
||||||
| |-- springboot-tdd/ # Spring Boot TDD(新規)
|
| |-- springboot-tdd/ # Spring Boot TDD(新規)
|
||||||
|
|||||||
@ -19,6 +19,10 @@
|
|||||||
- `django-patterns/` - Django ベストプラクティス
|
- `django-patterns/` - Django ベストプラクティス
|
||||||
- `django-tdd/` - Django テスト駆動開発
|
- `django-tdd/` - Django テスト駆動開発
|
||||||
- `django-security/` - Django セキュリティ
|
- `django-security/` - Django セキュリティ
|
||||||
|
- `quarkus-patterns/` - Quarkus アーキテクチャ、Camel、CDI、Panache パターン
|
||||||
|
- `quarkus-security/` - Quarkus セキュリティ: JWT/OIDC、RBAC、バリデーション
|
||||||
|
- `quarkus-tdd/` - Quarkus テスト駆動開発
|
||||||
|
- `quarkus-verification/` - Quarkus 検証ループ
|
||||||
- `springboot-patterns/` - Spring Boot パターン
|
- `springboot-patterns/` - Spring Boot パターン
|
||||||
- `springboot-tdd/` - Spring Boot テスト
|
- `springboot-tdd/` - Spring Boot テスト
|
||||||
- `springboot-security/` - Spring Boot セキュリティ
|
- `springboot-security/` - Spring Boot セキュリティ
|
||||||
|
|||||||
@ -65,7 +65,7 @@ mkdir -p $TARGET/skills $TARGET/rules
|
|||||||
|
|
||||||
### 2a: スキルカテゴリの選択
|
### 2a: スキルカテゴリの選択
|
||||||
|
|
||||||
27個のスキルが4つのカテゴリに分類されています。`multiSelect: true` で `AskUserQuestion` を使用します:
|
31個のスキルが4つのカテゴリに分類されています。`multiSelect: true` で `AskUserQuestion` を使用します:
|
||||||
|
|
||||||
```
|
```
|
||||||
Question: "どのスキルカテゴリをインストールしますか?"
|
Question: "どのスキルカテゴリをインストールしますか?"
|
||||||
@ -80,7 +80,7 @@ Options:
|
|||||||
|
|
||||||
選択された各カテゴリについて、以下の完全なスキルリストを表示し、ユーザーに確認または特定のものの選択解除を依頼します。リストが4項目を超える場合、リストをテキストとして表示し、`AskUserQuestion` で「リストされたすべてをインストール」オプションと、ユーザーが特定の名前を貼り付けるための「その他」オプションを使用します。
|
選択された各カテゴリについて、以下の完全なスキルリストを表示し、ユーザーに確認または特定のものの選択解除を依頼します。リストが4項目を超える場合、リストをテキストとして表示し、`AskUserQuestion` で「リストされたすべてをインストール」オプションと、ユーザーが特定の名前を貼り付けるための「その他」オプションを使用します。
|
||||||
|
|
||||||
**カテゴリ: Framework & Language(16スキル)**
|
**カテゴリ: Framework & Language(20スキル)**
|
||||||
|
|
||||||
| スキル | 説明 |
|
| スキル | 説明 |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
@ -96,6 +96,10 @@ Options:
|
|||||||
| `java-coding-standards` | Spring Boot 用 Java コーディング標準: 命名、不変性、Optional、ストリーム |
|
| `java-coding-standards` | Spring Boot 用 Java コーディング標準: 命名、不変性、Optional、ストリーム |
|
||||||
| `python-patterns` | Pythonic なイディオム、PEP 8、型ヒント、ベストプラクティス |
|
| `python-patterns` | Pythonic なイディオム、PEP 8、型ヒント、ベストプラクティス |
|
||||||
| `python-testing` | pytest、TDD、フィクスチャ、モック、パラメータ化による Python テスト |
|
| `python-testing` | pytest、TDD、フィクスチャ、モック、パラメータ化による Python テスト |
|
||||||
|
| `quarkus-patterns` | Quarkus アーキテクチャ、Camel メッセージング、CDI サービス、Panache データアクセス |
|
||||||
|
| `quarkus-security` | Quarkus セキュリティ: JWT/OIDC、RBAC、入力バリデーション、シークレット管理 |
|
||||||
|
| `quarkus-tdd` | JUnit 5、Mockito、REST Assured、Camel テストによる Quarkus TDD |
|
||||||
|
| `quarkus-verification` | Quarkus 検証: ビルド、静的解析、テスト、ネイティブコンパイル |
|
||||||
| `springboot-patterns` | Spring Boot アーキテクチャ、REST API、レイヤードサービス、キャッシング、非同期 |
|
| `springboot-patterns` | Spring Boot アーキテクチャ、REST API、レイヤードサービス、キャッシング、非同期 |
|
||||||
| `springboot-security` | Spring Security: 認証/認可、検証、CSRF、シークレット、レート制限 |
|
| `springboot-security` | Spring Security: 認証/認可、検証、CSRF、シークレット、レート制限 |
|
||||||
| `springboot-tdd` | JUnit 5、Mockito、MockMvc、Testcontainers による Spring Boot TDD |
|
| `springboot-tdd` | JUnit 5、Mockito、MockMvc、Testcontainers による Spring Boot TDD |
|
||||||
|
|||||||
754
docs/ja-JP/skills/quarkus-patterns/SKILL.md
Normal file
754
docs/ja-JP/skills/quarkus-patterns/SKILL.md
Normal file
@ -0,0 +1,754 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-patterns
|
||||||
|
description: Quarkus 3.x LTS architecture patterns with Camel for messaging, RESTful API design, CDI services, data access with Panache, and async processing. Use for Java Quarkus backend work with event-driven architectures.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Development Patterns
|
||||||
|
|
||||||
|
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Building REST APIs with JAX-RS or RESTEasy Reactive
|
||||||
|
- Structuring resource → service → repository layers
|
||||||
|
- Implementing event-driven patterns with Apache Camel and RabbitMQ
|
||||||
|
- Configuring Hibernate Panache, caching, or reactive streams
|
||||||
|
- Adding validation, exception mapping, or pagination
|
||||||
|
- Setting up profiles for dev/staging/production environments (YAML config)
|
||||||
|
- Custom logging with LogContext and Logback/Logstash encoder
|
||||||
|
- Working with CompletableFuture for async operations
|
||||||
|
- Implementing conditional flow processing
|
||||||
|
- Working with GraalVM native compilation
|
||||||
|
|
||||||
|
## Service Layer with Multiple Dependencies (Lombok)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class As2ProcessingService {
|
||||||
|
|
||||||
|
private final InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
private final EventService eventService;
|
||||||
|
private final DocumentJobService documentJobService;
|
||||||
|
private final BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
private final FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
public void processFile(Path filePath) throws Exception {
|
||||||
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
|
||||||
|
String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID);
|
||||||
|
|
||||||
|
// Conditional flow logic
|
||||||
|
boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW));
|
||||||
|
log.info("Is CHORUS_FLOW message: {}", isChorusFlow);
|
||||||
|
|
||||||
|
ValidationFlowConfig validationFlowConfig = isChorusFlow
|
||||||
|
? ValidationFlowConfig.xsdOnly()
|
||||||
|
: ValidationFlowConfig.allValidations();
|
||||||
|
|
||||||
|
InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator
|
||||||
|
.validateFlowWithConfig(filePath, validationFlowConfig,
|
||||||
|
EInvoiceSyntaxFormat.UBL, logContext);
|
||||||
|
|
||||||
|
FlowProfile flowProfile = isChorusFlow ?
|
||||||
|
FlowProfile.EXTENDED_CTC_FR :
|
||||||
|
this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult,
|
||||||
|
invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile());
|
||||||
|
|
||||||
|
log.info("Invoice validation completed. Message is valid");
|
||||||
|
|
||||||
|
// CompletableFuture async operation
|
||||||
|
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
||||||
|
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
||||||
|
fileStorageService.uploadOriginalFile(inputStream,
|
||||||
|
invoiceValidationResult.getSize(), logContext,
|
||||||
|
invoiceValidationResult.getInvoiceFormat());
|
||||||
|
|
||||||
|
StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join();
|
||||||
|
log.info("File uploaded successfully: {}", documentInfo.getPath());
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(documentInfo.getPath())) {
|
||||||
|
String errorMsg = "File path is empty after upload";
|
||||||
|
log.error(errorMsg);
|
||||||
|
this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg);
|
||||||
|
throw new As2ServerProcessingException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE");
|
||||||
|
|
||||||
|
BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities(
|
||||||
|
documentInfo, originalFileName, structureIdPartner,
|
||||||
|
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||||
|
|
||||||
|
// Async Camel publishing
|
||||||
|
businessRulesPublisher.publishAsync(payload);
|
||||||
|
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Patterns:**
|
||||||
|
- `@RequiredArgsConstructor` for constructor injection via Lombok
|
||||||
|
- `@Slf4j` for Logback logging
|
||||||
|
- Scoped LogContext with try-with-resources
|
||||||
|
- Conditional flow logic based on runtime parameters
|
||||||
|
- CompletableFuture with `.join()` for async operations
|
||||||
|
- Event tracking for success/error scenarios
|
||||||
|
- Async Camel message publishing
|
||||||
|
|
||||||
|
## Custom Logging Context Pattern (Logback)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ProcessingService {
|
||||||
|
|
||||||
|
public void processDocument(Document doc) {
|
||||||
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
// Add context to all log statements
|
||||||
|
logContext.put("documentId", doc.getId().toString());
|
||||||
|
logContext.put("documentType", doc.getType());
|
||||||
|
logContext.put("userId", SecurityContext.getUserId());
|
||||||
|
|
||||||
|
log.info("Starting document processing");
|
||||||
|
|
||||||
|
// All logs within this scope inherit the context
|
||||||
|
processInternal(doc);
|
||||||
|
|
||||||
|
log.info("Document processing completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Document processing failed", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logback Configuration (logback.xml):**
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<configuration>
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||||
|
<includeContext>true</includeContext>
|
||||||
|
<includeMdc>true</includeMdc>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="com.example" level="INFO"/>
|
||||||
|
<root level="WARN">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Service Pattern
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EventService {
|
||||||
|
private final EventRepository eventRepository;
|
||||||
|
|
||||||
|
public void createSuccessEvent(Object payload, String eventType) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setType(eventType);
|
||||||
|
event.setStatus(EventStatus.SUCCESS);
|
||||||
|
event.setPayload(serializePayload(payload));
|
||||||
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
|
eventRepository.persist(event);
|
||||||
|
log.info("Success event created: {}", eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setType(eventType);
|
||||||
|
event.setStatus(EventStatus.ERROR);
|
||||||
|
event.setErrorMessage(errorMessage);
|
||||||
|
event.setPayload(serializePayload(payload));
|
||||||
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
|
eventRepository.persist(event);
|
||||||
|
log.error("Error event created: {} - {}", eventType, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializePayload(Object payload) {
|
||||||
|
// JSON serialization
|
||||||
|
return objectMapper.writeValueAsString(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Message Publishing (RabbitMQ)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BusinessRulesPublisher {
|
||||||
|
private final ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
|
String businessRulesQueue;
|
||||||
|
|
||||||
|
public void publishAsync(BusinessRulesPayload payload) {
|
||||||
|
producerTemplate.asyncSendBody(
|
||||||
|
"direct:business-rules-publisher",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
log.info("Message published to business rules queue: {}", payload.getDocumentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishSync(BusinessRulesPayload payload) {
|
||||||
|
producerTemplate.sendBody(
|
||||||
|
"direct:business-rules-publisher",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Camel Route Configuration:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class BusinessRulesRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
|
String businessRulesQueue;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "rabbitmq.host")
|
||||||
|
String rabbitHost;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "rabbitmq.port")
|
||||||
|
Integer rabbitPort;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("direct:business-rules-publisher")
|
||||||
|
.routeId("business-rules-publisher")
|
||||||
|
.log("Publishing message to RabbitMQ: ${body}")
|
||||||
|
.marshal().json(JsonLibrary.Jackson)
|
||||||
|
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
||||||
|
businessRulesQueue, rabbitHost, rabbitPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Direct Routes (In-Memory)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class DocumentProcessingRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
// Error handling
|
||||||
|
onException(ValidationException.class)
|
||||||
|
.handled(true)
|
||||||
|
.to("direct:validation-error-handler")
|
||||||
|
.log("Validation error: ${exception.message}");
|
||||||
|
|
||||||
|
// Main processing route
|
||||||
|
from("direct:process-document")
|
||||||
|
.routeId("document-processing")
|
||||||
|
.log("Processing document: ${header.documentId}")
|
||||||
|
.bean(DocumentValidator.class, "validate")
|
||||||
|
.bean(DocumentTransformer.class, "transform")
|
||||||
|
.choice()
|
||||||
|
.when(header("documentType").isEqualTo("INVOICE"))
|
||||||
|
.to("direct:process-invoice")
|
||||||
|
.when(header("documentType").isEqualTo("CREDIT_NOTE"))
|
||||||
|
.to("direct:process-credit-note")
|
||||||
|
.otherwise()
|
||||||
|
.to("direct:process-generic")
|
||||||
|
.end();
|
||||||
|
|
||||||
|
from("direct:validation-error-handler")
|
||||||
|
.bean(EventService.class, "createErrorEvent")
|
||||||
|
.log("Validation error handled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel File Processing
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class FileMonitoringRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.input.directory")
|
||||||
|
String inputDirectory;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.processed.directory")
|
||||||
|
String processedDirectory;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.error.directory")
|
||||||
|
String errorDirectory;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
||||||
|
"&moveFailed=" + errorDirectory + "&delay=5000")
|
||||||
|
.routeId("file-monitor")
|
||||||
|
.log("Processing file: ${header.CamelFileName}")
|
||||||
|
.to("direct:process-file");
|
||||||
|
|
||||||
|
from("direct:process-file")
|
||||||
|
.bean(As2ProcessingService.class, "processFile")
|
||||||
|
.log("File processing completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Bean Invocation
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class InvoiceRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("direct:invoice-validation")
|
||||||
|
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
||||||
|
.log("Validation result: ${body}");
|
||||||
|
|
||||||
|
from("direct:persist-and-publish")
|
||||||
|
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
||||||
|
.bean(BusinessRulesPublisher.class, "publishAsync")
|
||||||
|
.bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST API Structure
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Path("/api/documents")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentResource {
|
||||||
|
private final DocumentService documentService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response list(
|
||||||
|
@QueryParam("page") @DefaultValue("0") int page,
|
||||||
|
@QueryParam("size") @DefaultValue("20") int size) {
|
||||||
|
PaginatedList<Document> documents = documentService.list(page, size);
|
||||||
|
return Response.ok(documents).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public Response create(@Valid CreateDocumentRequest request, @Context UriInfo uriInfo) {
|
||||||
|
Document document = documentService.create(request);
|
||||||
|
URI location = uriInfo.getAbsolutePathBuilder()
|
||||||
|
.path(String.valueOf(document.id))
|
||||||
|
.build();
|
||||||
|
return Response.created(location).entity(DocumentResponse.from(document)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
public Response getById(@PathParam("id") Long id) {
|
||||||
|
return documentService.findById(id)
|
||||||
|
.map(DocumentResponse::from)
|
||||||
|
.map(Response::ok)
|
||||||
|
.orElse(Response.status(Response.Status.NOT_FOUND))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Pattern (Panache Repository)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class DocumentRepository implements PanacheRepository<Document> {
|
||||||
|
|
||||||
|
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
||||||
|
return find("status = ?1 order by createdAt desc", status)
|
||||||
|
.page(page, size)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
||||||
|
return find("referenceNumber", referenceNumber).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
||||||
|
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Layer with Transactions
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentService {
|
||||||
|
private final DocumentRepository repo;
|
||||||
|
private final EventService eventService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Document create(CreateDocumentRequest request) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setReferenceNumber(request.referenceNumber());
|
||||||
|
document.setDescription(request.description());
|
||||||
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
|
document.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
|
repo.persist(document);
|
||||||
|
|
||||||
|
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findById(Long id) {
|
||||||
|
return repo.findByIdOptional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaginatedList<Document> list(int page, int size) {
|
||||||
|
return repo.findAll()
|
||||||
|
.page(page, size)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DTOs and Validation
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record CreateDocumentRequest(
|
||||||
|
@NotBlank @Size(max = 200) String referenceNumber,
|
||||||
|
@NotBlank @Size(max = 2000) String description,
|
||||||
|
@NotNull @FutureOrPresent Instant validUntil,
|
||||||
|
@NotEmpty List<@NotBlank String> categories) {}
|
||||||
|
|
||||||
|
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
||||||
|
public static DocumentResponse from(Document document) {
|
||||||
|
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
||||||
|
document.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exception Mapping
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
|
||||||
|
@Override
|
||||||
|
public Response toResponse(ConstraintViolationException exception) {
|
||||||
|
String message = exception.getConstraintViolations().stream()
|
||||||
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "validation_error", "message", message))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
@Slf4j
|
||||||
|
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(Exception exception) {
|
||||||
|
log.error("Unhandled exception", exception);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "internal_error", "message", "An unexpected error occurred"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CompletableFuture Async Operations
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FileStorageService {
|
||||||
|
private final S3Client s3Client;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
|
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
||||||
|
InputStream inputStream,
|
||||||
|
long size,
|
||||||
|
LogContext logContext,
|
||||||
|
InvoiceFormat format) {
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
String path = generateStoragePath(format);
|
||||||
|
|
||||||
|
PutObjectRequest request = PutObjectRequest.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.key(path)
|
||||||
|
.contentLength(size)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
||||||
|
|
||||||
|
log.info("File uploaded to S3: {}", path);
|
||||||
|
|
||||||
|
return new StoredDocumentInfo(path, size, Instant.now());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to upload file to S3", e);
|
||||||
|
throw new StorageException("Upload failed", e);
|
||||||
|
}
|
||||||
|
}, executorService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentCacheService {
|
||||||
|
private final DocumentRepository repo;
|
||||||
|
|
||||||
|
@CacheResult(cacheName = "document-cache")
|
||||||
|
public Optional<Document> getById(@CacheKey Long id) {
|
||||||
|
return repo.findByIdOptional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheInvalidate(cacheName = "document-cache")
|
||||||
|
public void evict(@CacheKey Long id) {}
|
||||||
|
|
||||||
|
@CacheInvalidateAll(cacheName = "document-cache")
|
||||||
|
public void evictAll() {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration as YAML
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# application.yml
|
||||||
|
"%dev":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: jdbc:postgresql://localhost:5432/dev_db
|
||||||
|
username: dev_user
|
||||||
|
password: dev_pass
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: drop-and-create
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: localhost
|
||||||
|
port: 5672
|
||||||
|
username: guest
|
||||||
|
password: guest
|
||||||
|
|
||||||
|
"%test":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: jdbc:h2:mem:test
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: drop-and-create
|
||||||
|
|
||||||
|
"%prod":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: ${DATABASE_URL}
|
||||||
|
username: ${DB_USER}
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: validate
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: ${RABBITMQ_HOST}
|
||||||
|
port: ${RABBITMQ_PORT}
|
||||||
|
username: ${RABBITMQ_USER}
|
||||||
|
password: ${RABBITMQ_PASSWORD}
|
||||||
|
|
||||||
|
# Camel configuration
|
||||||
|
camel:
|
||||||
|
rabbitmq:
|
||||||
|
queue:
|
||||||
|
business-rules: business-rules-queue
|
||||||
|
invoice-processing: invoice-processing-queue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Readiness
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DatabaseHealthCheck implements HealthCheck {
|
||||||
|
private final AgroalDataSource dataSource;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
try (Connection conn = dataSource.getConnection()) {
|
||||||
|
boolean valid = conn.isValid(2);
|
||||||
|
return HealthCheckResponse.named("Database connection")
|
||||||
|
.status(valid)
|
||||||
|
.build();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return HealthCheckResponse.down("Database connection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Liveness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class CamelHealthCheck implements HealthCheck {
|
||||||
|
@Inject
|
||||||
|
CamelContext camelContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
boolean isStarted = camelContext.getStatus().isStarted();
|
||||||
|
return HealthCheckResponse.named("Camel Context")
|
||||||
|
.status(isStarted)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies (Maven)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<properties>
|
||||||
|
<quarkus.platform.version>3.27.0</quarkus.platform.version>
|
||||||
|
<lombok.version>1.18.42</lombok.version>
|
||||||
|
<assertj-core.version>3.24.2</assertj-core.version>
|
||||||
|
<jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus.platform</groupId>
|
||||||
|
<artifactId>quarkus-bom</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-bom</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Quarkus Core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-arc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Camel Extensions -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-direct</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-bean</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Logging -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||||
|
<artifactId>quarkus-logging-logback</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.logstash.logback</groupId>
|
||||||
|
<artifactId>logstash-logback-encoder</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- Use `@RequiredArgsConstructor` with Lombok for constructor injection
|
||||||
|
- Keep service layer thin; delegate complex logic to specialized classes
|
||||||
|
- Use Camel routes for message routing and integration patterns
|
||||||
|
- Prefer Panache Repository pattern for data access
|
||||||
|
|
||||||
|
### Event-Driven
|
||||||
|
- Always track operations with EventService (success/error events)
|
||||||
|
- Use Camel `direct:` endpoints for in-memory routing
|
||||||
|
- Use `spring-rabbitmq` component for RabbitMQ integration
|
||||||
|
- Implement async publishing with `ProducerTemplate.asyncSendBody()`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Use Logback with Logstash encoder for structured logging
|
||||||
|
- Propagate LogContext through service calls with `SafeAutoCloseable`
|
||||||
|
- Add contextual information to LogContext for request tracing
|
||||||
|
- Use `@Slf4j` instead of manual logger instantiation
|
||||||
|
|
||||||
|
### Async Operations
|
||||||
|
- Use CompletableFuture for non-blocking I/O operations
|
||||||
|
- Call `.join()` when you need to wait for completion
|
||||||
|
- Handle exceptions from CompletableFuture properly
|
||||||
|
- Pass LogContext to async operations for tracing
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Use YAML configuration (`quarkus-config-yaml`)
|
||||||
|
- Profile-aware configuration for dev/test/prod environments
|
||||||
|
- Externalize sensitive configuration to environment variables
|
||||||
|
- Use `@ConfigProperty` for type-safe config injection
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
- Validate at resource layer with `@Valid`
|
||||||
|
- Use Bean Validation annotations on DTOs
|
||||||
|
- Map exceptions to proper HTTP responses with `@Provider`
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
- Use `@Transactional` on service methods that modify data
|
||||||
|
- Keep transactions short and focused
|
||||||
|
- Avoid calling async operations within transactions
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Use `camel-quarkus-junit5` for route testing
|
||||||
|
- Use AssertJ for assertions
|
||||||
|
- Mock all external dependencies
|
||||||
|
- Test conditional flow logic thoroughly
|
||||||
|
|
||||||
|
### Quarkus-Specific
|
||||||
|
- Stay on latest LTS version (3.x)
|
||||||
|
- Use Quarkus dev mode for hot reload
|
||||||
|
- Add health checks for production readiness
|
||||||
|
- Test native compilation compatibility periodically
|
||||||
453
docs/ja-JP/skills/quarkus-security/SKILL.md
Normal file
453
docs/ja-JP/skills/quarkus-security/SKILL.md
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-security
|
||||||
|
description: Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Security Review
|
||||||
|
|
||||||
|
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Adding authentication (JWT, OIDC, Basic Auth)
|
||||||
|
- Implementing authorization with @RolesAllowed or SecurityIdentity
|
||||||
|
- Validating user input (Bean Validation, custom validators)
|
||||||
|
- Configuring CORS or security headers
|
||||||
|
- Managing secrets (Vault, environment variables, config sources)
|
||||||
|
- Adding rate limiting or brute-force protection
|
||||||
|
- Scanning dependencies for CVEs
|
||||||
|
- Working with MicroProfile JWT or SmallRye JWT
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### JWT Authentication
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Resource protected with JWT
|
||||||
|
@Path("/api/protected")
|
||||||
|
@Authenticated
|
||||||
|
public class ProtectedResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JsonWebToken jwt;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response getData() {
|
||||||
|
String username = jwt.getName();
|
||||||
|
Set<String> roles = jwt.getGroups();
|
||||||
|
return Response.ok(Map.of(
|
||||||
|
"username", username,
|
||||||
|
"roles", roles,
|
||||||
|
"principal", securityIdentity.getPrincipal().getName()
|
||||||
|
)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration (application.properties):
|
||||||
|
```properties
|
||||||
|
mp.jwt.verify.publickey.location=publicKey.pem
|
||||||
|
mp.jwt.verify.issuer=https://auth.example.com
|
||||||
|
|
||||||
|
# OIDC
|
||||||
|
quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm
|
||||||
|
quarkus.oidc.client-id=backend-service
|
||||||
|
quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Authentication Filter
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
|
public class CustomAuthFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity identity;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
// Validate token and set SecurityIdentity
|
||||||
|
if (!validateToken(token)) {
|
||||||
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateToken(String token) {
|
||||||
|
// Token validation logic
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
### Role-Based Access Control
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Path("/api/admin")
|
||||||
|
@RolesAllowed("ADMIN")
|
||||||
|
public class AdminResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/users")
|
||||||
|
public List<UserDto> listUsers() {
|
||||||
|
return userService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/users/{id}")
|
||||||
|
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
||||||
|
public Response deleteUser(@PathParam("id") Long id) {
|
||||||
|
userService.delete(id);
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/api/users")
|
||||||
|
public class UserResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
public Response getUser(@PathParam("id") Long id) {
|
||||||
|
// Check ownership
|
||||||
|
if (!securityIdentity.hasRole("ADMIN") &&
|
||||||
|
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
return Response.ok(userService.findById(id)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOwner(Long userId, String username) {
|
||||||
|
return userService.isOwner(userId, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Security
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SecurityService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public boolean canAccessResource(Long resourceId) {
|
||||||
|
if (securityIdentity.isAnonymous()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (securityIdentity.hasRole("ADMIN")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = securityIdentity.getPrincipal().getName();
|
||||||
|
return resourceRepository.isOwner(resourceId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
### Bean Validation
|
||||||
|
|
||||||
|
```java
|
||||||
|
// BAD: No validation
|
||||||
|
@POST
|
||||||
|
public Response createUser(UserDto dto) {
|
||||||
|
return Response.ok(userService.create(dto)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Validated DTO
|
||||||
|
public record CreateUserDto(
|
||||||
|
@NotBlank @Size(max = 100) String name,
|
||||||
|
@NotBlank @Email String email,
|
||||||
|
@NotNull @Min(18) @Max(150) Integer age,
|
||||||
|
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/users")
|
||||||
|
public Response createUser(@Valid CreateUserDto dto) {
|
||||||
|
User user = userService.create(dto);
|
||||||
|
return Response.status(Response.Status.CREATED).entity(user).build();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Validators
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Constraint(validatedBy = UsernameValidator.class)
|
||||||
|
public @interface ValidUsername {
|
||||||
|
String message() default "Invalid username format";
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||||
|
if (value == null) return false;
|
||||||
|
return value.matches("^[a-zA-Z0-9_-]{3,20}$");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
public record CreateUserDto(
|
||||||
|
@ValidUsername String username,
|
||||||
|
@NotBlank @Email String email
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQL Injection Prevention
|
||||||
|
|
||||||
|
### Panache Active Record (Safe by Default)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// GOOD: Parameterized queries with Panache
|
||||||
|
List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
||||||
|
|
||||||
|
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||||
|
|
||||||
|
// GOOD: Named parameters
|
||||||
|
List<User> users = User.list("email = :email and age > :minAge",
|
||||||
|
Parameters.with("email", email).and("minAge", 18));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native Queries (Use Parameters)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// BAD: String concatenation
|
||||||
|
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||||
|
|
||||||
|
// GOOD: Parameterized native query
|
||||||
|
@Entity
|
||||||
|
public class User extends PanacheEntity {
|
||||||
|
public static List<User> findByEmailNative(String email) {
|
||||||
|
return getEntityManager()
|
||||||
|
.createNativeQuery("SELECT * FROM users WHERE email = :email", User.class)
|
||||||
|
.setParameter("email", email)
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Hashing
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class PasswordService {
|
||||||
|
|
||||||
|
public String hash(String plainPassword) {
|
||||||
|
return BcryptUtil.bcryptHash(plainPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(String plainPassword, String hashedPassword) {
|
||||||
|
return BcryptUtil.matches(plainPassword, hashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In service
|
||||||
|
@ApplicationScoped
|
||||||
|
public class UserService {
|
||||||
|
@Inject
|
||||||
|
PasswordService passwordService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User register(CreateUserDto dto) {
|
||||||
|
String hashedPassword = passwordService.hash(dto.password());
|
||||||
|
User user = new User();
|
||||||
|
user.email = dto.email();
|
||||||
|
user.password = hashedPassword;
|
||||||
|
user.persist();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authenticate(String email, String password) {
|
||||||
|
return User.find("email", email)
|
||||||
|
.firstResultOptional()
|
||||||
|
.map(u -> passwordService.verify(password, u.password))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS Configuration
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# application.properties
|
||||||
|
quarkus.http.cors=true
|
||||||
|
quarkus.http.cors.origins=https://app.example.com,https://admin.example.com
|
||||||
|
quarkus.http.cors.methods=GET,POST,PUT,DELETE
|
||||||
|
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
|
||||||
|
quarkus.http.cors.exposed-headers=Content-Disposition
|
||||||
|
quarkus.http.cors.access-control-max-age=24H
|
||||||
|
quarkus.http.cors.access-control-allow-credentials=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets Management
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# application.properties - NO SECRETS HERE
|
||||||
|
|
||||||
|
# Use environment variables
|
||||||
|
quarkus.datasource.username=${DB_USER}
|
||||||
|
quarkus.datasource.password=${DB_PASSWORD}
|
||||||
|
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
||||||
|
|
||||||
|
# Or use Vault
|
||||||
|
quarkus.vault.url=https://vault.example.com
|
||||||
|
quarkus.vault.authentication.kubernetes.role=my-role
|
||||||
|
```
|
||||||
|
|
||||||
|
### HashiCorp Vault Integration
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SecretService {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "api-key")
|
||||||
|
String apiKey; // Fetched from Vault
|
||||||
|
|
||||||
|
public String getSecret(String key) {
|
||||||
|
return ConfigProvider.getConfig().getValue(key, String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RateLimitFilter implements ContainerRequestFilter {
|
||||||
|
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
|
String clientId = getClientIdentifier(requestContext);
|
||||||
|
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||||
|
k -> RateLimiter.create(100.0)); // 100 requests per second
|
||||||
|
|
||||||
|
if (!limiter.tryAcquire()) {
|
||||||
|
requestContext.abortWith(
|
||||||
|
Response.status(429)
|
||||||
|
.entity(Map.of("error", "Too many requests"))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClientIdentifier(ContainerRequestContext ctx) {
|
||||||
|
// Use IP, API key, or user ID
|
||||||
|
return ctx.getHeaderString("X-Forwarded-For");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Headers
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||||
|
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||||
|
|
||||||
|
// Prevent clickjacking
|
||||||
|
headers.putSingle("X-Frame-Options", "DENY");
|
||||||
|
|
||||||
|
// XSS protection
|
||||||
|
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||||
|
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||||
|
|
||||||
|
// HSTS
|
||||||
|
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
|
||||||
|
// CSP
|
||||||
|
headers.putSingle("Content-Security-Policy",
|
||||||
|
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Logging
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuditService {
|
||||||
|
private static final Logger LOG = Logger.getLogger(AuditService.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public void logAccess(String resource, String action) {
|
||||||
|
String user = securityIdentity.isAnonymous()
|
||||||
|
? "anonymous"
|
||||||
|
: securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
|
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||||
|
user, action, resource, Instant.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in resource
|
||||||
|
@Path("/api/sensitive")
|
||||||
|
public class SensitiveResource {
|
||||||
|
@Inject
|
||||||
|
AuditService auditService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@RolesAllowed("ADMIN")
|
||||||
|
public Response getData() {
|
||||||
|
auditService.logAccess("sensitive-data", "READ");
|
||||||
|
return Response.ok(data).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Security Scanning
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Maven
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./gradlew dependencyCheckAnalyze
|
||||||
|
|
||||||
|
# Check Quarkus extensions
|
||||||
|
quarkus extension list --installable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Always use HTTPS in production
|
||||||
|
- Enable JWT or OIDC for stateless authentication
|
||||||
|
- Use `@RolesAllowed` for declarative authorization
|
||||||
|
- Validate all input with Bean Validation
|
||||||
|
- Hash passwords with BCrypt (never plaintext)
|
||||||
|
- Store secrets in Vault or environment variables
|
||||||
|
- Use parameterized queries to prevent SQL injection
|
||||||
|
- Add security headers to all responses
|
||||||
|
- Implement rate limiting for public endpoints
|
||||||
|
- Audit sensitive operations
|
||||||
|
- Keep dependencies updated and scan for CVEs
|
||||||
|
- Use SecurityIdentity for programmatic checks
|
||||||
|
- Set appropriate CORS policies
|
||||||
|
- Test authentication and authorization paths
|
||||||
908
docs/ja-JP/skills/quarkus-tdd/SKILL.md
Normal file
908
docs/ja-JP/skills/quarkus-tdd/SKILL.md
Normal file
@ -0,0 +1,908 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-tdd
|
||||||
|
description: Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito, REST Assured, Camel testing, and JaCoCo. Use when adding features, fixing bugs, or refactoring event-driven services.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus TDD Workflow
|
||||||
|
|
||||||
|
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- New features or REST endpoints
|
||||||
|
- Bug fixes or refactors
|
||||||
|
- Adding data access logic, security rules, or reactive streams
|
||||||
|
- Testing Apache Camel routes and event handlers
|
||||||
|
- Testing event-driven services with RabbitMQ
|
||||||
|
- Testing conditional flow logic
|
||||||
|
- Validating CompletableFuture async operations
|
||||||
|
- Testing LogContext propagation
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Write tests first (they should fail)
|
||||||
|
2. Implement minimal code to pass
|
||||||
|
3. Refactor with tests green
|
||||||
|
4. Enforce coverage with JaCoCo (80%+ target)
|
||||||
|
|
||||||
|
## Unit Tests with @Nested Organization
|
||||||
|
|
||||||
|
Follow this structured approach for comprehensive, readable tests:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("As2ProcessingService Unit Tests")
|
||||||
|
class As2ProcessingServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EventService eventService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DocumentJobService documentJobService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private As2ProcessingService as2ProcessingService;
|
||||||
|
|
||||||
|
private Path testFilePath;
|
||||||
|
private LogContext testLogContext;
|
||||||
|
private InvoiceValidationResult validationResult;
|
||||||
|
private StoredDocumentInfo documentInfo;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE - Common test data
|
||||||
|
testFilePath = Path.of("/tmp/test-invoice.xml");
|
||||||
|
|
||||||
|
testLogContext = new LogContext();
|
||||||
|
testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001");
|
||||||
|
testLogContext.put(As2Constants.FILE_NAME, "invoice.xml");
|
||||||
|
testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001");
|
||||||
|
|
||||||
|
validationResult = new InvoiceValidationResult();
|
||||||
|
validationResult.setValid(true);
|
||||||
|
validationResult.setSize(1024L);
|
||||||
|
validationResult.setDocumentHash("abc123");
|
||||||
|
|
||||||
|
documentInfo = new StoredDocumentInfo();
|
||||||
|
documentInfo.setPath("s3://bucket/path/invoice.xml");
|
||||||
|
documentInfo.setSize(1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for processFile")
|
||||||
|
class ProcessFile {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully process non-CHORUS file with all validations")
|
||||||
|
void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class)))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any()))
|
||||||
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class));
|
||||||
|
|
||||||
|
verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class),
|
||||||
|
eq("PERSISTENCE_BLOB_EVENT_TYPE"));
|
||||||
|
verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class),
|
||||||
|
eq("BUSINESS_RULES_MESSAGE_SENT"));
|
||||||
|
verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should bypass schematron validation for CHORUS_FLOW")
|
||||||
|
void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class)))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(),
|
||||||
|
eq(FlowProfile.EXTENDED_CTC_FR), any()))
|
||||||
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class));
|
||||||
|
|
||||||
|
verify(documentJobService).createDocumentAndJobEntities(
|
||||||
|
any(), any(), any(),
|
||||||
|
eq(FlowProfile.EXTENDED_CTC_FR),
|
||||||
|
any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create error event when file upload fails")
|
||||||
|
void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
documentInfo.setPath(""); // Blank path triggers error
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
As2ServerProcessingException exception = assertThrows(
|
||||||
|
As2ServerProcessingException.class,
|
||||||
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage())
|
||||||
|
.contains("File path is empty after upload");
|
||||||
|
|
||||||
|
verify(eventService).createErrorEvent(
|
||||||
|
eq(documentInfo),
|
||||||
|
eq("FILE_UPLOAD_FAILED"),
|
||||||
|
contains("File path is empty"));
|
||||||
|
|
||||||
|
verify(businessRulesPublisher, never()).publishAsync(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle CompletableFuture.join() failure")
|
||||||
|
void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
CompletableFuture<StoredDocumentInfo> failedFuture =
|
||||||
|
CompletableFuture.failedFuture(new StorageException("S3 connection failed"));
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(failedFuture);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
assertThrows(
|
||||||
|
CompletionException.class,
|
||||||
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when file path is null")
|
||||||
|
void givenNullFilePath_whenProcessFile_thenThrowsException() {
|
||||||
|
// ARRANGE
|
||||||
|
Path nullPath = null;
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
NullPointerException exception = assertThrows(
|
||||||
|
NullPointerException.class,
|
||||||
|
() -> as2ProcessingService.processFile(nullPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Testing Patterns
|
||||||
|
|
||||||
|
1. **@Nested Classes**: Group tests by method being tested
|
||||||
|
2. **@DisplayName**: Provide readable test descriptions for test reports
|
||||||
|
3. **Naming Convention**: `givenX_whenY_thenZ` for clarity
|
||||||
|
4. **AAA Pattern**: Explicit `// ARRANGE`, `// ACT`, `// ASSERT` comments
|
||||||
|
5. **@BeforeEach**: Setup common test data to reduce duplication
|
||||||
|
6. **assertDoesNotThrow**: Test success scenarios without catching exceptions
|
||||||
|
7. **assertThrows**: Test exception scenarios with message validation using AssertJ
|
||||||
|
8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions
|
||||||
|
9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly
|
||||||
|
10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios
|
||||||
|
|
||||||
|
## Testing Camel Routes
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("Business Rules Camel Route Tests")
|
||||||
|
class BusinessRulesRouteTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CamelContext camelContext;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
EventService eventService;
|
||||||
|
|
||||||
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE - Test data
|
||||||
|
testPayload = new BusinessRulesPayload();
|
||||||
|
testPayload.setDocumentId(1L);
|
||||||
|
testPayload.setFlowProfile(FlowProfile.BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for business-rules-publisher route")
|
||||||
|
class BusinessRulesPublisher {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully publish message to RabbitMQ")
|
||||||
|
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||||
|
mockRabbitMQ.expectedMessageCount(1);
|
||||||
|
mockRabbitMQ.expectedBodiesReceived(testPayload);
|
||||||
|
|
||||||
|
// Replace real endpoint with mock for testing
|
||||||
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
|
advice.replaceFromWith("direct:business-rules-publisher");
|
||||||
|
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockRabbitMQ.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
||||||
|
assertThat(mockRabbitMQ.getExchanges().get(0).getIn().getBody(BusinessRulesPayload.class))
|
||||||
|
.isEqualTo(testPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle marshalling to JSON")
|
||||||
|
void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
||||||
|
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
||||||
|
mockMarshal.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
|
advice.weaveAddLast().to("mock:marshal");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockMarshal.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
|
assertThat(body).contains("\"documentId\":1");
|
||||||
|
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for document-processing route")
|
||||||
|
class DocumentProcessing {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should route invoice to correct processor")
|
||||||
|
void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
||||||
|
mockInvoice.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
|
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBodyAndHeader("direct:process-document",
|
||||||
|
testPayload, "documentType", "INVOICE");
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockInvoice.assertIsSatisfied(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle validation errors gracefully")
|
||||||
|
void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
||||||
|
mockError.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
|
advice.weaveByToString(".*direct:validation-error-handler.*")
|
||||||
|
.replace().to("mock:error");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
|
// Mock validator to throw exception
|
||||||
|
when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:process-document", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockError.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
Exception exception = mockError.getExchanges().get(0).getException();
|
||||||
|
assertThat(exception).isInstanceOf(ValidationException.class);
|
||||||
|
assertThat(exception.getMessage()).contains("Invalid document");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Event Services
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("EventService Unit Tests")
|
||||||
|
class EventServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private EventService eventService;
|
||||||
|
|
||||||
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE
|
||||||
|
testPayload = new BusinessRulesPayload();
|
||||||
|
testPayload.setDocumentId(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for createSuccessEvent")
|
||||||
|
class CreateSuccessEvent {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create success event with correct attributes")
|
||||||
|
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() ->
|
||||||
|
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(eventRepository).persist(argThat(event ->
|
||||||
|
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||||
|
event.getStatus() == EventStatus.SUCCESS &&
|
||||||
|
event.getPayload().equals("{\"documentId\":1}") &&
|
||||||
|
event.getTimestamp() != null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when payload is null")
|
||||||
|
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||||
|
// ARRANGE
|
||||||
|
Object nullPayload = null;
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
||||||
|
verify(eventRepository, never()).persist(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for createErrorEvent")
|
||||||
|
class CreateErrorEvent {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create error event with error message")
|
||||||
|
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
String errorMessage = "Processing failed";
|
||||||
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() ->
|
||||||
|
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(eventRepository).persist(argThat(event ->
|
||||||
|
event.getType().equals("PROCESSING_ERROR") &&
|
||||||
|
event.getStatus() == EventStatus.ERROR &&
|
||||||
|
event.getErrorMessage().equals(errorMessage) &&
|
||||||
|
event.getPayload().equals("{\"documentId\":1}")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@DisplayName("Should reject invalid error messages")
|
||||||
|
@ValueSource(strings = {"", " "})
|
||||||
|
void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
|
||||||
|
// ACT & ASSERT
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing CompletableFuture
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("FileStorageService Unit Tests")
|
||||||
|
class FileStorageServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private S3Client s3Client;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExecutorService executorService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
private InputStream testInputStream;
|
||||||
|
private LogContext testLogContext;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE
|
||||||
|
testInputStream = new ByteArrayInputStream("test content".getBytes());
|
||||||
|
testLogContext = new LogContext();
|
||||||
|
testLogContext.put("traceId", "trace-123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for uploadOriginalFile")
|
||||||
|
class UploadOriginalFile {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully upload file and return document info")
|
||||||
|
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
Callable<?> callable = invocation.getArgument(0);
|
||||||
|
return CompletableFuture.completedFuture(callable.call());
|
||||||
|
});
|
||||||
|
|
||||||
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
|
.thenReturn(PutObjectResponse.builder().build());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
|
StoredDocumentInfo result = future.join();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getPath()).isNotBlank();
|
||||||
|
assertThat(result.getSize()).isEqualTo(1024L);
|
||||||
|
assertThat(result.getUploadedAt()).isNotNull();
|
||||||
|
|
||||||
|
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle S3 upload failure")
|
||||||
|
void givenS3Failure_whenUpload_thenCompletableFutureFails() {
|
||||||
|
// ARRANGE
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
return CompletableFuture.failedFuture(new StorageException("S3 unavailable"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThatThrownBy(() -> future.join())
|
||||||
|
.isInstanceOf(CompletionException.class)
|
||||||
|
.hasCauseInstanceOf(StorageException.class)
|
||||||
|
.hasMessageContaining("S3 unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should propagate LogContext to async operation")
|
||||||
|
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
||||||
|
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
Callable<?> callable = invocation.getArgument(0);
|
||||||
|
capturedContext.set(CustomLog.getCurrentContext());
|
||||||
|
return CompletableFuture.completedFuture(callable.call());
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL).join();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThat(capturedContext.get()).isNotNull();
|
||||||
|
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource Layer Tests (REST Assured)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("DocumentResource API Tests")
|
||||||
|
class DocumentResourceTest {
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
DocumentService documentService;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for GET /api/documents")
|
||||||
|
class ListDocuments {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return list of documents")
|
||||||
|
void givenDocumentsExist_whenList_thenReturnsOk() {
|
||||||
|
// ARRANGE
|
||||||
|
List<Document> documents = List.of(createDocument(1L, "DOC-001"));
|
||||||
|
when(documentService.list(0, 20)).thenReturn(documents);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.when().get("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$.size()", is(1))
|
||||||
|
.body("[0].referenceNumber", equalTo("DOC-001"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for POST /api/documents")
|
||||||
|
class CreateDocument {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create document and return 201")
|
||||||
|
void givenValidRequest_whenCreate_thenReturns201() {
|
||||||
|
// ARRANGE
|
||||||
|
Document document = createDocument(1L, "DOC-001");
|
||||||
|
when(documentService.create(any())).thenReturn(document);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "DOC-001",
|
||||||
|
"description": "Test document",
|
||||||
|
"validUntil": "2030-01-01T00:00:00Z",
|
||||||
|
"categories": ["test"]
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.header("Location", containsString("/api/documents/1"))
|
||||||
|
.body("referenceNumber", equalTo("DOC-001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return 400 for invalid input")
|
||||||
|
void givenInvalidRequest_whenCreate_thenReturns400() {
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "",
|
||||||
|
"description": "Test"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document createDocument(Long id, String referenceNumber) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setId(id);
|
||||||
|
document.setReferenceNumber(referenceNumber);
|
||||||
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Tests with Real Database
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@TestProfile(IntegrationTestProfile.class)
|
||||||
|
@DisplayName("Document Integration Tests")
|
||||||
|
class DocumentIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
@DisplayName("Should create and retrieve document via API")
|
||||||
|
void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
|
||||||
|
// ACT - Create via API
|
||||||
|
Long id = given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "INT-001",
|
||||||
|
"description": "Integration test",
|
||||||
|
"validUntil": "2030-01-01T00:00:00Z",
|
||||||
|
"categories": ["test"]
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.extract().path("id");
|
||||||
|
|
||||||
|
// ASSERT - Retrieve via API
|
||||||
|
given()
|
||||||
|
.when().get("/api/documents/" + id)
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("referenceNumber", equalTo("INT-001"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage with JaCoCo
|
||||||
|
|
||||||
|
### Maven Configuration (Complete)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.13</version>
|
||||||
|
<executions>
|
||||||
|
<!-- Prepare agent for test execution -->
|
||||||
|
<execution>
|
||||||
|
<id>prepare-agent</id>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<!-- Generate coverage report -->
|
||||||
|
<execution>
|
||||||
|
<id>report</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<!-- Enforce coverage thresholds -->
|
||||||
|
<execution>
|
||||||
|
<id>check</id>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.80</minimum>
|
||||||
|
</limit>
|
||||||
|
<limit>
|
||||||
|
<counter>BRANCH</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.70</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests with coverage:
|
||||||
|
```bash
|
||||||
|
mvn clean test
|
||||||
|
mvn jacoco:report
|
||||||
|
mvn jacoco:check
|
||||||
|
|
||||||
|
# Report at: target/site/jacoco/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Dependencies
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependencies>
|
||||||
|
<!-- Quarkus Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Mockito -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- AssertJ (preferred over JUnit assertions) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.24.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- REST Assured -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.rest-assured</groupId>
|
||||||
|
<artifactId>rest-assured</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Camel Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
- Use `@Nested` classes to group tests by method being tested
|
||||||
|
- Use `@DisplayName` for readable test descriptions visible in reports
|
||||||
|
- Follow `givenX_whenY_thenZ` naming convention for test methods
|
||||||
|
- Use `@BeforeEach` for common test data setup to reduce duplication
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
- Follow AAA pattern with explicit comments (`// ARRANGE`, `// ACT`, `// ASSERT`)
|
||||||
|
- Use `assertDoesNotThrow` for success scenarios
|
||||||
|
- Use `assertThrows` for exception scenarios with message validation
|
||||||
|
- Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()`
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
- Test happy paths for all public methods
|
||||||
|
- Test null input handling
|
||||||
|
- Test edge cases (empty collections, boundary values, negative IDs, blank strings)
|
||||||
|
- Test exception scenarios comprehensively
|
||||||
|
- Mock all external dependencies (repositories, services, Camel endpoints)
|
||||||
|
- Aim for 80%+ line coverage, 70%+ branch coverage
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
- **Always use AssertJ** (`assertThat`) instead of JUnit assertions
|
||||||
|
- Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)`
|
||||||
|
- For exceptions: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
|
||||||
|
- For collections: `extracting()`, `filteredOn()`, `containsExactly()`
|
||||||
|
|
||||||
|
### Testing Integration
|
||||||
|
- Use `@QuarkusTest` for integration tests
|
||||||
|
- Use `@InjectMock` to mock dependencies in Quarkus tests
|
||||||
|
- Prefer REST Assured for API testing
|
||||||
|
- Use `@TestProfile` for test-specific configuration
|
||||||
|
|
||||||
|
### Event-Driven Testing
|
||||||
|
- Test Camel routes with `AdviceWith` and `MockEndpoint`
|
||||||
|
- Use `@CamelQuarkusTest` annotation (if using standalone Camel tests)
|
||||||
|
- Verify message content, headers, and routing logic
|
||||||
|
- Test error handling routes separately
|
||||||
|
- Mock external systems (RabbitMQ, S3, databases) in unit tests
|
||||||
|
|
||||||
|
### Camel Route Testing
|
||||||
|
- Use `MockEndpoint` for asserting message flow
|
||||||
|
- Use `AdviceWith` to modify routes for testing (replace endpoints with mocks)
|
||||||
|
- Test message transformation and marshalling
|
||||||
|
- Test exception handling and dead letter queues
|
||||||
|
|
||||||
|
### Testing Async Operations
|
||||||
|
- Test CompletableFuture success and failure scenarios
|
||||||
|
- Use `.join()` in tests to wait for async completion
|
||||||
|
- Test exception propagation from CompletableFuture
|
||||||
|
- Verify LogContext propagation to async operations
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Keep tests fast and isolated
|
||||||
|
- Run tests in continuous mode: `mvn quarkus:test`
|
||||||
|
- Use parameterized tests (`@ParameterizedTest`) for input variations
|
||||||
|
- Build reusable test data builders or factory methods
|
||||||
|
|
||||||
|
### Quarkus-Specific
|
||||||
|
- Stay on latest LTS version (Quarkus 3.x)
|
||||||
|
- Test native compilation compatibility periodically
|
||||||
|
- Use Quarkus test profiles for different scenarios
|
||||||
|
- Leverage Quarkus dev services for local testing
|
||||||
|
- Use `@InjectMock` instead of `@MockBean` (Quarkus-specific)
|
||||||
|
|
||||||
|
### Verification Best Practices
|
||||||
|
- Always verify interactions on mocked dependencies
|
||||||
|
- Use `verify(mock, never())` to ensure methods are NOT called in error scenarios
|
||||||
|
- Use `argThat()` for complex argument matching
|
||||||
|
- Verify the order of calls when it matters: `InOrder` from Mockito
|
||||||
481
docs/ja-JP/skills/quarkus-verification/SKILL.md
Normal file
481
docs/ja-JP/skills/quarkus-verification/SKILL.md
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-verification
|
||||||
|
description: "Verification loop for Quarkus projects: build, static analysis, tests with coverage, security scans, native compilation, and diff review before release or PR."
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Verification Loop
|
||||||
|
|
||||||
|
Run before PRs, after major changes, and pre-deploy.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Before opening a pull request for a Quarkus service
|
||||||
|
- After major refactoring or dependency upgrades
|
||||||
|
- Pre-deployment verification for staging or production
|
||||||
|
- Running full build → lint → test → security scan → native compilation pipeline
|
||||||
|
- Validating test coverage meets thresholds (80%+)
|
||||||
|
- Testing native image compatibility
|
||||||
|
|
||||||
|
## Phase 1: Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Maven
|
||||||
|
mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./gradlew clean assemble -x test
|
||||||
|
```
|
||||||
|
|
||||||
|
If build fails, stop and fix compilation errors.
|
||||||
|
|
||||||
|
## Phase 2: Static Analysis
|
||||||
|
|
||||||
|
### Checkstyle, PMD, SpotBugs (Maven)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn checkstyle:check pmd:check spotbugs:check
|
||||||
|
```
|
||||||
|
|
||||||
|
### SonarQube (if configured)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn sonar:sonar \
|
||||||
|
-Dsonar.projectKey=my-quarkus-project \
|
||||||
|
-Dsonar.host.url=http://localhost:9000 \
|
||||||
|
-Dsonar.login=${SONAR_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues to Address
|
||||||
|
|
||||||
|
- Unused imports or variables
|
||||||
|
- Complex methods (high cyclomatic complexity)
|
||||||
|
- Potential null pointer dereferences
|
||||||
|
- Security issues flagged by SpotBugs
|
||||||
|
|
||||||
|
## Phase 3: Tests + Coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
mvn clean test
|
||||||
|
|
||||||
|
# Generate coverage report
|
||||||
|
mvn jacoco:report
|
||||||
|
|
||||||
|
# Enforce coverage threshold (80%)
|
||||||
|
mvn jacoco:check
|
||||||
|
|
||||||
|
# Or with Gradle
|
||||||
|
./gradlew test jacocoTestReport jacocoTestCoverageVerification
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Categories
|
||||||
|
|
||||||
|
#### Unit Tests
|
||||||
|
Test service logic with mocked dependencies:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserServiceTest {
|
||||||
|
@Mock UserRepository userRepository;
|
||||||
|
@InjectMocks UserService userService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_validInput_returnsUser() {
|
||||||
|
var dto = new CreateUserDto("Alice", "alice@example.com");
|
||||||
|
var expected = new User();
|
||||||
|
expected.id = 1L;
|
||||||
|
expected.name = dto.name();
|
||||||
|
|
||||||
|
when(userRepository.persist(any(User.class))).thenReturn(expected);
|
||||||
|
|
||||||
|
User result = userService.create(dto);
|
||||||
|
|
||||||
|
assertThat(result.name).isEqualTo("Alice");
|
||||||
|
verify(userRepository).persist(any(User.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Integration Tests
|
||||||
|
Test with real database (Testcontainers):
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@QuarkusTestResource(PostgresTestResource.class)
|
||||||
|
class UserRepositoryIntegrationTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UserRepository userRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void findByEmail_existingUser_returnsUser() {
|
||||||
|
User user = new User();
|
||||||
|
user.name = "Alice";
|
||||||
|
user.email = "alice@example.com";
|
||||||
|
userRepository.persist(user);
|
||||||
|
|
||||||
|
Optional<User> found = userRepository.findByEmail("alice@example.com");
|
||||||
|
|
||||||
|
assertThat(found).isPresent();
|
||||||
|
assertThat(found.get().name).isEqualTo("Alice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Tests
|
||||||
|
Test REST endpoints with REST Assured:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
class UserResourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_validInput_returns201() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{"name": "Alice", "email": "alice@example.com"}
|
||||||
|
""")
|
||||||
|
.when().post("/api/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.body("name", equalTo("Alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_invalidEmail_returns400() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{"name": "Alice", "email": "invalid"}
|
||||||
|
""")
|
||||||
|
.when().post("/api/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage Report
|
||||||
|
|
||||||
|
Check `target/site/jacoco/index.html` for detailed coverage:
|
||||||
|
- Overall line coverage (target: 80%+)
|
||||||
|
- Branch coverage (target: 70%+)
|
||||||
|
- Identify uncovered critical paths
|
||||||
|
|
||||||
|
## Phase 4: Security Scanning
|
||||||
|
|
||||||
|
### Dependency Vulnerabilities (Maven)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
```
|
||||||
|
|
||||||
|
Review `target/dependency-check-report.html` for CVEs.
|
||||||
|
|
||||||
|
### Quarkus Security Audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check vulnerable extensions
|
||||||
|
mvn quarkus:audit
|
||||||
|
|
||||||
|
# List all extensions
|
||||||
|
mvn quarkus:list-extensions
|
||||||
|
```
|
||||||
|
|
||||||
|
### OWASP ZAP (API Security Testing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -t owasp/zap2docker-stable zap-api-scan.py \
|
||||||
|
-t http://localhost:8080/q/openapi \
|
||||||
|
-f openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Security Checks
|
||||||
|
|
||||||
|
- [ ] All secrets in environment variables (not in code)
|
||||||
|
- [ ] Input validation on all endpoints
|
||||||
|
- [ ] Authentication/authorization configured
|
||||||
|
- [ ] CORS properly configured
|
||||||
|
- [ ] Security headers set
|
||||||
|
- [ ] Passwords hashed with BCrypt
|
||||||
|
- [ ] SQL injection protection (parameterized queries)
|
||||||
|
- [ ] Rate limiting on public endpoints
|
||||||
|
|
||||||
|
## Phase 5: Native Compilation
|
||||||
|
|
||||||
|
Test GraalVM native image compatibility:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build native executable
|
||||||
|
mvn package -Dnative
|
||||||
|
|
||||||
|
# Or with container
|
||||||
|
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||||
|
|
||||||
|
# Test native executable
|
||||||
|
./target/*-runner
|
||||||
|
|
||||||
|
# Run basic smoke tests
|
||||||
|
curl http://localhost:8080/q/health/live
|
||||||
|
curl http://localhost:8080/q/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native Image Troubleshooting
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
- **Reflection**: Add reflection config for dynamic classes
|
||||||
|
- **Resources**: Include resources with `quarkus.native.resources.includes`
|
||||||
|
- **JNI**: Register JNI classes if using native libraries
|
||||||
|
|
||||||
|
Example reflection config:
|
||||||
|
```java
|
||||||
|
@RegisterForReflection(targets = {MyDynamicClass.class})
|
||||||
|
public class ReflectionConfiguration {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 6: Performance Testing
|
||||||
|
|
||||||
|
### Load Testing with K6
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// load-test.js
|
||||||
|
import http from 'k6/http';
|
||||||
|
import { check } from 'k6';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: '30s', target: 50 },
|
||||||
|
{ duration: '1m', target: 100 },
|
||||||
|
{ duration: '30s', target: 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const res = http.get('http://localhost:8080/api/markets');
|
||||||
|
check(res, {
|
||||||
|
'status is 200': (r) => r.status === 200,
|
||||||
|
'response time < 200ms': (r) => r.timings.duration < 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
k6 run load-test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics to Monitor
|
||||||
|
|
||||||
|
- Response time (p50, p95, p99)
|
||||||
|
- Throughput (requests/sec)
|
||||||
|
- Error rate
|
||||||
|
- Memory usage
|
||||||
|
- CPU usage
|
||||||
|
|
||||||
|
## Phase 7: Health Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Liveness
|
||||||
|
curl http://localhost:8080/q/health/live
|
||||||
|
|
||||||
|
# Readiness
|
||||||
|
curl http://localhost:8080/q/health/ready
|
||||||
|
|
||||||
|
# All health checks
|
||||||
|
curl http://localhost:8080/q/health
|
||||||
|
|
||||||
|
# Metrics (if enabled)
|
||||||
|
curl http://localhost:8080/q/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected responses:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "UP",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"name": "Database connection",
|
||||||
|
"status": "UP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 8: Container Image Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build container image
|
||||||
|
mvn package -Dquarkus.container-image.build=true
|
||||||
|
|
||||||
|
# Or with specific registry
|
||||||
|
mvn package \
|
||||||
|
-Dquarkus.container-image.build=true \
|
||||||
|
-Dquarkus.container-image.registry=docker.io \
|
||||||
|
-Dquarkus.container-image.group=myorg \
|
||||||
|
-Dquarkus.container-image.tag=1.0.0
|
||||||
|
|
||||||
|
# Test container
|
||||||
|
docker run -p 8080:8080 myorg/my-quarkus-app:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Security Scan
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trivy
|
||||||
|
trivy image myorg/my-quarkus-app:1.0.0
|
||||||
|
|
||||||
|
# Grype
|
||||||
|
grype myorg/my-quarkus-app:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 9: Configuration Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all configuration properties
|
||||||
|
mvn quarkus:info
|
||||||
|
|
||||||
|
# List all config sources
|
||||||
|
curl http://localhost:8080/q/dev/io.quarkus.quarkus-vertx-http/config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment-Specific Checks
|
||||||
|
|
||||||
|
- [ ] Database URLs configured per environment
|
||||||
|
- [ ] Secrets externalized (Vault, env vars)
|
||||||
|
- [ ] Logging levels appropriate
|
||||||
|
- [ ] CORS origins set correctly
|
||||||
|
- [ ] Rate limiting configured
|
||||||
|
- [ ] Monitoring/tracing enabled
|
||||||
|
|
||||||
|
## Phase 10: Documentation Review
|
||||||
|
|
||||||
|
- [ ] OpenAPI/Swagger docs up to date (`/q/swagger-ui`)
|
||||||
|
- [ ] README has setup instructions
|
||||||
|
- [ ] API changes documented
|
||||||
|
- [ ] Migration guide for breaking changes
|
||||||
|
- [ ] Configuration properties documented
|
||||||
|
|
||||||
|
Generate OpenAPI spec:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/q/openapi -o openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- [ ] Build passes without warnings
|
||||||
|
- [ ] Static analysis clean (no high/medium issues)
|
||||||
|
- [ ] Code follows team conventions
|
||||||
|
- [ ] No commented-out code or TODOs in PR
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Code coverage ≥ 80%
|
||||||
|
- [ ] Integration tests with real database
|
||||||
|
- [ ] Security tests pass
|
||||||
|
- [ ] Performance within acceptable limits
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- [ ] No dependency vulnerabilities
|
||||||
|
- [ ] Authentication/authorization tested
|
||||||
|
- [ ] Input validation complete
|
||||||
|
- [ ] Secrets not in source code
|
||||||
|
- [ ] Security headers configured
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
- [ ] Native compilation successful
|
||||||
|
- [ ] Container image builds
|
||||||
|
- [ ] Health checks respond correctly
|
||||||
|
- [ ] Configuration valid for target environment
|
||||||
|
|
||||||
|
### Native Image
|
||||||
|
- [ ] Native executable builds
|
||||||
|
- [ ] Native tests pass
|
||||||
|
- [ ] Startup time < 100ms
|
||||||
|
- [ ] Memory footprint acceptable
|
||||||
|
|
||||||
|
## Automated Verification Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Phase 1: Build ==="
|
||||||
|
mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
echo "=== Phase 2: Static Analysis ==="
|
||||||
|
mvn checkstyle:check pmd:check spotbugs:check
|
||||||
|
|
||||||
|
echo "=== Phase 3: Tests + Coverage ==="
|
||||||
|
mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
|
echo "=== Phase 4: Security Scan ==="
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
echo "=== Phase 5: Native Compilation ==="
|
||||||
|
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||||
|
|
||||||
|
echo "=== All Phases Complete ==="
|
||||||
|
echo "Review reports:"
|
||||||
|
echo " - Coverage: target/site/jacoco/index.html"
|
||||||
|
echo " - Security: target/dependency-check-report.html"
|
||||||
|
echo " - Native: target/*-runner"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Verification
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '21'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Cache Maven packages
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.m2
|
||||||
|
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
- name: Test with Coverage
|
||||||
|
run: mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
|
- name: Security Scan
|
||||||
|
run: mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
- name: Upload Coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: target/site/jacoco/jacoco.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Run verification loop before every PR
|
||||||
|
- Automate in CI/CD pipeline
|
||||||
|
- Fix issues immediately; don't accumulate debt
|
||||||
|
- Keep coverage above 80%
|
||||||
|
- Update dependencies regularly
|
||||||
|
- Test native compilation periodically
|
||||||
|
- Monitor performance trends
|
||||||
|
- Document breaking changes
|
||||||
|
- Review security scan results
|
||||||
|
- Validate configuration for each environment
|
||||||
@ -150,4 +150,6 @@ Remaining errors: 1
|
|||||||
|
|
||||||
Son: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
Son: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||||
|
|
||||||
Detaylı Java ve Spring Boot kalıpları için, `skill: springboot-patterns`'a bakın.
|
Detaylı Java kalıpları ve örnekler için:
|
||||||
|
- **[SPRING]**: `skill: springboot-patterns`'a bakın
|
||||||
|
- **[QUARKUS]**: `skill: quarkus-patterns`'a bakın
|
||||||
|
|||||||
@ -89,4 +89,6 @@ grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
|||||||
- **Uyarı**: Sadece MEDIUM sorunlar
|
- **Uyarı**: Sadece MEDIUM sorunlar
|
||||||
- **Bloke Et**: CRITICAL veya HIGH sorunlar bulundu
|
- **Bloke Et**: CRITICAL veya HIGH sorunlar bulundu
|
||||||
|
|
||||||
Detaylı Spring Boot kalıpları ve örnekleri için, `skill: springboot-patterns`'a bakın.
|
Detaylı kalıplar ve örnekler için:
|
||||||
|
- **[SPRING]**: `skill: springboot-patterns`'a bakın
|
||||||
|
- **[QUARKUS]**: `skill: quarkus-patterns`'a bakın
|
||||||
|
|||||||
754
docs/tr/skills/quarkus-patterns/SKILL.md
Normal file
754
docs/tr/skills/quarkus-patterns/SKILL.md
Normal file
@ -0,0 +1,754 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-patterns
|
||||||
|
description: Quarkus 3.x LTS architecture patterns with Camel for messaging, RESTful API design, CDI services, data access with Panache, and async processing. Use for Java Quarkus backend work with event-driven architectures.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Development Patterns
|
||||||
|
|
||||||
|
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Building REST APIs with JAX-RS or RESTEasy Reactive
|
||||||
|
- Structuring resource → service → repository layers
|
||||||
|
- Implementing event-driven patterns with Apache Camel and RabbitMQ
|
||||||
|
- Configuring Hibernate Panache, caching, or reactive streams
|
||||||
|
- Adding validation, exception mapping, or pagination
|
||||||
|
- Setting up profiles for dev/staging/production environments (YAML config)
|
||||||
|
- Custom logging with LogContext and Logback/Logstash encoder
|
||||||
|
- Working with CompletableFuture for async operations
|
||||||
|
- Implementing conditional flow processing
|
||||||
|
- Working with GraalVM native compilation
|
||||||
|
|
||||||
|
## Service Layer with Multiple Dependencies (Lombok)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class As2ProcessingService {
|
||||||
|
|
||||||
|
private final InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
private final EventService eventService;
|
||||||
|
private final DocumentJobService documentJobService;
|
||||||
|
private final BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
private final FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
public void processFile(Path filePath) throws Exception {
|
||||||
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
|
||||||
|
String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID);
|
||||||
|
|
||||||
|
// Conditional flow logic
|
||||||
|
boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW));
|
||||||
|
log.info("Is CHORUS_FLOW message: {}", isChorusFlow);
|
||||||
|
|
||||||
|
ValidationFlowConfig validationFlowConfig = isChorusFlow
|
||||||
|
? ValidationFlowConfig.xsdOnly()
|
||||||
|
: ValidationFlowConfig.allValidations();
|
||||||
|
|
||||||
|
InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator
|
||||||
|
.validateFlowWithConfig(filePath, validationFlowConfig,
|
||||||
|
EInvoiceSyntaxFormat.UBL, logContext);
|
||||||
|
|
||||||
|
FlowProfile flowProfile = isChorusFlow ?
|
||||||
|
FlowProfile.EXTENDED_CTC_FR :
|
||||||
|
this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult,
|
||||||
|
invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile());
|
||||||
|
|
||||||
|
log.info("Invoice validation completed. Message is valid");
|
||||||
|
|
||||||
|
// CompletableFuture async operation
|
||||||
|
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
||||||
|
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
||||||
|
fileStorageService.uploadOriginalFile(inputStream,
|
||||||
|
invoiceValidationResult.getSize(), logContext,
|
||||||
|
invoiceValidationResult.getInvoiceFormat());
|
||||||
|
|
||||||
|
StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join();
|
||||||
|
log.info("File uploaded successfully: {}", documentInfo.getPath());
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(documentInfo.getPath())) {
|
||||||
|
String errorMsg = "File path is empty after upload";
|
||||||
|
log.error(errorMsg);
|
||||||
|
this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg);
|
||||||
|
throw new As2ServerProcessingException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE");
|
||||||
|
|
||||||
|
BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities(
|
||||||
|
documentInfo, originalFileName, structureIdPartner,
|
||||||
|
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||||
|
|
||||||
|
// Async Camel publishing
|
||||||
|
businessRulesPublisher.publishAsync(payload);
|
||||||
|
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Patterns:**
|
||||||
|
- `@RequiredArgsConstructor` for constructor injection via Lombok
|
||||||
|
- `@Slf4j` for Logback logging
|
||||||
|
- Scoped LogContext with try-with-resources
|
||||||
|
- Conditional flow logic based on runtime parameters
|
||||||
|
- CompletableFuture with `.join()` for async operations
|
||||||
|
- Event tracking for success/error scenarios
|
||||||
|
- Async Camel message publishing
|
||||||
|
|
||||||
|
## Custom Logging Context Pattern (Logback)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ProcessingService {
|
||||||
|
|
||||||
|
public void processDocument(Document doc) {
|
||||||
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
// Add context to all log statements
|
||||||
|
logContext.put("documentId", doc.getId().toString());
|
||||||
|
logContext.put("documentType", doc.getType());
|
||||||
|
logContext.put("userId", SecurityContext.getUserId());
|
||||||
|
|
||||||
|
log.info("Starting document processing");
|
||||||
|
|
||||||
|
// All logs within this scope inherit the context
|
||||||
|
processInternal(doc);
|
||||||
|
|
||||||
|
log.info("Document processing completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Document processing failed", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logback Configuration (logback.xml):**
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<configuration>
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||||
|
<includeContext>true</includeContext>
|
||||||
|
<includeMdc>true</includeMdc>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="com.example" level="INFO"/>
|
||||||
|
<root level="WARN">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Service Pattern
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EventService {
|
||||||
|
private final EventRepository eventRepository;
|
||||||
|
|
||||||
|
public void createSuccessEvent(Object payload, String eventType) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setType(eventType);
|
||||||
|
event.setStatus(EventStatus.SUCCESS);
|
||||||
|
event.setPayload(serializePayload(payload));
|
||||||
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
|
eventRepository.persist(event);
|
||||||
|
log.info("Success event created: {}", eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setType(eventType);
|
||||||
|
event.setStatus(EventStatus.ERROR);
|
||||||
|
event.setErrorMessage(errorMessage);
|
||||||
|
event.setPayload(serializePayload(payload));
|
||||||
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
|
eventRepository.persist(event);
|
||||||
|
log.error("Error event created: {} - {}", eventType, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializePayload(Object payload) {
|
||||||
|
// JSON serialization
|
||||||
|
return objectMapper.writeValueAsString(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Message Publishing (RabbitMQ)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BusinessRulesPublisher {
|
||||||
|
private final ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
|
String businessRulesQueue;
|
||||||
|
|
||||||
|
public void publishAsync(BusinessRulesPayload payload) {
|
||||||
|
producerTemplate.asyncSendBody(
|
||||||
|
"direct:business-rules-publisher",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
log.info("Message published to business rules queue: {}", payload.getDocumentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishSync(BusinessRulesPayload payload) {
|
||||||
|
producerTemplate.sendBody(
|
||||||
|
"direct:business-rules-publisher",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Camel Route Configuration:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class BusinessRulesRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
|
String businessRulesQueue;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "rabbitmq.host")
|
||||||
|
String rabbitHost;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "rabbitmq.port")
|
||||||
|
Integer rabbitPort;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("direct:business-rules-publisher")
|
||||||
|
.routeId("business-rules-publisher")
|
||||||
|
.log("Publishing message to RabbitMQ: ${body}")
|
||||||
|
.marshal().json(JsonLibrary.Jackson)
|
||||||
|
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
||||||
|
businessRulesQueue, rabbitHost, rabbitPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Direct Routes (In-Memory)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class DocumentProcessingRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
// Error handling
|
||||||
|
onException(ValidationException.class)
|
||||||
|
.handled(true)
|
||||||
|
.to("direct:validation-error-handler")
|
||||||
|
.log("Validation error: ${exception.message}");
|
||||||
|
|
||||||
|
// Main processing route
|
||||||
|
from("direct:process-document")
|
||||||
|
.routeId("document-processing")
|
||||||
|
.log("Processing document: ${header.documentId}")
|
||||||
|
.bean(DocumentValidator.class, "validate")
|
||||||
|
.bean(DocumentTransformer.class, "transform")
|
||||||
|
.choice()
|
||||||
|
.when(header("documentType").isEqualTo("INVOICE"))
|
||||||
|
.to("direct:process-invoice")
|
||||||
|
.when(header("documentType").isEqualTo("CREDIT_NOTE"))
|
||||||
|
.to("direct:process-credit-note")
|
||||||
|
.otherwise()
|
||||||
|
.to("direct:process-generic")
|
||||||
|
.end();
|
||||||
|
|
||||||
|
from("direct:validation-error-handler")
|
||||||
|
.bean(EventService.class, "createErrorEvent")
|
||||||
|
.log("Validation error handled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel File Processing
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class FileMonitoringRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.input.directory")
|
||||||
|
String inputDirectory;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.processed.directory")
|
||||||
|
String processedDirectory;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.error.directory")
|
||||||
|
String errorDirectory;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
||||||
|
"&moveFailed=" + errorDirectory + "&delay=5000")
|
||||||
|
.routeId("file-monitor")
|
||||||
|
.log("Processing file: ${header.CamelFileName}")
|
||||||
|
.to("direct:process-file");
|
||||||
|
|
||||||
|
from("direct:process-file")
|
||||||
|
.bean(As2ProcessingService.class, "processFile")
|
||||||
|
.log("File processing completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Bean Invocation
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class InvoiceRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("direct:invoice-validation")
|
||||||
|
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
||||||
|
.log("Validation result: ${body}");
|
||||||
|
|
||||||
|
from("direct:persist-and-publish")
|
||||||
|
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
||||||
|
.bean(BusinessRulesPublisher.class, "publishAsync")
|
||||||
|
.bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST API Structure
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Path("/api/documents")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentResource {
|
||||||
|
private final DocumentService documentService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response list(
|
||||||
|
@QueryParam("page") @DefaultValue("0") int page,
|
||||||
|
@QueryParam("size") @DefaultValue("20") int size) {
|
||||||
|
PaginatedList<Document> documents = documentService.list(page, size);
|
||||||
|
return Response.ok(documents).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public Response create(@Valid CreateDocumentRequest request, @Context UriInfo uriInfo) {
|
||||||
|
Document document = documentService.create(request);
|
||||||
|
URI location = uriInfo.getAbsolutePathBuilder()
|
||||||
|
.path(String.valueOf(document.id))
|
||||||
|
.build();
|
||||||
|
return Response.created(location).entity(DocumentResponse.from(document)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
public Response getById(@PathParam("id") Long id) {
|
||||||
|
return documentService.findById(id)
|
||||||
|
.map(DocumentResponse::from)
|
||||||
|
.map(Response::ok)
|
||||||
|
.orElse(Response.status(Response.Status.NOT_FOUND))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Pattern (Panache Repository)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class DocumentRepository implements PanacheRepository<Document> {
|
||||||
|
|
||||||
|
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
||||||
|
return find("status = ?1 order by createdAt desc", status)
|
||||||
|
.page(page, size)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
||||||
|
return find("referenceNumber", referenceNumber).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
||||||
|
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Layer with Transactions
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentService {
|
||||||
|
private final DocumentRepository repo;
|
||||||
|
private final EventService eventService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Document create(CreateDocumentRequest request) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setReferenceNumber(request.referenceNumber());
|
||||||
|
document.setDescription(request.description());
|
||||||
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
|
document.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
|
repo.persist(document);
|
||||||
|
|
||||||
|
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findById(Long id) {
|
||||||
|
return repo.findByIdOptional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaginatedList<Document> list(int page, int size) {
|
||||||
|
return repo.findAll()
|
||||||
|
.page(page, size)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DTOs and Validation
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record CreateDocumentRequest(
|
||||||
|
@NotBlank @Size(max = 200) String referenceNumber,
|
||||||
|
@NotBlank @Size(max = 2000) String description,
|
||||||
|
@NotNull @FutureOrPresent Instant validUntil,
|
||||||
|
@NotEmpty List<@NotBlank String> categories) {}
|
||||||
|
|
||||||
|
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
||||||
|
public static DocumentResponse from(Document document) {
|
||||||
|
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
||||||
|
document.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exception Mapping
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
|
||||||
|
@Override
|
||||||
|
public Response toResponse(ConstraintViolationException exception) {
|
||||||
|
String message = exception.getConstraintViolations().stream()
|
||||||
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "validation_error", "message", message))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
@Slf4j
|
||||||
|
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(Exception exception) {
|
||||||
|
log.error("Unhandled exception", exception);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "internal_error", "message", "An unexpected error occurred"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CompletableFuture Async Operations
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FileStorageService {
|
||||||
|
private final S3Client s3Client;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
|
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
||||||
|
InputStream inputStream,
|
||||||
|
long size,
|
||||||
|
LogContext logContext,
|
||||||
|
InvoiceFormat format) {
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
String path = generateStoragePath(format);
|
||||||
|
|
||||||
|
PutObjectRequest request = PutObjectRequest.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.key(path)
|
||||||
|
.contentLength(size)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
||||||
|
|
||||||
|
log.info("File uploaded to S3: {}", path);
|
||||||
|
|
||||||
|
return new StoredDocumentInfo(path, size, Instant.now());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to upload file to S3", e);
|
||||||
|
throw new StorageException("Upload failed", e);
|
||||||
|
}
|
||||||
|
}, executorService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentCacheService {
|
||||||
|
private final DocumentRepository repo;
|
||||||
|
|
||||||
|
@CacheResult(cacheName = "document-cache")
|
||||||
|
public Optional<Document> getById(@CacheKey Long id) {
|
||||||
|
return repo.findByIdOptional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheInvalidate(cacheName = "document-cache")
|
||||||
|
public void evict(@CacheKey Long id) {}
|
||||||
|
|
||||||
|
@CacheInvalidateAll(cacheName = "document-cache")
|
||||||
|
public void evictAll() {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration as YAML
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# application.yml
|
||||||
|
"%dev":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: jdbc:postgresql://localhost:5432/dev_db
|
||||||
|
username: dev_user
|
||||||
|
password: dev_pass
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: drop-and-create
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: localhost
|
||||||
|
port: 5672
|
||||||
|
username: guest
|
||||||
|
password: guest
|
||||||
|
|
||||||
|
"%test":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: jdbc:h2:mem:test
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: drop-and-create
|
||||||
|
|
||||||
|
"%prod":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: ${DATABASE_URL}
|
||||||
|
username: ${DB_USER}
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: validate
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: ${RABBITMQ_HOST}
|
||||||
|
port: ${RABBITMQ_PORT}
|
||||||
|
username: ${RABBITMQ_USER}
|
||||||
|
password: ${RABBITMQ_PASSWORD}
|
||||||
|
|
||||||
|
# Camel configuration
|
||||||
|
camel:
|
||||||
|
rabbitmq:
|
||||||
|
queue:
|
||||||
|
business-rules: business-rules-queue
|
||||||
|
invoice-processing: invoice-processing-queue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Readiness
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DatabaseHealthCheck implements HealthCheck {
|
||||||
|
private final AgroalDataSource dataSource;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
try (Connection conn = dataSource.getConnection()) {
|
||||||
|
boolean valid = conn.isValid(2);
|
||||||
|
return HealthCheckResponse.named("Database connection")
|
||||||
|
.status(valid)
|
||||||
|
.build();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return HealthCheckResponse.down("Database connection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Liveness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class CamelHealthCheck implements HealthCheck {
|
||||||
|
@Inject
|
||||||
|
CamelContext camelContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
boolean isStarted = camelContext.getStatus().isStarted();
|
||||||
|
return HealthCheckResponse.named("Camel Context")
|
||||||
|
.status(isStarted)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies (Maven)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<properties>
|
||||||
|
<quarkus.platform.version>3.27.0</quarkus.platform.version>
|
||||||
|
<lombok.version>1.18.42</lombok.version>
|
||||||
|
<assertj-core.version>3.24.2</assertj-core.version>
|
||||||
|
<jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus.platform</groupId>
|
||||||
|
<artifactId>quarkus-bom</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-bom</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Quarkus Core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-arc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Camel Extensions -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-direct</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-bean</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Logging -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||||
|
<artifactId>quarkus-logging-logback</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.logstash.logback</groupId>
|
||||||
|
<artifactId>logstash-logback-encoder</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- Use `@RequiredArgsConstructor` with Lombok for constructor injection
|
||||||
|
- Keep service layer thin; delegate complex logic to specialized classes
|
||||||
|
- Use Camel routes for message routing and integration patterns
|
||||||
|
- Prefer Panache Repository pattern for data access
|
||||||
|
|
||||||
|
### Event-Driven
|
||||||
|
- Always track operations with EventService (success/error events)
|
||||||
|
- Use Camel `direct:` endpoints for in-memory routing
|
||||||
|
- Use `spring-rabbitmq` component for RabbitMQ integration
|
||||||
|
- Implement async publishing with `ProducerTemplate.asyncSendBody()`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Use Logback with Logstash encoder for structured logging
|
||||||
|
- Propagate LogContext through service calls with `SafeAutoCloseable`
|
||||||
|
- Add contextual information to LogContext for request tracing
|
||||||
|
- Use `@Slf4j` instead of manual logger instantiation
|
||||||
|
|
||||||
|
### Async Operations
|
||||||
|
- Use CompletableFuture for non-blocking I/O operations
|
||||||
|
- Call `.join()` when you need to wait for completion
|
||||||
|
- Handle exceptions from CompletableFuture properly
|
||||||
|
- Pass LogContext to async operations for tracing
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Use YAML configuration (`quarkus-config-yaml`)
|
||||||
|
- Profile-aware configuration for dev/test/prod environments
|
||||||
|
- Externalize sensitive configuration to environment variables
|
||||||
|
- Use `@ConfigProperty` for type-safe config injection
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
- Validate at resource layer with `@Valid`
|
||||||
|
- Use Bean Validation annotations on DTOs
|
||||||
|
- Map exceptions to proper HTTP responses with `@Provider`
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
- Use `@Transactional` on service methods that modify data
|
||||||
|
- Keep transactions short and focused
|
||||||
|
- Avoid calling async operations within transactions
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Use `camel-quarkus-junit5` for route testing
|
||||||
|
- Use AssertJ for assertions
|
||||||
|
- Mock all external dependencies
|
||||||
|
- Test conditional flow logic thoroughly
|
||||||
|
|
||||||
|
### Quarkus-Specific
|
||||||
|
- Stay on latest LTS version (3.x)
|
||||||
|
- Use Quarkus dev mode for hot reload
|
||||||
|
- Add health checks for production readiness
|
||||||
|
- Test native compilation compatibility periodically
|
||||||
453
docs/tr/skills/quarkus-security/SKILL.md
Normal file
453
docs/tr/skills/quarkus-security/SKILL.md
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-security
|
||||||
|
description: Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Security Review
|
||||||
|
|
||||||
|
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Adding authentication (JWT, OIDC, Basic Auth)
|
||||||
|
- Implementing authorization with @RolesAllowed or SecurityIdentity
|
||||||
|
- Validating user input (Bean Validation, custom validators)
|
||||||
|
- Configuring CORS or security headers
|
||||||
|
- Managing secrets (Vault, environment variables, config sources)
|
||||||
|
- Adding rate limiting or brute-force protection
|
||||||
|
- Scanning dependencies for CVEs
|
||||||
|
- Working with MicroProfile JWT or SmallRye JWT
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### JWT Authentication
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Resource protected with JWT
|
||||||
|
@Path("/api/protected")
|
||||||
|
@Authenticated
|
||||||
|
public class ProtectedResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JsonWebToken jwt;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response getData() {
|
||||||
|
String username = jwt.getName();
|
||||||
|
Set<String> roles = jwt.getGroups();
|
||||||
|
return Response.ok(Map.of(
|
||||||
|
"username", username,
|
||||||
|
"roles", roles,
|
||||||
|
"principal", securityIdentity.getPrincipal().getName()
|
||||||
|
)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration (application.properties):
|
||||||
|
```properties
|
||||||
|
mp.jwt.verify.publickey.location=publicKey.pem
|
||||||
|
mp.jwt.verify.issuer=https://auth.example.com
|
||||||
|
|
||||||
|
# OIDC
|
||||||
|
quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm
|
||||||
|
quarkus.oidc.client-id=backend-service
|
||||||
|
quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Authentication Filter
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
|
public class CustomAuthFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity identity;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
// Validate token and set SecurityIdentity
|
||||||
|
if (!validateToken(token)) {
|
||||||
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateToken(String token) {
|
||||||
|
// Token validation logic
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
### Role-Based Access Control
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Path("/api/admin")
|
||||||
|
@RolesAllowed("ADMIN")
|
||||||
|
public class AdminResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/users")
|
||||||
|
public List<UserDto> listUsers() {
|
||||||
|
return userService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/users/{id}")
|
||||||
|
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
||||||
|
public Response deleteUser(@PathParam("id") Long id) {
|
||||||
|
userService.delete(id);
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/api/users")
|
||||||
|
public class UserResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
public Response getUser(@PathParam("id") Long id) {
|
||||||
|
// Check ownership
|
||||||
|
if (!securityIdentity.hasRole("ADMIN") &&
|
||||||
|
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
return Response.ok(userService.findById(id)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOwner(Long userId, String username) {
|
||||||
|
return userService.isOwner(userId, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Security
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SecurityService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public boolean canAccessResource(Long resourceId) {
|
||||||
|
if (securityIdentity.isAnonymous()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (securityIdentity.hasRole("ADMIN")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = securityIdentity.getPrincipal().getName();
|
||||||
|
return resourceRepository.isOwner(resourceId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
### Bean Validation
|
||||||
|
|
||||||
|
```java
|
||||||
|
// BAD: No validation
|
||||||
|
@POST
|
||||||
|
public Response createUser(UserDto dto) {
|
||||||
|
return Response.ok(userService.create(dto)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Validated DTO
|
||||||
|
public record CreateUserDto(
|
||||||
|
@NotBlank @Size(max = 100) String name,
|
||||||
|
@NotBlank @Email String email,
|
||||||
|
@NotNull @Min(18) @Max(150) Integer age,
|
||||||
|
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/users")
|
||||||
|
public Response createUser(@Valid CreateUserDto dto) {
|
||||||
|
User user = userService.create(dto);
|
||||||
|
return Response.status(Response.Status.CREATED).entity(user).build();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Validators
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Constraint(validatedBy = UsernameValidator.class)
|
||||||
|
public @interface ValidUsername {
|
||||||
|
String message() default "Invalid username format";
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||||
|
if (value == null) return false;
|
||||||
|
return value.matches("^[a-zA-Z0-9_-]{3,20}$");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
public record CreateUserDto(
|
||||||
|
@ValidUsername String username,
|
||||||
|
@NotBlank @Email String email
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQL Injection Prevention
|
||||||
|
|
||||||
|
### Panache Active Record (Safe by Default)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// GOOD: Parameterized queries with Panache
|
||||||
|
List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
||||||
|
|
||||||
|
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||||
|
|
||||||
|
// GOOD: Named parameters
|
||||||
|
List<User> users = User.list("email = :email and age > :minAge",
|
||||||
|
Parameters.with("email", email).and("minAge", 18));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native Queries (Use Parameters)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// BAD: String concatenation
|
||||||
|
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||||
|
|
||||||
|
// GOOD: Parameterized native query
|
||||||
|
@Entity
|
||||||
|
public class User extends PanacheEntity {
|
||||||
|
public static List<User> findByEmailNative(String email) {
|
||||||
|
return getEntityManager()
|
||||||
|
.createNativeQuery("SELECT * FROM users WHERE email = :email", User.class)
|
||||||
|
.setParameter("email", email)
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Hashing
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class PasswordService {
|
||||||
|
|
||||||
|
public String hash(String plainPassword) {
|
||||||
|
return BcryptUtil.bcryptHash(plainPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(String plainPassword, String hashedPassword) {
|
||||||
|
return BcryptUtil.matches(plainPassword, hashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In service
|
||||||
|
@ApplicationScoped
|
||||||
|
public class UserService {
|
||||||
|
@Inject
|
||||||
|
PasswordService passwordService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User register(CreateUserDto dto) {
|
||||||
|
String hashedPassword = passwordService.hash(dto.password());
|
||||||
|
User user = new User();
|
||||||
|
user.email = dto.email();
|
||||||
|
user.password = hashedPassword;
|
||||||
|
user.persist();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authenticate(String email, String password) {
|
||||||
|
return User.find("email", email)
|
||||||
|
.firstResultOptional()
|
||||||
|
.map(u -> passwordService.verify(password, u.password))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS Configuration
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# application.properties
|
||||||
|
quarkus.http.cors=true
|
||||||
|
quarkus.http.cors.origins=https://app.example.com,https://admin.example.com
|
||||||
|
quarkus.http.cors.methods=GET,POST,PUT,DELETE
|
||||||
|
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
|
||||||
|
quarkus.http.cors.exposed-headers=Content-Disposition
|
||||||
|
quarkus.http.cors.access-control-max-age=24H
|
||||||
|
quarkus.http.cors.access-control-allow-credentials=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets Management
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# application.properties - NO SECRETS HERE
|
||||||
|
|
||||||
|
# Use environment variables
|
||||||
|
quarkus.datasource.username=${DB_USER}
|
||||||
|
quarkus.datasource.password=${DB_PASSWORD}
|
||||||
|
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
||||||
|
|
||||||
|
# Or use Vault
|
||||||
|
quarkus.vault.url=https://vault.example.com
|
||||||
|
quarkus.vault.authentication.kubernetes.role=my-role
|
||||||
|
```
|
||||||
|
|
||||||
|
### HashiCorp Vault Integration
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SecretService {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "api-key")
|
||||||
|
String apiKey; // Fetched from Vault
|
||||||
|
|
||||||
|
public String getSecret(String key) {
|
||||||
|
return ConfigProvider.getConfig().getValue(key, String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RateLimitFilter implements ContainerRequestFilter {
|
||||||
|
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
|
String clientId = getClientIdentifier(requestContext);
|
||||||
|
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||||
|
k -> RateLimiter.create(100.0)); // 100 requests per second
|
||||||
|
|
||||||
|
if (!limiter.tryAcquire()) {
|
||||||
|
requestContext.abortWith(
|
||||||
|
Response.status(429)
|
||||||
|
.entity(Map.of("error", "Too many requests"))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClientIdentifier(ContainerRequestContext ctx) {
|
||||||
|
// Use IP, API key, or user ID
|
||||||
|
return ctx.getHeaderString("X-Forwarded-For");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Headers
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||||
|
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||||
|
|
||||||
|
// Prevent clickjacking
|
||||||
|
headers.putSingle("X-Frame-Options", "DENY");
|
||||||
|
|
||||||
|
// XSS protection
|
||||||
|
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||||
|
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||||
|
|
||||||
|
// HSTS
|
||||||
|
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
|
||||||
|
// CSP
|
||||||
|
headers.putSingle("Content-Security-Policy",
|
||||||
|
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Logging
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuditService {
|
||||||
|
private static final Logger LOG = Logger.getLogger(AuditService.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public void logAccess(String resource, String action) {
|
||||||
|
String user = securityIdentity.isAnonymous()
|
||||||
|
? "anonymous"
|
||||||
|
: securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
|
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||||
|
user, action, resource, Instant.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in resource
|
||||||
|
@Path("/api/sensitive")
|
||||||
|
public class SensitiveResource {
|
||||||
|
@Inject
|
||||||
|
AuditService auditService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@RolesAllowed("ADMIN")
|
||||||
|
public Response getData() {
|
||||||
|
auditService.logAccess("sensitive-data", "READ");
|
||||||
|
return Response.ok(data).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Security Scanning
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Maven
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./gradlew dependencyCheckAnalyze
|
||||||
|
|
||||||
|
# Check Quarkus extensions
|
||||||
|
quarkus extension list --installable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Always use HTTPS in production
|
||||||
|
- Enable JWT or OIDC for stateless authentication
|
||||||
|
- Use `@RolesAllowed` for declarative authorization
|
||||||
|
- Validate all input with Bean Validation
|
||||||
|
- Hash passwords with BCrypt (never plaintext)
|
||||||
|
- Store secrets in Vault or environment variables
|
||||||
|
- Use parameterized queries to prevent SQL injection
|
||||||
|
- Add security headers to all responses
|
||||||
|
- Implement rate limiting for public endpoints
|
||||||
|
- Audit sensitive operations
|
||||||
|
- Keep dependencies updated and scan for CVEs
|
||||||
|
- Use SecurityIdentity for programmatic checks
|
||||||
|
- Set appropriate CORS policies
|
||||||
|
- Test authentication and authorization paths
|
||||||
908
docs/tr/skills/quarkus-tdd/SKILL.md
Normal file
908
docs/tr/skills/quarkus-tdd/SKILL.md
Normal file
@ -0,0 +1,908 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-tdd
|
||||||
|
description: Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito, REST Assured, Camel testing, and JaCoCo. Use when adding features, fixing bugs, or refactoring event-driven services.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus TDD Workflow
|
||||||
|
|
||||||
|
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- New features or REST endpoints
|
||||||
|
- Bug fixes or refactors
|
||||||
|
- Adding data access logic, security rules, or reactive streams
|
||||||
|
- Testing Apache Camel routes and event handlers
|
||||||
|
- Testing event-driven services with RabbitMQ
|
||||||
|
- Testing conditional flow logic
|
||||||
|
- Validating CompletableFuture async operations
|
||||||
|
- Testing LogContext propagation
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Write tests first (they should fail)
|
||||||
|
2. Implement minimal code to pass
|
||||||
|
3. Refactor with tests green
|
||||||
|
4. Enforce coverage with JaCoCo (80%+ target)
|
||||||
|
|
||||||
|
## Unit Tests with @Nested Organization
|
||||||
|
|
||||||
|
Follow this structured approach for comprehensive, readable tests:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("As2ProcessingService Unit Tests")
|
||||||
|
class As2ProcessingServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EventService eventService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DocumentJobService documentJobService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private As2ProcessingService as2ProcessingService;
|
||||||
|
|
||||||
|
private Path testFilePath;
|
||||||
|
private LogContext testLogContext;
|
||||||
|
private InvoiceValidationResult validationResult;
|
||||||
|
private StoredDocumentInfo documentInfo;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE - Common test data
|
||||||
|
testFilePath = Path.of("/tmp/test-invoice.xml");
|
||||||
|
|
||||||
|
testLogContext = new LogContext();
|
||||||
|
testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001");
|
||||||
|
testLogContext.put(As2Constants.FILE_NAME, "invoice.xml");
|
||||||
|
testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001");
|
||||||
|
|
||||||
|
validationResult = new InvoiceValidationResult();
|
||||||
|
validationResult.setValid(true);
|
||||||
|
validationResult.setSize(1024L);
|
||||||
|
validationResult.setDocumentHash("abc123");
|
||||||
|
|
||||||
|
documentInfo = new StoredDocumentInfo();
|
||||||
|
documentInfo.setPath("s3://bucket/path/invoice.xml");
|
||||||
|
documentInfo.setSize(1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for processFile")
|
||||||
|
class ProcessFile {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully process non-CHORUS file with all validations")
|
||||||
|
void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class)))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any()))
|
||||||
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class));
|
||||||
|
|
||||||
|
verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class),
|
||||||
|
eq("PERSISTENCE_BLOB_EVENT_TYPE"));
|
||||||
|
verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class),
|
||||||
|
eq("BUSINESS_RULES_MESSAGE_SENT"));
|
||||||
|
verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should bypass schematron validation for CHORUS_FLOW")
|
||||||
|
void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class)))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(),
|
||||||
|
eq(FlowProfile.EXTENDED_CTC_FR), any()))
|
||||||
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class));
|
||||||
|
|
||||||
|
verify(documentJobService).createDocumentAndJobEntities(
|
||||||
|
any(), any(), any(),
|
||||||
|
eq(FlowProfile.EXTENDED_CTC_FR),
|
||||||
|
any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create error event when file upload fails")
|
||||||
|
void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
documentInfo.setPath(""); // Blank path triggers error
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
As2ServerProcessingException exception = assertThrows(
|
||||||
|
As2ServerProcessingException.class,
|
||||||
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage())
|
||||||
|
.contains("File path is empty after upload");
|
||||||
|
|
||||||
|
verify(eventService).createErrorEvent(
|
||||||
|
eq(documentInfo),
|
||||||
|
eq("FILE_UPLOAD_FAILED"),
|
||||||
|
contains("File path is empty"));
|
||||||
|
|
||||||
|
verify(businessRulesPublisher, never()).publishAsync(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle CompletableFuture.join() failure")
|
||||||
|
void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
CompletableFuture<StoredDocumentInfo> failedFuture =
|
||||||
|
CompletableFuture.failedFuture(new StorageException("S3 connection failed"));
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(failedFuture);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
assertThrows(
|
||||||
|
CompletionException.class,
|
||||||
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when file path is null")
|
||||||
|
void givenNullFilePath_whenProcessFile_thenThrowsException() {
|
||||||
|
// ARRANGE
|
||||||
|
Path nullPath = null;
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
NullPointerException exception = assertThrows(
|
||||||
|
NullPointerException.class,
|
||||||
|
() -> as2ProcessingService.processFile(nullPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Testing Patterns
|
||||||
|
|
||||||
|
1. **@Nested Classes**: Group tests by method being tested
|
||||||
|
2. **@DisplayName**: Provide readable test descriptions for test reports
|
||||||
|
3. **Naming Convention**: `givenX_whenY_thenZ` for clarity
|
||||||
|
4. **AAA Pattern**: Explicit `// ARRANGE`, `// ACT`, `// ASSERT` comments
|
||||||
|
5. **@BeforeEach**: Setup common test data to reduce duplication
|
||||||
|
6. **assertDoesNotThrow**: Test success scenarios without catching exceptions
|
||||||
|
7. **assertThrows**: Test exception scenarios with message validation using AssertJ
|
||||||
|
8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions
|
||||||
|
9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly
|
||||||
|
10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios
|
||||||
|
|
||||||
|
## Testing Camel Routes
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("Business Rules Camel Route Tests")
|
||||||
|
class BusinessRulesRouteTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CamelContext camelContext;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
EventService eventService;
|
||||||
|
|
||||||
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE - Test data
|
||||||
|
testPayload = new BusinessRulesPayload();
|
||||||
|
testPayload.setDocumentId(1L);
|
||||||
|
testPayload.setFlowProfile(FlowProfile.BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for business-rules-publisher route")
|
||||||
|
class BusinessRulesPublisher {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully publish message to RabbitMQ")
|
||||||
|
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||||
|
mockRabbitMQ.expectedMessageCount(1);
|
||||||
|
mockRabbitMQ.expectedBodiesReceived(testPayload);
|
||||||
|
|
||||||
|
// Replace real endpoint with mock for testing
|
||||||
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
|
advice.replaceFromWith("direct:business-rules-publisher");
|
||||||
|
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockRabbitMQ.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
||||||
|
assertThat(mockRabbitMQ.getExchanges().get(0).getIn().getBody(BusinessRulesPayload.class))
|
||||||
|
.isEqualTo(testPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle marshalling to JSON")
|
||||||
|
void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
||||||
|
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
||||||
|
mockMarshal.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
|
advice.weaveAddLast().to("mock:marshal");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockMarshal.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
|
assertThat(body).contains("\"documentId\":1");
|
||||||
|
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for document-processing route")
|
||||||
|
class DocumentProcessing {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should route invoice to correct processor")
|
||||||
|
void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
||||||
|
mockInvoice.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
|
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBodyAndHeader("direct:process-document",
|
||||||
|
testPayload, "documentType", "INVOICE");
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockInvoice.assertIsSatisfied(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle validation errors gracefully")
|
||||||
|
void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
||||||
|
mockError.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
|
advice.weaveByToString(".*direct:validation-error-handler.*")
|
||||||
|
.replace().to("mock:error");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
|
// Mock validator to throw exception
|
||||||
|
when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:process-document", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockError.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
Exception exception = mockError.getExchanges().get(0).getException();
|
||||||
|
assertThat(exception).isInstanceOf(ValidationException.class);
|
||||||
|
assertThat(exception.getMessage()).contains("Invalid document");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Event Services
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("EventService Unit Tests")
|
||||||
|
class EventServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private EventService eventService;
|
||||||
|
|
||||||
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE
|
||||||
|
testPayload = new BusinessRulesPayload();
|
||||||
|
testPayload.setDocumentId(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for createSuccessEvent")
|
||||||
|
class CreateSuccessEvent {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create success event with correct attributes")
|
||||||
|
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() ->
|
||||||
|
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(eventRepository).persist(argThat(event ->
|
||||||
|
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||||
|
event.getStatus() == EventStatus.SUCCESS &&
|
||||||
|
event.getPayload().equals("{\"documentId\":1}") &&
|
||||||
|
event.getTimestamp() != null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when payload is null")
|
||||||
|
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||||
|
// ARRANGE
|
||||||
|
Object nullPayload = null;
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
||||||
|
verify(eventRepository, never()).persist(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for createErrorEvent")
|
||||||
|
class CreateErrorEvent {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create error event with error message")
|
||||||
|
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
String errorMessage = "Processing failed";
|
||||||
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() ->
|
||||||
|
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(eventRepository).persist(argThat(event ->
|
||||||
|
event.getType().equals("PROCESSING_ERROR") &&
|
||||||
|
event.getStatus() == EventStatus.ERROR &&
|
||||||
|
event.getErrorMessage().equals(errorMessage) &&
|
||||||
|
event.getPayload().equals("{\"documentId\":1}")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@DisplayName("Should reject invalid error messages")
|
||||||
|
@ValueSource(strings = {"", " "})
|
||||||
|
void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
|
||||||
|
// ACT & ASSERT
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing CompletableFuture
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("FileStorageService Unit Tests")
|
||||||
|
class FileStorageServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private S3Client s3Client;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExecutorService executorService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
private InputStream testInputStream;
|
||||||
|
private LogContext testLogContext;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE
|
||||||
|
testInputStream = new ByteArrayInputStream("test content".getBytes());
|
||||||
|
testLogContext = new LogContext();
|
||||||
|
testLogContext.put("traceId", "trace-123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for uploadOriginalFile")
|
||||||
|
class UploadOriginalFile {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully upload file and return document info")
|
||||||
|
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
Callable<?> callable = invocation.getArgument(0);
|
||||||
|
return CompletableFuture.completedFuture(callable.call());
|
||||||
|
});
|
||||||
|
|
||||||
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
|
.thenReturn(PutObjectResponse.builder().build());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
|
StoredDocumentInfo result = future.join();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getPath()).isNotBlank();
|
||||||
|
assertThat(result.getSize()).isEqualTo(1024L);
|
||||||
|
assertThat(result.getUploadedAt()).isNotNull();
|
||||||
|
|
||||||
|
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle S3 upload failure")
|
||||||
|
void givenS3Failure_whenUpload_thenCompletableFutureFails() {
|
||||||
|
// ARRANGE
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
return CompletableFuture.failedFuture(new StorageException("S3 unavailable"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThatThrownBy(() -> future.join())
|
||||||
|
.isInstanceOf(CompletionException.class)
|
||||||
|
.hasCauseInstanceOf(StorageException.class)
|
||||||
|
.hasMessageContaining("S3 unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should propagate LogContext to async operation")
|
||||||
|
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
||||||
|
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
Callable<?> callable = invocation.getArgument(0);
|
||||||
|
capturedContext.set(CustomLog.getCurrentContext());
|
||||||
|
return CompletableFuture.completedFuture(callable.call());
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL).join();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThat(capturedContext.get()).isNotNull();
|
||||||
|
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource Layer Tests (REST Assured)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("DocumentResource API Tests")
|
||||||
|
class DocumentResourceTest {
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
DocumentService documentService;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for GET /api/documents")
|
||||||
|
class ListDocuments {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return list of documents")
|
||||||
|
void givenDocumentsExist_whenList_thenReturnsOk() {
|
||||||
|
// ARRANGE
|
||||||
|
List<Document> documents = List.of(createDocument(1L, "DOC-001"));
|
||||||
|
when(documentService.list(0, 20)).thenReturn(documents);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.when().get("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$.size()", is(1))
|
||||||
|
.body("[0].referenceNumber", equalTo("DOC-001"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for POST /api/documents")
|
||||||
|
class CreateDocument {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create document and return 201")
|
||||||
|
void givenValidRequest_whenCreate_thenReturns201() {
|
||||||
|
// ARRANGE
|
||||||
|
Document document = createDocument(1L, "DOC-001");
|
||||||
|
when(documentService.create(any())).thenReturn(document);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "DOC-001",
|
||||||
|
"description": "Test document",
|
||||||
|
"validUntil": "2030-01-01T00:00:00Z",
|
||||||
|
"categories": ["test"]
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.header("Location", containsString("/api/documents/1"))
|
||||||
|
.body("referenceNumber", equalTo("DOC-001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return 400 for invalid input")
|
||||||
|
void givenInvalidRequest_whenCreate_thenReturns400() {
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "",
|
||||||
|
"description": "Test"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document createDocument(Long id, String referenceNumber) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setId(id);
|
||||||
|
document.setReferenceNumber(referenceNumber);
|
||||||
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Tests with Real Database
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@TestProfile(IntegrationTestProfile.class)
|
||||||
|
@DisplayName("Document Integration Tests")
|
||||||
|
class DocumentIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
@DisplayName("Should create and retrieve document via API")
|
||||||
|
void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
|
||||||
|
// ACT - Create via API
|
||||||
|
Long id = given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "INT-001",
|
||||||
|
"description": "Integration test",
|
||||||
|
"validUntil": "2030-01-01T00:00:00Z",
|
||||||
|
"categories": ["test"]
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.extract().path("id");
|
||||||
|
|
||||||
|
// ASSERT - Retrieve via API
|
||||||
|
given()
|
||||||
|
.when().get("/api/documents/" + id)
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("referenceNumber", equalTo("INT-001"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage with JaCoCo
|
||||||
|
|
||||||
|
### Maven Configuration (Complete)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.13</version>
|
||||||
|
<executions>
|
||||||
|
<!-- Prepare agent for test execution -->
|
||||||
|
<execution>
|
||||||
|
<id>prepare-agent</id>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<!-- Generate coverage report -->
|
||||||
|
<execution>
|
||||||
|
<id>report</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<!-- Enforce coverage thresholds -->
|
||||||
|
<execution>
|
||||||
|
<id>check</id>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.80</minimum>
|
||||||
|
</limit>
|
||||||
|
<limit>
|
||||||
|
<counter>BRANCH</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.70</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests with coverage:
|
||||||
|
```bash
|
||||||
|
mvn clean test
|
||||||
|
mvn jacoco:report
|
||||||
|
mvn jacoco:check
|
||||||
|
|
||||||
|
# Report at: target/site/jacoco/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Dependencies
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependencies>
|
||||||
|
<!-- Quarkus Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Mockito -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- AssertJ (preferred over JUnit assertions) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.24.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- REST Assured -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.rest-assured</groupId>
|
||||||
|
<artifactId>rest-assured</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Camel Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
- Use `@Nested` classes to group tests by method being tested
|
||||||
|
- Use `@DisplayName` for readable test descriptions visible in reports
|
||||||
|
- Follow `givenX_whenY_thenZ` naming convention for test methods
|
||||||
|
- Use `@BeforeEach` for common test data setup to reduce duplication
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
- Follow AAA pattern with explicit comments (`// ARRANGE`, `// ACT`, `// ASSERT`)
|
||||||
|
- Use `assertDoesNotThrow` for success scenarios
|
||||||
|
- Use `assertThrows` for exception scenarios with message validation
|
||||||
|
- Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()`
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
- Test happy paths for all public methods
|
||||||
|
- Test null input handling
|
||||||
|
- Test edge cases (empty collections, boundary values, negative IDs, blank strings)
|
||||||
|
- Test exception scenarios comprehensively
|
||||||
|
- Mock all external dependencies (repositories, services, Camel endpoints)
|
||||||
|
- Aim for 80%+ line coverage, 70%+ branch coverage
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
- **Always use AssertJ** (`assertThat`) instead of JUnit assertions
|
||||||
|
- Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)`
|
||||||
|
- For exceptions: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
|
||||||
|
- For collections: `extracting()`, `filteredOn()`, `containsExactly()`
|
||||||
|
|
||||||
|
### Testing Integration
|
||||||
|
- Use `@QuarkusTest` for integration tests
|
||||||
|
- Use `@InjectMock` to mock dependencies in Quarkus tests
|
||||||
|
- Prefer REST Assured for API testing
|
||||||
|
- Use `@TestProfile` for test-specific configuration
|
||||||
|
|
||||||
|
### Event-Driven Testing
|
||||||
|
- Test Camel routes with `AdviceWith` and `MockEndpoint`
|
||||||
|
- Use `@CamelQuarkusTest` annotation (if using standalone Camel tests)
|
||||||
|
- Verify message content, headers, and routing logic
|
||||||
|
- Test error handling routes separately
|
||||||
|
- Mock external systems (RabbitMQ, S3, databases) in unit tests
|
||||||
|
|
||||||
|
### Camel Route Testing
|
||||||
|
- Use `MockEndpoint` for asserting message flow
|
||||||
|
- Use `AdviceWith` to modify routes for testing (replace endpoints with mocks)
|
||||||
|
- Test message transformation and marshalling
|
||||||
|
- Test exception handling and dead letter queues
|
||||||
|
|
||||||
|
### Testing Async Operations
|
||||||
|
- Test CompletableFuture success and failure scenarios
|
||||||
|
- Use `.join()` in tests to wait for async completion
|
||||||
|
- Test exception propagation from CompletableFuture
|
||||||
|
- Verify LogContext propagation to async operations
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Keep tests fast and isolated
|
||||||
|
- Run tests in continuous mode: `mvn quarkus:test`
|
||||||
|
- Use parameterized tests (`@ParameterizedTest`) for input variations
|
||||||
|
- Build reusable test data builders or factory methods
|
||||||
|
|
||||||
|
### Quarkus-Specific
|
||||||
|
- Stay on latest LTS version (Quarkus 3.x)
|
||||||
|
- Test native compilation compatibility periodically
|
||||||
|
- Use Quarkus test profiles for different scenarios
|
||||||
|
- Leverage Quarkus dev services for local testing
|
||||||
|
- Use `@InjectMock` instead of `@MockBean` (Quarkus-specific)
|
||||||
|
|
||||||
|
### Verification Best Practices
|
||||||
|
- Always verify interactions on mocked dependencies
|
||||||
|
- Use `verify(mock, never())` to ensure methods are NOT called in error scenarios
|
||||||
|
- Use `argThat()` for complex argument matching
|
||||||
|
- Verify the order of calls when it matters: `InOrder` from Mockito
|
||||||
481
docs/tr/skills/quarkus-verification/SKILL.md
Normal file
481
docs/tr/skills/quarkus-verification/SKILL.md
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-verification
|
||||||
|
description: "Verification loop for Quarkus projects: build, static analysis, tests with coverage, security scans, native compilation, and diff review before release or PR."
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Verification Loop
|
||||||
|
|
||||||
|
Run before PRs, after major changes, and pre-deploy.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Before opening a pull request for a Quarkus service
|
||||||
|
- After major refactoring or dependency upgrades
|
||||||
|
- Pre-deployment verification for staging or production
|
||||||
|
- Running full build → lint → test → security scan → native compilation pipeline
|
||||||
|
- Validating test coverage meets thresholds (80%+)
|
||||||
|
- Testing native image compatibility
|
||||||
|
|
||||||
|
## Phase 1: Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Maven
|
||||||
|
mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./gradlew clean assemble -x test
|
||||||
|
```
|
||||||
|
|
||||||
|
If build fails, stop and fix compilation errors.
|
||||||
|
|
||||||
|
## Phase 2: Static Analysis
|
||||||
|
|
||||||
|
### Checkstyle, PMD, SpotBugs (Maven)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn checkstyle:check pmd:check spotbugs:check
|
||||||
|
```
|
||||||
|
|
||||||
|
### SonarQube (if configured)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn sonar:sonar \
|
||||||
|
-Dsonar.projectKey=my-quarkus-project \
|
||||||
|
-Dsonar.host.url=http://localhost:9000 \
|
||||||
|
-Dsonar.login=${SONAR_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues to Address
|
||||||
|
|
||||||
|
- Unused imports or variables
|
||||||
|
- Complex methods (high cyclomatic complexity)
|
||||||
|
- Potential null pointer dereferences
|
||||||
|
- Security issues flagged by SpotBugs
|
||||||
|
|
||||||
|
## Phase 3: Tests + Coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
mvn clean test
|
||||||
|
|
||||||
|
# Generate coverage report
|
||||||
|
mvn jacoco:report
|
||||||
|
|
||||||
|
# Enforce coverage threshold (80%)
|
||||||
|
mvn jacoco:check
|
||||||
|
|
||||||
|
# Or with Gradle
|
||||||
|
./gradlew test jacocoTestReport jacocoTestCoverageVerification
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Categories
|
||||||
|
|
||||||
|
#### Unit Tests
|
||||||
|
Test service logic with mocked dependencies:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserServiceTest {
|
||||||
|
@Mock UserRepository userRepository;
|
||||||
|
@InjectMocks UserService userService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_validInput_returnsUser() {
|
||||||
|
var dto = new CreateUserDto("Alice", "alice@example.com");
|
||||||
|
var expected = new User();
|
||||||
|
expected.id = 1L;
|
||||||
|
expected.name = dto.name();
|
||||||
|
|
||||||
|
when(userRepository.persist(any(User.class))).thenReturn(expected);
|
||||||
|
|
||||||
|
User result = userService.create(dto);
|
||||||
|
|
||||||
|
assertThat(result.name).isEqualTo("Alice");
|
||||||
|
verify(userRepository).persist(any(User.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Integration Tests
|
||||||
|
Test with real database (Testcontainers):
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@QuarkusTestResource(PostgresTestResource.class)
|
||||||
|
class UserRepositoryIntegrationTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UserRepository userRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void findByEmail_existingUser_returnsUser() {
|
||||||
|
User user = new User();
|
||||||
|
user.name = "Alice";
|
||||||
|
user.email = "alice@example.com";
|
||||||
|
userRepository.persist(user);
|
||||||
|
|
||||||
|
Optional<User> found = userRepository.findByEmail("alice@example.com");
|
||||||
|
|
||||||
|
assertThat(found).isPresent();
|
||||||
|
assertThat(found.get().name).isEqualTo("Alice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Tests
|
||||||
|
Test REST endpoints with REST Assured:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
class UserResourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_validInput_returns201() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{"name": "Alice", "email": "alice@example.com"}
|
||||||
|
""")
|
||||||
|
.when().post("/api/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.body("name", equalTo("Alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_invalidEmail_returns400() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{"name": "Alice", "email": "invalid"}
|
||||||
|
""")
|
||||||
|
.when().post("/api/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage Report
|
||||||
|
|
||||||
|
Check `target/site/jacoco/index.html` for detailed coverage:
|
||||||
|
- Overall line coverage (target: 80%+)
|
||||||
|
- Branch coverage (target: 70%+)
|
||||||
|
- Identify uncovered critical paths
|
||||||
|
|
||||||
|
## Phase 4: Security Scanning
|
||||||
|
|
||||||
|
### Dependency Vulnerabilities (Maven)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
```
|
||||||
|
|
||||||
|
Review `target/dependency-check-report.html` for CVEs.
|
||||||
|
|
||||||
|
### Quarkus Security Audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check vulnerable extensions
|
||||||
|
mvn quarkus:audit
|
||||||
|
|
||||||
|
# List all extensions
|
||||||
|
mvn quarkus:list-extensions
|
||||||
|
```
|
||||||
|
|
||||||
|
### OWASP ZAP (API Security Testing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -t owasp/zap2docker-stable zap-api-scan.py \
|
||||||
|
-t http://localhost:8080/q/openapi \
|
||||||
|
-f openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Security Checks
|
||||||
|
|
||||||
|
- [ ] All secrets in environment variables (not in code)
|
||||||
|
- [ ] Input validation on all endpoints
|
||||||
|
- [ ] Authentication/authorization configured
|
||||||
|
- [ ] CORS properly configured
|
||||||
|
- [ ] Security headers set
|
||||||
|
- [ ] Passwords hashed with BCrypt
|
||||||
|
- [ ] SQL injection protection (parameterized queries)
|
||||||
|
- [ ] Rate limiting on public endpoints
|
||||||
|
|
||||||
|
## Phase 5: Native Compilation
|
||||||
|
|
||||||
|
Test GraalVM native image compatibility:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build native executable
|
||||||
|
mvn package -Dnative
|
||||||
|
|
||||||
|
# Or with container
|
||||||
|
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||||
|
|
||||||
|
# Test native executable
|
||||||
|
./target/*-runner
|
||||||
|
|
||||||
|
# Run basic smoke tests
|
||||||
|
curl http://localhost:8080/q/health/live
|
||||||
|
curl http://localhost:8080/q/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native Image Troubleshooting
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
- **Reflection**: Add reflection config for dynamic classes
|
||||||
|
- **Resources**: Include resources with `quarkus.native.resources.includes`
|
||||||
|
- **JNI**: Register JNI classes if using native libraries
|
||||||
|
|
||||||
|
Example reflection config:
|
||||||
|
```java
|
||||||
|
@RegisterForReflection(targets = {MyDynamicClass.class})
|
||||||
|
public class ReflectionConfiguration {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 6: Performance Testing
|
||||||
|
|
||||||
|
### Load Testing with K6
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// load-test.js
|
||||||
|
import http from 'k6/http';
|
||||||
|
import { check } from 'k6';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: '30s', target: 50 },
|
||||||
|
{ duration: '1m', target: 100 },
|
||||||
|
{ duration: '30s', target: 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const res = http.get('http://localhost:8080/api/markets');
|
||||||
|
check(res, {
|
||||||
|
'status is 200': (r) => r.status === 200,
|
||||||
|
'response time < 200ms': (r) => r.timings.duration < 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
k6 run load-test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics to Monitor
|
||||||
|
|
||||||
|
- Response time (p50, p95, p99)
|
||||||
|
- Throughput (requests/sec)
|
||||||
|
- Error rate
|
||||||
|
- Memory usage
|
||||||
|
- CPU usage
|
||||||
|
|
||||||
|
## Phase 7: Health Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Liveness
|
||||||
|
curl http://localhost:8080/q/health/live
|
||||||
|
|
||||||
|
# Readiness
|
||||||
|
curl http://localhost:8080/q/health/ready
|
||||||
|
|
||||||
|
# All health checks
|
||||||
|
curl http://localhost:8080/q/health
|
||||||
|
|
||||||
|
# Metrics (if enabled)
|
||||||
|
curl http://localhost:8080/q/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected responses:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "UP",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"name": "Database connection",
|
||||||
|
"status": "UP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 8: Container Image Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build container image
|
||||||
|
mvn package -Dquarkus.container-image.build=true
|
||||||
|
|
||||||
|
# Or with specific registry
|
||||||
|
mvn package \
|
||||||
|
-Dquarkus.container-image.build=true \
|
||||||
|
-Dquarkus.container-image.registry=docker.io \
|
||||||
|
-Dquarkus.container-image.group=myorg \
|
||||||
|
-Dquarkus.container-image.tag=1.0.0
|
||||||
|
|
||||||
|
# Test container
|
||||||
|
docker run -p 8080:8080 myorg/my-quarkus-app:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Security Scan
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trivy
|
||||||
|
trivy image myorg/my-quarkus-app:1.0.0
|
||||||
|
|
||||||
|
# Grype
|
||||||
|
grype myorg/my-quarkus-app:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 9: Configuration Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all configuration properties
|
||||||
|
mvn quarkus:info
|
||||||
|
|
||||||
|
# List all config sources
|
||||||
|
curl http://localhost:8080/q/dev/io.quarkus.quarkus-vertx-http/config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment-Specific Checks
|
||||||
|
|
||||||
|
- [ ] Database URLs configured per environment
|
||||||
|
- [ ] Secrets externalized (Vault, env vars)
|
||||||
|
- [ ] Logging levels appropriate
|
||||||
|
- [ ] CORS origins set correctly
|
||||||
|
- [ ] Rate limiting configured
|
||||||
|
- [ ] Monitoring/tracing enabled
|
||||||
|
|
||||||
|
## Phase 10: Documentation Review
|
||||||
|
|
||||||
|
- [ ] OpenAPI/Swagger docs up to date (`/q/swagger-ui`)
|
||||||
|
- [ ] README has setup instructions
|
||||||
|
- [ ] API changes documented
|
||||||
|
- [ ] Migration guide for breaking changes
|
||||||
|
- [ ] Configuration properties documented
|
||||||
|
|
||||||
|
Generate OpenAPI spec:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/q/openapi -o openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- [ ] Build passes without warnings
|
||||||
|
- [ ] Static analysis clean (no high/medium issues)
|
||||||
|
- [ ] Code follows team conventions
|
||||||
|
- [ ] No commented-out code or TODOs in PR
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Code coverage ≥ 80%
|
||||||
|
- [ ] Integration tests with real database
|
||||||
|
- [ ] Security tests pass
|
||||||
|
- [ ] Performance within acceptable limits
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- [ ] No dependency vulnerabilities
|
||||||
|
- [ ] Authentication/authorization tested
|
||||||
|
- [ ] Input validation complete
|
||||||
|
- [ ] Secrets not in source code
|
||||||
|
- [ ] Security headers configured
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
- [ ] Native compilation successful
|
||||||
|
- [ ] Container image builds
|
||||||
|
- [ ] Health checks respond correctly
|
||||||
|
- [ ] Configuration valid for target environment
|
||||||
|
|
||||||
|
### Native Image
|
||||||
|
- [ ] Native executable builds
|
||||||
|
- [ ] Native tests pass
|
||||||
|
- [ ] Startup time < 100ms
|
||||||
|
- [ ] Memory footprint acceptable
|
||||||
|
|
||||||
|
## Automated Verification Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Phase 1: Build ==="
|
||||||
|
mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
echo "=== Phase 2: Static Analysis ==="
|
||||||
|
mvn checkstyle:check pmd:check spotbugs:check
|
||||||
|
|
||||||
|
echo "=== Phase 3: Tests + Coverage ==="
|
||||||
|
mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
|
echo "=== Phase 4: Security Scan ==="
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
echo "=== Phase 5: Native Compilation ==="
|
||||||
|
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||||
|
|
||||||
|
echo "=== All Phases Complete ==="
|
||||||
|
echo "Review reports:"
|
||||||
|
echo " - Coverage: target/site/jacoco/index.html"
|
||||||
|
echo " - Security: target/dependency-check-report.html"
|
||||||
|
echo " - Native: target/*-runner"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Verification
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '21'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Cache Maven packages
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.m2
|
||||||
|
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
- name: Test with Coverage
|
||||||
|
run: mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
|
- name: Security Scan
|
||||||
|
run: mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
- name: Upload Coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: target/site/jacoco/jacoco.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Run verification loop before every PR
|
||||||
|
- Automate in CI/CD pipeline
|
||||||
|
- Fix issues immediately; don't accumulate debt
|
||||||
|
- Keep coverage above 80%
|
||||||
|
- Update dependencies regularly
|
||||||
|
- Test native compilation periodically
|
||||||
|
- Monitor performance trends
|
||||||
|
- Document breaking changes
|
||||||
|
- Review security scan results
|
||||||
|
- Validate configuration for each environment
|
||||||
@ -333,6 +333,10 @@ everything-claude-code/
|
|||||||
| |-- laravel-verification/ # Laravel 验证循环(新增)
|
| |-- laravel-verification/ # Laravel 验证循环(新增)
|
||||||
| |-- python-patterns/ # Python 习惯用法与最佳实践(新增)
|
| |-- python-patterns/ # Python 习惯用法与最佳实践(新增)
|
||||||
| |-- python-testing/ # 使用 pytest 的 Python 测试(新增)
|
| |-- python-testing/ # 使用 pytest 的 Python 测试(新增)
|
||||||
|
| |-- quarkus-patterns/ # Java Quarkus 模式(新增)
|
||||||
|
| |-- quarkus-security/ # Quarkus 安全(新增)
|
||||||
|
| |-- quarkus-tdd/ # Quarkus TDD(新增)
|
||||||
|
| |-- quarkus-verification/ # Quarkus 验证(新增)
|
||||||
| |-- springboot-patterns/ # Java Spring Boot 模式(新增)
|
| |-- springboot-patterns/ # Java Spring Boot 模式(新增)
|
||||||
| |-- springboot-security/ # Spring Boot 安全(新增)
|
| |-- springboot-security/ # Spring Boot 安全(新增)
|
||||||
| |-- springboot-tdd/ # Spring Boot TDD(新增)
|
| |-- springboot-tdd/ # Spring Boot TDD(新增)
|
||||||
@ -657,7 +661,7 @@ cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/
|
|||||||
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
||||||
|
|
||||||
# Optional: add niche/framework-specific skills only when needed
|
# Optional: add niche/framework-specific skills only when needed
|
||||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns; do
|
# for s in django-patterns django-tdd laravel-patterns springboot-patterns quarkus-patterns; do
|
||||||
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
||||||
# done
|
# done
|
||||||
```
|
```
|
||||||
|
|||||||
@ -151,4 +151,6 @@ grep -A5 "annotationProcessorPaths\|annotationProcessor" pom.xml build.gradle
|
|||||||
|
|
||||||
最终:`Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
最终:`Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||||
|
|
||||||
有关详细的 Java 和 Spring Boot 模式,请参阅 `skill: springboot-patterns`。
|
有关详细的模式和示例:
|
||||||
|
* **[SPRING]**:请参阅 `skill: springboot-patterns`
|
||||||
|
* **[QUARKUS]**:请参阅 `skill: quarkus-patterns`
|
||||||
|
|||||||
@ -102,4 +102,6 @@ grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
|||||||
* **警告**:仅存在**中**优先级问题
|
* **警告**:仅存在**中**优先级问题
|
||||||
* **阻止**:发现**关键**或**高**优先级问题
|
* **阻止**:发现**关键**或**高**优先级问题
|
||||||
|
|
||||||
有关详细的Spring Boot模式和示例,请参阅 `skill: springboot-patterns`。
|
有关详细的模式和示例:
|
||||||
|
* **[SPRING]**:请参阅 `skill: springboot-patterns`
|
||||||
|
* **[QUARKUS]**:请参阅 `skill: quarkus-patterns`
|
||||||
|
|||||||
@ -144,4 +144,5 @@ public record ApiResponse<T>(boolean success, T data, String error) {
|
|||||||
## 参考
|
## 参考
|
||||||
|
|
||||||
有关 Spring Boot 架构模式,请参见技能:`springboot-patterns`。
|
有关 Spring Boot 架构模式,请参见技能:`springboot-patterns`。
|
||||||
|
有关使用 Camel 和 Panache 的 Quarkus 架构模式,请参见技能:`quarkus-patterns`。
|
||||||
有关实体设计和查询优化,请参见技能:`jpa-patterns`。
|
有关实体设计和查询优化,请参见技能:`jpa-patterns`。
|
||||||
|
|||||||
@ -98,4 +98,5 @@ try {
|
|||||||
## 参考
|
## 参考
|
||||||
|
|
||||||
关于 Spring Security 认证与授权模式,请参见技能:`springboot-security`。
|
关于 Spring Security 认证与授权模式,请参见技能:`springboot-security`。
|
||||||
|
关于使用 JWT/OIDC、RBAC 和 CDI 的 Quarkus 安全模式,请参见技能:`quarkus-security`。
|
||||||
关于通用安全检查清单,请参见技能:`security-review`。
|
关于通用安全检查清单,请参见技能:`security-review`。
|
||||||
|
|||||||
@ -113,6 +113,7 @@ class OrderRepositoryIT {
|
|||||||
```
|
```
|
||||||
|
|
||||||
关于 Spring Boot 集成测试,请参阅技能:`springboot-tdd`。
|
关于 Spring Boot 集成测试,请参阅技能:`springboot-tdd`。
|
||||||
|
关于 Quarkus 集成测试,请参阅技能:`quarkus-tdd`。
|
||||||
|
|
||||||
## 测试命名
|
## 测试命名
|
||||||
|
|
||||||
@ -130,4 +131,5 @@ class OrderRepositoryIT {
|
|||||||
## 参考
|
## 参考
|
||||||
|
|
||||||
关于使用 MockMvc 和 Testcontainers 的 Spring Boot TDD 模式,请参阅技能:`springboot-tdd`。
|
关于使用 MockMvc 和 Testcontainers 的 Spring Boot TDD 模式,请参阅技能:`springboot-tdd`。
|
||||||
|
关于使用 REST Assured 和 Camel 测试的 Quarkus TDD 模式,请参阅技能:`quarkus-tdd`。
|
||||||
关于测试期望,请参阅技能:`java-coding-standards`。
|
关于测试期望,请参阅技能:`java-coding-standards`。
|
||||||
|
|||||||
@ -126,6 +126,10 @@ mkdir -p $TARGET/skills $TARGET/rules
|
|||||||
| `java-coding-standards` | Spring Boot 的 Java 编码标准:命名、不可变性、Optional、流 |
|
| `java-coding-standards` | Spring Boot 的 Java 编码标准:命名、不可变性、Optional、流 |
|
||||||
| `python-patterns` | Pythonic 惯用法、PEP 8、类型提示、最佳实践 |
|
| `python-patterns` | Pythonic 惯用法、PEP 8、类型提示、最佳实践 |
|
||||||
| `python-testing` | 使用 pytest、TDD、夹具、模拟、参数化进行 Python 测试 |
|
| `python-testing` | 使用 pytest、TDD、夹具、模拟、参数化进行 Python 测试 |
|
||||||
|
| `quarkus-patterns` | Quarkus 架构、使用 Camel 的事件驱动模式、Panache 数据访问、CDI 服务 |
|
||||||
|
| `quarkus-security` | Quarkus 安全:JWT/OIDC 认证、RBAC、Bean 验证、CORS、密钥管理 |
|
||||||
|
| `quarkus-tdd` | 使用 JUnit 5、Mockito、REST Assured、Camel 测试进行 Quarkus TDD |
|
||||||
|
| `quarkus-verification` | Quarkus 验证:构建、静态分析、测试、安全扫描、原生编译 |
|
||||||
| `springboot-patterns` | Spring Boot 架构、REST API、分层服务、缓存、异步处理 |
|
| `springboot-patterns` | Spring Boot 架构、REST API、分层服务、缓存、异步处理 |
|
||||||
| `springboot-security` | Spring Security:认证/授权、验证、CSRF、密钥、速率限制 |
|
| `springboot-security` | Spring Security:认证/授权、验证、CSRF、密钥、速率限制 |
|
||||||
| `springboot-tdd` | 使用 JUnit 5、Mockito、MockMvc、Testcontainers 进行 Spring Boot TDD |
|
| `springboot-tdd` | 使用 JUnit 5、Mockito、MockMvc、Testcontainers 进行 Spring Boot TDD |
|
||||||
@ -274,6 +278,7 @@ grep -rn "skills/" $TARGET/skills/
|
|||||||
|
|
||||||
* `django-tdd` 可能会引用 `django-patterns`
|
* `django-tdd` 可能会引用 `django-patterns`
|
||||||
* `laravel-tdd` 可能会引用 `laravel-patterns`
|
* `laravel-tdd` 可能会引用 `laravel-patterns`
|
||||||
|
* `quarkus-tdd` 可能会引用 `quarkus-patterns`
|
||||||
* `springboot-tdd` 可能会引用 `springboot-patterns`
|
* `springboot-tdd` 可能会引用 `springboot-patterns`
|
||||||
* `continuous-learning-v2` 引用 `~/.claude/homunculus/` 目录
|
* `continuous-learning-v2` 引用 `~/.claude/homunculus/` 目录
|
||||||
* `python-testing` 可能会引用 `python-patterns`
|
* `python-testing` 可能会引用 `python-patterns`
|
||||||
|
|||||||
@ -117,6 +117,7 @@ metadata:
|
|||||||
| Python / Django | django-patterns, django-tdd, django-security, django-verification, python-patterns, python-testing | python-reviewer |
|
| Python / Django | django-patterns, django-tdd, django-security, django-verification, python-patterns, python-testing | python-reviewer |
|
||||||
| Go | golang-patterns, golang-testing | go-reviewer, go-build-resolver |
|
| Go | golang-patterns, golang-testing | go-reviewer, go-build-resolver |
|
||||||
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | code-reviewer |
|
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | code-reviewer |
|
||||||
|
| Quarkus / Java | quarkus-patterns, quarkus-tdd, quarkus-security, quarkus-verification, java-coding-standards, jpa-patterns | code-reviewer |
|
||||||
| Kotlin / Android | kotlin-coroutines-flows, compose-multiplatform-patterns, android-clean-architecture | kotlin-reviewer |
|
| Kotlin / Android | kotlin-coroutines-flows, compose-multiplatform-patterns, android-clean-architecture | kotlin-reviewer |
|
||||||
| TypeScript / React | frontend-patterns, backend-patterns, coding-standards | code-reviewer |
|
| TypeScript / React | frontend-patterns, backend-patterns, coding-standards | code-reviewer |
|
||||||
| Swift / iOS | swiftui-patterns, swift-concurrency-6-2, swift-actor-persistence, swift-protocol-di-testing | code-reviewer |
|
| Swift / iOS | swiftui-patterns, swift-concurrency-6-2, swift-actor-persistence, swift-protocol-di-testing | code-reviewer |
|
||||||
|
|||||||
754
docs/zh-CN/skills/quarkus-patterns/SKILL.md
Normal file
754
docs/zh-CN/skills/quarkus-patterns/SKILL.md
Normal file
@ -0,0 +1,754 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-patterns
|
||||||
|
description: Quarkus 3.x LTS architecture patterns with Camel for messaging, RESTful API design, CDI services, data access with Panache, and async processing. Use for Java Quarkus backend work with event-driven architectures.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Development Patterns
|
||||||
|
|
||||||
|
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Building REST APIs with JAX-RS or RESTEasy Reactive
|
||||||
|
- Structuring resource → service → repository layers
|
||||||
|
- Implementing event-driven patterns with Apache Camel and RabbitMQ
|
||||||
|
- Configuring Hibernate Panache, caching, or reactive streams
|
||||||
|
- Adding validation, exception mapping, or pagination
|
||||||
|
- Setting up profiles for dev/staging/production environments (YAML config)
|
||||||
|
- Custom logging with LogContext and Logback/Logstash encoder
|
||||||
|
- Working with CompletableFuture for async operations
|
||||||
|
- Implementing conditional flow processing
|
||||||
|
- Working with GraalVM native compilation
|
||||||
|
|
||||||
|
## Service Layer with Multiple Dependencies (Lombok)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class As2ProcessingService {
|
||||||
|
|
||||||
|
private final InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
private final EventService eventService;
|
||||||
|
private final DocumentJobService documentJobService;
|
||||||
|
private final BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
private final FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
public void processFile(Path filePath) throws Exception {
|
||||||
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
|
||||||
|
String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID);
|
||||||
|
|
||||||
|
// Conditional flow logic
|
||||||
|
boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW));
|
||||||
|
log.info("Is CHORUS_FLOW message: {}", isChorusFlow);
|
||||||
|
|
||||||
|
ValidationFlowConfig validationFlowConfig = isChorusFlow
|
||||||
|
? ValidationFlowConfig.xsdOnly()
|
||||||
|
: ValidationFlowConfig.allValidations();
|
||||||
|
|
||||||
|
InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator
|
||||||
|
.validateFlowWithConfig(filePath, validationFlowConfig,
|
||||||
|
EInvoiceSyntaxFormat.UBL, logContext);
|
||||||
|
|
||||||
|
FlowProfile flowProfile = isChorusFlow ?
|
||||||
|
FlowProfile.EXTENDED_CTC_FR :
|
||||||
|
this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult,
|
||||||
|
invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile());
|
||||||
|
|
||||||
|
log.info("Invoice validation completed. Message is valid");
|
||||||
|
|
||||||
|
// CompletableFuture async operation
|
||||||
|
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
||||||
|
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
||||||
|
fileStorageService.uploadOriginalFile(inputStream,
|
||||||
|
invoiceValidationResult.getSize(), logContext,
|
||||||
|
invoiceValidationResult.getInvoiceFormat());
|
||||||
|
|
||||||
|
StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join();
|
||||||
|
log.info("File uploaded successfully: {}", documentInfo.getPath());
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(documentInfo.getPath())) {
|
||||||
|
String errorMsg = "File path is empty after upload";
|
||||||
|
log.error(errorMsg);
|
||||||
|
this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg);
|
||||||
|
throw new As2ServerProcessingException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE");
|
||||||
|
|
||||||
|
BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities(
|
||||||
|
documentInfo, originalFileName, structureIdPartner,
|
||||||
|
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||||
|
|
||||||
|
// Async Camel publishing
|
||||||
|
businessRulesPublisher.publishAsync(payload);
|
||||||
|
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Patterns:**
|
||||||
|
- `@RequiredArgsConstructor` for constructor injection via Lombok
|
||||||
|
- `@Slf4j` for Logback logging
|
||||||
|
- Scoped LogContext with try-with-resources
|
||||||
|
- Conditional flow logic based on runtime parameters
|
||||||
|
- CompletableFuture with `.join()` for async operations
|
||||||
|
- Event tracking for success/error scenarios
|
||||||
|
- Async Camel message publishing
|
||||||
|
|
||||||
|
## Custom Logging Context Pattern (Logback)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ProcessingService {
|
||||||
|
|
||||||
|
public void processDocument(Document doc) {
|
||||||
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
// Add context to all log statements
|
||||||
|
logContext.put("documentId", doc.getId().toString());
|
||||||
|
logContext.put("documentType", doc.getType());
|
||||||
|
logContext.put("userId", SecurityContext.getUserId());
|
||||||
|
|
||||||
|
log.info("Starting document processing");
|
||||||
|
|
||||||
|
// All logs within this scope inherit the context
|
||||||
|
processInternal(doc);
|
||||||
|
|
||||||
|
log.info("Document processing completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Document processing failed", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logback Configuration (logback.xml):**
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<configuration>
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||||
|
<includeContext>true</includeContext>
|
||||||
|
<includeMdc>true</includeMdc>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="com.example" level="INFO"/>
|
||||||
|
<root level="WARN">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Service Pattern
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EventService {
|
||||||
|
private final EventRepository eventRepository;
|
||||||
|
|
||||||
|
public void createSuccessEvent(Object payload, String eventType) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setType(eventType);
|
||||||
|
event.setStatus(EventStatus.SUCCESS);
|
||||||
|
event.setPayload(serializePayload(payload));
|
||||||
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
|
eventRepository.persist(event);
|
||||||
|
log.info("Success event created: {}", eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setType(eventType);
|
||||||
|
event.setStatus(EventStatus.ERROR);
|
||||||
|
event.setErrorMessage(errorMessage);
|
||||||
|
event.setPayload(serializePayload(payload));
|
||||||
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
|
eventRepository.persist(event);
|
||||||
|
log.error("Error event created: {} - {}", eventType, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializePayload(Object payload) {
|
||||||
|
// JSON serialization
|
||||||
|
return objectMapper.writeValueAsString(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Message Publishing (RabbitMQ)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BusinessRulesPublisher {
|
||||||
|
private final ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
|
String businessRulesQueue;
|
||||||
|
|
||||||
|
public void publishAsync(BusinessRulesPayload payload) {
|
||||||
|
producerTemplate.asyncSendBody(
|
||||||
|
"direct:business-rules-publisher",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
log.info("Message published to business rules queue: {}", payload.getDocumentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishSync(BusinessRulesPayload payload) {
|
||||||
|
producerTemplate.sendBody(
|
||||||
|
"direct:business-rules-publisher",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Camel Route Configuration:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class BusinessRulesRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
|
String businessRulesQueue;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "rabbitmq.host")
|
||||||
|
String rabbitHost;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "rabbitmq.port")
|
||||||
|
Integer rabbitPort;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("direct:business-rules-publisher")
|
||||||
|
.routeId("business-rules-publisher")
|
||||||
|
.log("Publishing message to RabbitMQ: ${body}")
|
||||||
|
.marshal().json(JsonLibrary.Jackson)
|
||||||
|
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
||||||
|
businessRulesQueue, rabbitHost, rabbitPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Direct Routes (In-Memory)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class DocumentProcessingRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
// Error handling
|
||||||
|
onException(ValidationException.class)
|
||||||
|
.handled(true)
|
||||||
|
.to("direct:validation-error-handler")
|
||||||
|
.log("Validation error: ${exception.message}");
|
||||||
|
|
||||||
|
// Main processing route
|
||||||
|
from("direct:process-document")
|
||||||
|
.routeId("document-processing")
|
||||||
|
.log("Processing document: ${header.documentId}")
|
||||||
|
.bean(DocumentValidator.class, "validate")
|
||||||
|
.bean(DocumentTransformer.class, "transform")
|
||||||
|
.choice()
|
||||||
|
.when(header("documentType").isEqualTo("INVOICE"))
|
||||||
|
.to("direct:process-invoice")
|
||||||
|
.when(header("documentType").isEqualTo("CREDIT_NOTE"))
|
||||||
|
.to("direct:process-credit-note")
|
||||||
|
.otherwise()
|
||||||
|
.to("direct:process-generic")
|
||||||
|
.end();
|
||||||
|
|
||||||
|
from("direct:validation-error-handler")
|
||||||
|
.bean(EventService.class, "createErrorEvent")
|
||||||
|
.log("Validation error handled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel File Processing
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class FileMonitoringRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.input.directory")
|
||||||
|
String inputDirectory;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.processed.directory")
|
||||||
|
String processedDirectory;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.error.directory")
|
||||||
|
String errorDirectory;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
||||||
|
"&moveFailed=" + errorDirectory + "&delay=5000")
|
||||||
|
.routeId("file-monitor")
|
||||||
|
.log("Processing file: ${header.CamelFileName}")
|
||||||
|
.to("direct:process-file");
|
||||||
|
|
||||||
|
from("direct:process-file")
|
||||||
|
.bean(As2ProcessingService.class, "processFile")
|
||||||
|
.log("File processing completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Bean Invocation
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class InvoiceRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("direct:invoice-validation")
|
||||||
|
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
||||||
|
.log("Validation result: ${body}");
|
||||||
|
|
||||||
|
from("direct:persist-and-publish")
|
||||||
|
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
||||||
|
.bean(BusinessRulesPublisher.class, "publishAsync")
|
||||||
|
.bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST API Structure
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Path("/api/documents")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentResource {
|
||||||
|
private final DocumentService documentService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response list(
|
||||||
|
@QueryParam("page") @DefaultValue("0") int page,
|
||||||
|
@QueryParam("size") @DefaultValue("20") int size) {
|
||||||
|
PaginatedList<Document> documents = documentService.list(page, size);
|
||||||
|
return Response.ok(documents).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public Response create(@Valid CreateDocumentRequest request, @Context UriInfo uriInfo) {
|
||||||
|
Document document = documentService.create(request);
|
||||||
|
URI location = uriInfo.getAbsolutePathBuilder()
|
||||||
|
.path(String.valueOf(document.id))
|
||||||
|
.build();
|
||||||
|
return Response.created(location).entity(DocumentResponse.from(document)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
public Response getById(@PathParam("id") Long id) {
|
||||||
|
return documentService.findById(id)
|
||||||
|
.map(DocumentResponse::from)
|
||||||
|
.map(Response::ok)
|
||||||
|
.orElse(Response.status(Response.Status.NOT_FOUND))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Pattern (Panache Repository)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class DocumentRepository implements PanacheRepository<Document> {
|
||||||
|
|
||||||
|
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
||||||
|
return find("status = ?1 order by createdAt desc", status)
|
||||||
|
.page(page, size)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
||||||
|
return find("referenceNumber", referenceNumber).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
||||||
|
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Layer with Transactions
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentService {
|
||||||
|
private final DocumentRepository repo;
|
||||||
|
private final EventService eventService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Document create(CreateDocumentRequest request) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setReferenceNumber(request.referenceNumber());
|
||||||
|
document.setDescription(request.description());
|
||||||
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
|
document.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
|
repo.persist(document);
|
||||||
|
|
||||||
|
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findById(Long id) {
|
||||||
|
return repo.findByIdOptional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaginatedList<Document> list(int page, int size) {
|
||||||
|
return repo.findAll()
|
||||||
|
.page(page, size)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DTOs and Validation
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record CreateDocumentRequest(
|
||||||
|
@NotBlank @Size(max = 200) String referenceNumber,
|
||||||
|
@NotBlank @Size(max = 2000) String description,
|
||||||
|
@NotNull @FutureOrPresent Instant validUntil,
|
||||||
|
@NotEmpty List<@NotBlank String> categories) {}
|
||||||
|
|
||||||
|
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
||||||
|
public static DocumentResponse from(Document document) {
|
||||||
|
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
||||||
|
document.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exception Mapping
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
|
||||||
|
@Override
|
||||||
|
public Response toResponse(ConstraintViolationException exception) {
|
||||||
|
String message = exception.getConstraintViolations().stream()
|
||||||
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "validation_error", "message", message))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
@Slf4j
|
||||||
|
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(Exception exception) {
|
||||||
|
log.error("Unhandled exception", exception);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "internal_error", "message", "An unexpected error occurred"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CompletableFuture Async Operations
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FileStorageService {
|
||||||
|
private final S3Client s3Client;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
|
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
||||||
|
InputStream inputStream,
|
||||||
|
long size,
|
||||||
|
LogContext logContext,
|
||||||
|
InvoiceFormat format) {
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
String path = generateStoragePath(format);
|
||||||
|
|
||||||
|
PutObjectRequest request = PutObjectRequest.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.key(path)
|
||||||
|
.contentLength(size)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
||||||
|
|
||||||
|
log.info("File uploaded to S3: {}", path);
|
||||||
|
|
||||||
|
return new StoredDocumentInfo(path, size, Instant.now());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to upload file to S3", e);
|
||||||
|
throw new StorageException("Upload failed", e);
|
||||||
|
}
|
||||||
|
}, executorService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentCacheService {
|
||||||
|
private final DocumentRepository repo;
|
||||||
|
|
||||||
|
@CacheResult(cacheName = "document-cache")
|
||||||
|
public Optional<Document> getById(@CacheKey Long id) {
|
||||||
|
return repo.findByIdOptional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheInvalidate(cacheName = "document-cache")
|
||||||
|
public void evict(@CacheKey Long id) {}
|
||||||
|
|
||||||
|
@CacheInvalidateAll(cacheName = "document-cache")
|
||||||
|
public void evictAll() {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration as YAML
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# application.yml
|
||||||
|
"%dev":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: jdbc:postgresql://localhost:5432/dev_db
|
||||||
|
username: dev_user
|
||||||
|
password: dev_pass
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: drop-and-create
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: localhost
|
||||||
|
port: 5672
|
||||||
|
username: guest
|
||||||
|
password: guest
|
||||||
|
|
||||||
|
"%test":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: jdbc:h2:mem:test
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: drop-and-create
|
||||||
|
|
||||||
|
"%prod":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: ${DATABASE_URL}
|
||||||
|
username: ${DB_USER}
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: validate
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: ${RABBITMQ_HOST}
|
||||||
|
port: ${RABBITMQ_PORT}
|
||||||
|
username: ${RABBITMQ_USER}
|
||||||
|
password: ${RABBITMQ_PASSWORD}
|
||||||
|
|
||||||
|
# Camel configuration
|
||||||
|
camel:
|
||||||
|
rabbitmq:
|
||||||
|
queue:
|
||||||
|
business-rules: business-rules-queue
|
||||||
|
invoice-processing: invoice-processing-queue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Readiness
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DatabaseHealthCheck implements HealthCheck {
|
||||||
|
private final AgroalDataSource dataSource;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
try (Connection conn = dataSource.getConnection()) {
|
||||||
|
boolean valid = conn.isValid(2);
|
||||||
|
return HealthCheckResponse.named("Database connection")
|
||||||
|
.status(valid)
|
||||||
|
.build();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return HealthCheckResponse.down("Database connection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Liveness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class CamelHealthCheck implements HealthCheck {
|
||||||
|
@Inject
|
||||||
|
CamelContext camelContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
boolean isStarted = camelContext.getStatus().isStarted();
|
||||||
|
return HealthCheckResponse.named("Camel Context")
|
||||||
|
.status(isStarted)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies (Maven)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<properties>
|
||||||
|
<quarkus.platform.version>3.27.0</quarkus.platform.version>
|
||||||
|
<lombok.version>1.18.42</lombok.version>
|
||||||
|
<assertj-core.version>3.24.2</assertj-core.version>
|
||||||
|
<jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus.platform</groupId>
|
||||||
|
<artifactId>quarkus-bom</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-bom</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Quarkus Core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-arc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Camel Extensions -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-direct</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-bean</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Logging -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||||
|
<artifactId>quarkus-logging-logback</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.logstash.logback</groupId>
|
||||||
|
<artifactId>logstash-logback-encoder</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- Use `@RequiredArgsConstructor` with Lombok for constructor injection
|
||||||
|
- Keep service layer thin; delegate complex logic to specialized classes
|
||||||
|
- Use Camel routes for message routing and integration patterns
|
||||||
|
- Prefer Panache Repository pattern for data access
|
||||||
|
|
||||||
|
### Event-Driven
|
||||||
|
- Always track operations with EventService (success/error events)
|
||||||
|
- Use Camel `direct:` endpoints for in-memory routing
|
||||||
|
- Use `spring-rabbitmq` component for RabbitMQ integration
|
||||||
|
- Implement async publishing with `ProducerTemplate.asyncSendBody()`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Use Logback with Logstash encoder for structured logging
|
||||||
|
- Propagate LogContext through service calls with `SafeAutoCloseable`
|
||||||
|
- Add contextual information to LogContext for request tracing
|
||||||
|
- Use `@Slf4j` instead of manual logger instantiation
|
||||||
|
|
||||||
|
### Async Operations
|
||||||
|
- Use CompletableFuture for non-blocking I/O operations
|
||||||
|
- Call `.join()` when you need to wait for completion
|
||||||
|
- Handle exceptions from CompletableFuture properly
|
||||||
|
- Pass LogContext to async operations for tracing
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Use YAML configuration (`quarkus-config-yaml`)
|
||||||
|
- Profile-aware configuration for dev/test/prod environments
|
||||||
|
- Externalize sensitive configuration to environment variables
|
||||||
|
- Use `@ConfigProperty` for type-safe config injection
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
- Validate at resource layer with `@Valid`
|
||||||
|
- Use Bean Validation annotations on DTOs
|
||||||
|
- Map exceptions to proper HTTP responses with `@Provider`
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
- Use `@Transactional` on service methods that modify data
|
||||||
|
- Keep transactions short and focused
|
||||||
|
- Avoid calling async operations within transactions
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Use `camel-quarkus-junit5` for route testing
|
||||||
|
- Use AssertJ for assertions
|
||||||
|
- Mock all external dependencies
|
||||||
|
- Test conditional flow logic thoroughly
|
||||||
|
|
||||||
|
### Quarkus-Specific
|
||||||
|
- Stay on latest LTS version (3.x)
|
||||||
|
- Use Quarkus dev mode for hot reload
|
||||||
|
- Add health checks for production readiness
|
||||||
|
- Test native compilation compatibility periodically
|
||||||
453
docs/zh-CN/skills/quarkus-security/SKILL.md
Normal file
453
docs/zh-CN/skills/quarkus-security/SKILL.md
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-security
|
||||||
|
description: Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Security Review
|
||||||
|
|
||||||
|
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Adding authentication (JWT, OIDC, Basic Auth)
|
||||||
|
- Implementing authorization with @RolesAllowed or SecurityIdentity
|
||||||
|
- Validating user input (Bean Validation, custom validators)
|
||||||
|
- Configuring CORS or security headers
|
||||||
|
- Managing secrets (Vault, environment variables, config sources)
|
||||||
|
- Adding rate limiting or brute-force protection
|
||||||
|
- Scanning dependencies for CVEs
|
||||||
|
- Working with MicroProfile JWT or SmallRye JWT
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### JWT Authentication
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Resource protected with JWT
|
||||||
|
@Path("/api/protected")
|
||||||
|
@Authenticated
|
||||||
|
public class ProtectedResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JsonWebToken jwt;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response getData() {
|
||||||
|
String username = jwt.getName();
|
||||||
|
Set<String> roles = jwt.getGroups();
|
||||||
|
return Response.ok(Map.of(
|
||||||
|
"username", username,
|
||||||
|
"roles", roles,
|
||||||
|
"principal", securityIdentity.getPrincipal().getName()
|
||||||
|
)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration (application.properties):
|
||||||
|
```properties
|
||||||
|
mp.jwt.verify.publickey.location=publicKey.pem
|
||||||
|
mp.jwt.verify.issuer=https://auth.example.com
|
||||||
|
|
||||||
|
# OIDC
|
||||||
|
quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm
|
||||||
|
quarkus.oidc.client-id=backend-service
|
||||||
|
quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Authentication Filter
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
|
public class CustomAuthFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity identity;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
// Validate token and set SecurityIdentity
|
||||||
|
if (!validateToken(token)) {
|
||||||
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateToken(String token) {
|
||||||
|
// Token validation logic
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
### Role-Based Access Control
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Path("/api/admin")
|
||||||
|
@RolesAllowed("ADMIN")
|
||||||
|
public class AdminResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/users")
|
||||||
|
public List<UserDto> listUsers() {
|
||||||
|
return userService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/users/{id}")
|
||||||
|
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
||||||
|
public Response deleteUser(@PathParam("id") Long id) {
|
||||||
|
userService.delete(id);
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/api/users")
|
||||||
|
public class UserResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
public Response getUser(@PathParam("id") Long id) {
|
||||||
|
// Check ownership
|
||||||
|
if (!securityIdentity.hasRole("ADMIN") &&
|
||||||
|
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
return Response.ok(userService.findById(id)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOwner(Long userId, String username) {
|
||||||
|
return userService.isOwner(userId, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Security
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SecurityService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public boolean canAccessResource(Long resourceId) {
|
||||||
|
if (securityIdentity.isAnonymous()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (securityIdentity.hasRole("ADMIN")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = securityIdentity.getPrincipal().getName();
|
||||||
|
return resourceRepository.isOwner(resourceId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
### Bean Validation
|
||||||
|
|
||||||
|
```java
|
||||||
|
// BAD: No validation
|
||||||
|
@POST
|
||||||
|
public Response createUser(UserDto dto) {
|
||||||
|
return Response.ok(userService.create(dto)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Validated DTO
|
||||||
|
public record CreateUserDto(
|
||||||
|
@NotBlank @Size(max = 100) String name,
|
||||||
|
@NotBlank @Email String email,
|
||||||
|
@NotNull @Min(18) @Max(150) Integer age,
|
||||||
|
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/users")
|
||||||
|
public Response createUser(@Valid CreateUserDto dto) {
|
||||||
|
User user = userService.create(dto);
|
||||||
|
return Response.status(Response.Status.CREATED).entity(user).build();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Validators
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Constraint(validatedBy = UsernameValidator.class)
|
||||||
|
public @interface ValidUsername {
|
||||||
|
String message() default "Invalid username format";
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||||
|
if (value == null) return false;
|
||||||
|
return value.matches("^[a-zA-Z0-9_-]{3,20}$");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
public record CreateUserDto(
|
||||||
|
@ValidUsername String username,
|
||||||
|
@NotBlank @Email String email
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQL Injection Prevention
|
||||||
|
|
||||||
|
### Panache Active Record (Safe by Default)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// GOOD: Parameterized queries with Panache
|
||||||
|
List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
||||||
|
|
||||||
|
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||||
|
|
||||||
|
// GOOD: Named parameters
|
||||||
|
List<User> users = User.list("email = :email and age > :minAge",
|
||||||
|
Parameters.with("email", email).and("minAge", 18));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native Queries (Use Parameters)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// BAD: String concatenation
|
||||||
|
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||||
|
|
||||||
|
// GOOD: Parameterized native query
|
||||||
|
@Entity
|
||||||
|
public class User extends PanacheEntity {
|
||||||
|
public static List<User> findByEmailNative(String email) {
|
||||||
|
return getEntityManager()
|
||||||
|
.createNativeQuery("SELECT * FROM users WHERE email = :email", User.class)
|
||||||
|
.setParameter("email", email)
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Hashing
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class PasswordService {
|
||||||
|
|
||||||
|
public String hash(String plainPassword) {
|
||||||
|
return BcryptUtil.bcryptHash(plainPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(String plainPassword, String hashedPassword) {
|
||||||
|
return BcryptUtil.matches(plainPassword, hashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In service
|
||||||
|
@ApplicationScoped
|
||||||
|
public class UserService {
|
||||||
|
@Inject
|
||||||
|
PasswordService passwordService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User register(CreateUserDto dto) {
|
||||||
|
String hashedPassword = passwordService.hash(dto.password());
|
||||||
|
User user = new User();
|
||||||
|
user.email = dto.email();
|
||||||
|
user.password = hashedPassword;
|
||||||
|
user.persist();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authenticate(String email, String password) {
|
||||||
|
return User.find("email", email)
|
||||||
|
.firstResultOptional()
|
||||||
|
.map(u -> passwordService.verify(password, u.password))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS Configuration
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# application.properties
|
||||||
|
quarkus.http.cors=true
|
||||||
|
quarkus.http.cors.origins=https://app.example.com,https://admin.example.com
|
||||||
|
quarkus.http.cors.methods=GET,POST,PUT,DELETE
|
||||||
|
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
|
||||||
|
quarkus.http.cors.exposed-headers=Content-Disposition
|
||||||
|
quarkus.http.cors.access-control-max-age=24H
|
||||||
|
quarkus.http.cors.access-control-allow-credentials=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets Management
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# application.properties - NO SECRETS HERE
|
||||||
|
|
||||||
|
# Use environment variables
|
||||||
|
quarkus.datasource.username=${DB_USER}
|
||||||
|
quarkus.datasource.password=${DB_PASSWORD}
|
||||||
|
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
||||||
|
|
||||||
|
# Or use Vault
|
||||||
|
quarkus.vault.url=https://vault.example.com
|
||||||
|
quarkus.vault.authentication.kubernetes.role=my-role
|
||||||
|
```
|
||||||
|
|
||||||
|
### HashiCorp Vault Integration
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SecretService {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "api-key")
|
||||||
|
String apiKey; // Fetched from Vault
|
||||||
|
|
||||||
|
public String getSecret(String key) {
|
||||||
|
return ConfigProvider.getConfig().getValue(key, String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RateLimitFilter implements ContainerRequestFilter {
|
||||||
|
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
|
String clientId = getClientIdentifier(requestContext);
|
||||||
|
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||||
|
k -> RateLimiter.create(100.0)); // 100 requests per second
|
||||||
|
|
||||||
|
if (!limiter.tryAcquire()) {
|
||||||
|
requestContext.abortWith(
|
||||||
|
Response.status(429)
|
||||||
|
.entity(Map.of("error", "Too many requests"))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClientIdentifier(ContainerRequestContext ctx) {
|
||||||
|
// Use IP, API key, or user ID
|
||||||
|
return ctx.getHeaderString("X-Forwarded-For");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Headers
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||||
|
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||||
|
|
||||||
|
// Prevent clickjacking
|
||||||
|
headers.putSingle("X-Frame-Options", "DENY");
|
||||||
|
|
||||||
|
// XSS protection
|
||||||
|
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||||
|
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||||
|
|
||||||
|
// HSTS
|
||||||
|
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
|
||||||
|
// CSP
|
||||||
|
headers.putSingle("Content-Security-Policy",
|
||||||
|
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Logging
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuditService {
|
||||||
|
private static final Logger LOG = Logger.getLogger(AuditService.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public void logAccess(String resource, String action) {
|
||||||
|
String user = securityIdentity.isAnonymous()
|
||||||
|
? "anonymous"
|
||||||
|
: securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
|
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||||
|
user, action, resource, Instant.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in resource
|
||||||
|
@Path("/api/sensitive")
|
||||||
|
public class SensitiveResource {
|
||||||
|
@Inject
|
||||||
|
AuditService auditService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@RolesAllowed("ADMIN")
|
||||||
|
public Response getData() {
|
||||||
|
auditService.logAccess("sensitive-data", "READ");
|
||||||
|
return Response.ok(data).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Security Scanning
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Maven
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./gradlew dependencyCheckAnalyze
|
||||||
|
|
||||||
|
# Check Quarkus extensions
|
||||||
|
quarkus extension list --installable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Always use HTTPS in production
|
||||||
|
- Enable JWT or OIDC for stateless authentication
|
||||||
|
- Use `@RolesAllowed` for declarative authorization
|
||||||
|
- Validate all input with Bean Validation
|
||||||
|
- Hash passwords with BCrypt (never plaintext)
|
||||||
|
- Store secrets in Vault or environment variables
|
||||||
|
- Use parameterized queries to prevent SQL injection
|
||||||
|
- Add security headers to all responses
|
||||||
|
- Implement rate limiting for public endpoints
|
||||||
|
- Audit sensitive operations
|
||||||
|
- Keep dependencies updated and scan for CVEs
|
||||||
|
- Use SecurityIdentity for programmatic checks
|
||||||
|
- Set appropriate CORS policies
|
||||||
|
- Test authentication and authorization paths
|
||||||
908
docs/zh-CN/skills/quarkus-tdd/SKILL.md
Normal file
908
docs/zh-CN/skills/quarkus-tdd/SKILL.md
Normal file
@ -0,0 +1,908 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-tdd
|
||||||
|
description: Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito, REST Assured, Camel testing, and JaCoCo. Use when adding features, fixing bugs, or refactoring event-driven services.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus TDD Workflow
|
||||||
|
|
||||||
|
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- New features or REST endpoints
|
||||||
|
- Bug fixes or refactors
|
||||||
|
- Adding data access logic, security rules, or reactive streams
|
||||||
|
- Testing Apache Camel routes and event handlers
|
||||||
|
- Testing event-driven services with RabbitMQ
|
||||||
|
- Testing conditional flow logic
|
||||||
|
- Validating CompletableFuture async operations
|
||||||
|
- Testing LogContext propagation
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Write tests first (they should fail)
|
||||||
|
2. Implement minimal code to pass
|
||||||
|
3. Refactor with tests green
|
||||||
|
4. Enforce coverage with JaCoCo (80%+ target)
|
||||||
|
|
||||||
|
## Unit Tests with @Nested Organization
|
||||||
|
|
||||||
|
Follow this structured approach for comprehensive, readable tests:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("As2ProcessingService Unit Tests")
|
||||||
|
class As2ProcessingServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EventService eventService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DocumentJobService documentJobService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private As2ProcessingService as2ProcessingService;
|
||||||
|
|
||||||
|
private Path testFilePath;
|
||||||
|
private LogContext testLogContext;
|
||||||
|
private InvoiceValidationResult validationResult;
|
||||||
|
private StoredDocumentInfo documentInfo;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE - Common test data
|
||||||
|
testFilePath = Path.of("/tmp/test-invoice.xml");
|
||||||
|
|
||||||
|
testLogContext = new LogContext();
|
||||||
|
testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001");
|
||||||
|
testLogContext.put(As2Constants.FILE_NAME, "invoice.xml");
|
||||||
|
testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001");
|
||||||
|
|
||||||
|
validationResult = new InvoiceValidationResult();
|
||||||
|
validationResult.setValid(true);
|
||||||
|
validationResult.setSize(1024L);
|
||||||
|
validationResult.setDocumentHash("abc123");
|
||||||
|
|
||||||
|
documentInfo = new StoredDocumentInfo();
|
||||||
|
documentInfo.setPath("s3://bucket/path/invoice.xml");
|
||||||
|
documentInfo.setSize(1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for processFile")
|
||||||
|
class ProcessFile {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully process non-CHORUS file with all validations")
|
||||||
|
void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class)))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any()))
|
||||||
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class));
|
||||||
|
|
||||||
|
verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class),
|
||||||
|
eq("PERSISTENCE_BLOB_EVENT_TYPE"));
|
||||||
|
verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class),
|
||||||
|
eq("BUSINESS_RULES_MESSAGE_SENT"));
|
||||||
|
verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should bypass schematron validation for CHORUS_FLOW")
|
||||||
|
void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class)))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(),
|
||||||
|
eq(FlowProfile.EXTENDED_CTC_FR), any()))
|
||||||
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class));
|
||||||
|
|
||||||
|
verify(documentJobService).createDocumentAndJobEntities(
|
||||||
|
any(), any(), any(),
|
||||||
|
eq(FlowProfile.EXTENDED_CTC_FR),
|
||||||
|
any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create error event when file upload fails")
|
||||||
|
void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
documentInfo.setPath(""); // Blank path triggers error
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
As2ServerProcessingException exception = assertThrows(
|
||||||
|
As2ServerProcessingException.class,
|
||||||
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage())
|
||||||
|
.contains("File path is empty after upload");
|
||||||
|
|
||||||
|
verify(eventService).createErrorEvent(
|
||||||
|
eq(documentInfo),
|
||||||
|
eq("FILE_UPLOAD_FAILED"),
|
||||||
|
contains("File path is empty"));
|
||||||
|
|
||||||
|
verify(businessRulesPublisher, never()).publishAsync(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle CompletableFuture.join() failure")
|
||||||
|
void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
CompletableFuture<StoredDocumentInfo> failedFuture =
|
||||||
|
CompletableFuture.failedFuture(new StorageException("S3 connection failed"));
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(failedFuture);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
assertThrows(
|
||||||
|
CompletionException.class,
|
||||||
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when file path is null")
|
||||||
|
void givenNullFilePath_whenProcessFile_thenThrowsException() {
|
||||||
|
// ARRANGE
|
||||||
|
Path nullPath = null;
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
NullPointerException exception = assertThrows(
|
||||||
|
NullPointerException.class,
|
||||||
|
() -> as2ProcessingService.processFile(nullPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Testing Patterns
|
||||||
|
|
||||||
|
1. **@Nested Classes**: Group tests by method being tested
|
||||||
|
2. **@DisplayName**: Provide readable test descriptions for test reports
|
||||||
|
3. **Naming Convention**: `givenX_whenY_thenZ` for clarity
|
||||||
|
4. **AAA Pattern**: Explicit `// ARRANGE`, `// ACT`, `// ASSERT` comments
|
||||||
|
5. **@BeforeEach**: Setup common test data to reduce duplication
|
||||||
|
6. **assertDoesNotThrow**: Test success scenarios without catching exceptions
|
||||||
|
7. **assertThrows**: Test exception scenarios with message validation using AssertJ
|
||||||
|
8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions
|
||||||
|
9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly
|
||||||
|
10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios
|
||||||
|
|
||||||
|
## Testing Camel Routes
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("Business Rules Camel Route Tests")
|
||||||
|
class BusinessRulesRouteTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CamelContext camelContext;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
EventService eventService;
|
||||||
|
|
||||||
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE - Test data
|
||||||
|
testPayload = new BusinessRulesPayload();
|
||||||
|
testPayload.setDocumentId(1L);
|
||||||
|
testPayload.setFlowProfile(FlowProfile.BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for business-rules-publisher route")
|
||||||
|
class BusinessRulesPublisher {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully publish message to RabbitMQ")
|
||||||
|
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||||
|
mockRabbitMQ.expectedMessageCount(1);
|
||||||
|
mockRabbitMQ.expectedBodiesReceived(testPayload);
|
||||||
|
|
||||||
|
// Replace real endpoint with mock for testing
|
||||||
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
|
advice.replaceFromWith("direct:business-rules-publisher");
|
||||||
|
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockRabbitMQ.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
||||||
|
assertThat(mockRabbitMQ.getExchanges().get(0).getIn().getBody(BusinessRulesPayload.class))
|
||||||
|
.isEqualTo(testPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle marshalling to JSON")
|
||||||
|
void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
||||||
|
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
||||||
|
mockMarshal.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
|
advice.weaveAddLast().to("mock:marshal");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockMarshal.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
|
assertThat(body).contains("\"documentId\":1");
|
||||||
|
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for document-processing route")
|
||||||
|
class DocumentProcessing {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should route invoice to correct processor")
|
||||||
|
void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
||||||
|
mockInvoice.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
|
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBodyAndHeader("direct:process-document",
|
||||||
|
testPayload, "documentType", "INVOICE");
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockInvoice.assertIsSatisfied(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle validation errors gracefully")
|
||||||
|
void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
||||||
|
mockError.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
|
advice.weaveByToString(".*direct:validation-error-handler.*")
|
||||||
|
.replace().to("mock:error");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
|
// Mock validator to throw exception
|
||||||
|
when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:process-document", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockError.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
Exception exception = mockError.getExchanges().get(0).getException();
|
||||||
|
assertThat(exception).isInstanceOf(ValidationException.class);
|
||||||
|
assertThat(exception.getMessage()).contains("Invalid document");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Event Services
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("EventService Unit Tests")
|
||||||
|
class EventServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private EventService eventService;
|
||||||
|
|
||||||
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE
|
||||||
|
testPayload = new BusinessRulesPayload();
|
||||||
|
testPayload.setDocumentId(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for createSuccessEvent")
|
||||||
|
class CreateSuccessEvent {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create success event with correct attributes")
|
||||||
|
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() ->
|
||||||
|
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(eventRepository).persist(argThat(event ->
|
||||||
|
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||||
|
event.getStatus() == EventStatus.SUCCESS &&
|
||||||
|
event.getPayload().equals("{\"documentId\":1}") &&
|
||||||
|
event.getTimestamp() != null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when payload is null")
|
||||||
|
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||||
|
// ARRANGE
|
||||||
|
Object nullPayload = null;
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
||||||
|
verify(eventRepository, never()).persist(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for createErrorEvent")
|
||||||
|
class CreateErrorEvent {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create error event with error message")
|
||||||
|
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
String errorMessage = "Processing failed";
|
||||||
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() ->
|
||||||
|
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(eventRepository).persist(argThat(event ->
|
||||||
|
event.getType().equals("PROCESSING_ERROR") &&
|
||||||
|
event.getStatus() == EventStatus.ERROR &&
|
||||||
|
event.getErrorMessage().equals(errorMessage) &&
|
||||||
|
event.getPayload().equals("{\"documentId\":1}")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@DisplayName("Should reject invalid error messages")
|
||||||
|
@ValueSource(strings = {"", " "})
|
||||||
|
void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
|
||||||
|
// ACT & ASSERT
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing CompletableFuture
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("FileStorageService Unit Tests")
|
||||||
|
class FileStorageServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private S3Client s3Client;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExecutorService executorService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
private InputStream testInputStream;
|
||||||
|
private LogContext testLogContext;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE
|
||||||
|
testInputStream = new ByteArrayInputStream("test content".getBytes());
|
||||||
|
testLogContext = new LogContext();
|
||||||
|
testLogContext.put("traceId", "trace-123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for uploadOriginalFile")
|
||||||
|
class UploadOriginalFile {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully upload file and return document info")
|
||||||
|
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
Callable<?> callable = invocation.getArgument(0);
|
||||||
|
return CompletableFuture.completedFuture(callable.call());
|
||||||
|
});
|
||||||
|
|
||||||
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
|
.thenReturn(PutObjectResponse.builder().build());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
|
StoredDocumentInfo result = future.join();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getPath()).isNotBlank();
|
||||||
|
assertThat(result.getSize()).isEqualTo(1024L);
|
||||||
|
assertThat(result.getUploadedAt()).isNotNull();
|
||||||
|
|
||||||
|
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle S3 upload failure")
|
||||||
|
void givenS3Failure_whenUpload_thenCompletableFutureFails() {
|
||||||
|
// ARRANGE
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
return CompletableFuture.failedFuture(new StorageException("S3 unavailable"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThatThrownBy(() -> future.join())
|
||||||
|
.isInstanceOf(CompletionException.class)
|
||||||
|
.hasCauseInstanceOf(StorageException.class)
|
||||||
|
.hasMessageContaining("S3 unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should propagate LogContext to async operation")
|
||||||
|
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
||||||
|
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
Callable<?> callable = invocation.getArgument(0);
|
||||||
|
capturedContext.set(CustomLog.getCurrentContext());
|
||||||
|
return CompletableFuture.completedFuture(callable.call());
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL).join();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThat(capturedContext.get()).isNotNull();
|
||||||
|
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource Layer Tests (REST Assured)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("DocumentResource API Tests")
|
||||||
|
class DocumentResourceTest {
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
DocumentService documentService;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for GET /api/documents")
|
||||||
|
class ListDocuments {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return list of documents")
|
||||||
|
void givenDocumentsExist_whenList_thenReturnsOk() {
|
||||||
|
// ARRANGE
|
||||||
|
List<Document> documents = List.of(createDocument(1L, "DOC-001"));
|
||||||
|
when(documentService.list(0, 20)).thenReturn(documents);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.when().get("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$.size()", is(1))
|
||||||
|
.body("[0].referenceNumber", equalTo("DOC-001"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for POST /api/documents")
|
||||||
|
class CreateDocument {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create document and return 201")
|
||||||
|
void givenValidRequest_whenCreate_thenReturns201() {
|
||||||
|
// ARRANGE
|
||||||
|
Document document = createDocument(1L, "DOC-001");
|
||||||
|
when(documentService.create(any())).thenReturn(document);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "DOC-001",
|
||||||
|
"description": "Test document",
|
||||||
|
"validUntil": "2030-01-01T00:00:00Z",
|
||||||
|
"categories": ["test"]
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.header("Location", containsString("/api/documents/1"))
|
||||||
|
.body("referenceNumber", equalTo("DOC-001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return 400 for invalid input")
|
||||||
|
void givenInvalidRequest_whenCreate_thenReturns400() {
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "",
|
||||||
|
"description": "Test"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document createDocument(Long id, String referenceNumber) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setId(id);
|
||||||
|
document.setReferenceNumber(referenceNumber);
|
||||||
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Tests with Real Database
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@TestProfile(IntegrationTestProfile.class)
|
||||||
|
@DisplayName("Document Integration Tests")
|
||||||
|
class DocumentIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
@DisplayName("Should create and retrieve document via API")
|
||||||
|
void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
|
||||||
|
// ACT - Create via API
|
||||||
|
Long id = given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "INT-001",
|
||||||
|
"description": "Integration test",
|
||||||
|
"validUntil": "2030-01-01T00:00:00Z",
|
||||||
|
"categories": ["test"]
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.extract().path("id");
|
||||||
|
|
||||||
|
// ASSERT - Retrieve via API
|
||||||
|
given()
|
||||||
|
.when().get("/api/documents/" + id)
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("referenceNumber", equalTo("INT-001"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage with JaCoCo
|
||||||
|
|
||||||
|
### Maven Configuration (Complete)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.13</version>
|
||||||
|
<executions>
|
||||||
|
<!-- Prepare agent for test execution -->
|
||||||
|
<execution>
|
||||||
|
<id>prepare-agent</id>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<!-- Generate coverage report -->
|
||||||
|
<execution>
|
||||||
|
<id>report</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<!-- Enforce coverage thresholds -->
|
||||||
|
<execution>
|
||||||
|
<id>check</id>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.80</minimum>
|
||||||
|
</limit>
|
||||||
|
<limit>
|
||||||
|
<counter>BRANCH</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.70</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests with coverage:
|
||||||
|
```bash
|
||||||
|
mvn clean test
|
||||||
|
mvn jacoco:report
|
||||||
|
mvn jacoco:check
|
||||||
|
|
||||||
|
# Report at: target/site/jacoco/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Dependencies
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependencies>
|
||||||
|
<!-- Quarkus Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Mockito -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- AssertJ (preferred over JUnit assertions) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.24.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- REST Assured -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.rest-assured</groupId>
|
||||||
|
<artifactId>rest-assured</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Camel Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
- Use `@Nested` classes to group tests by method being tested
|
||||||
|
- Use `@DisplayName` for readable test descriptions visible in reports
|
||||||
|
- Follow `givenX_whenY_thenZ` naming convention for test methods
|
||||||
|
- Use `@BeforeEach` for common test data setup to reduce duplication
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
- Follow AAA pattern with explicit comments (`// ARRANGE`, `// ACT`, `// ASSERT`)
|
||||||
|
- Use `assertDoesNotThrow` for success scenarios
|
||||||
|
- Use `assertThrows` for exception scenarios with message validation
|
||||||
|
- Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()`
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
- Test happy paths for all public methods
|
||||||
|
- Test null input handling
|
||||||
|
- Test edge cases (empty collections, boundary values, negative IDs, blank strings)
|
||||||
|
- Test exception scenarios comprehensively
|
||||||
|
- Mock all external dependencies (repositories, services, Camel endpoints)
|
||||||
|
- Aim for 80%+ line coverage, 70%+ branch coverage
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
- **Always use AssertJ** (`assertThat`) instead of JUnit assertions
|
||||||
|
- Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)`
|
||||||
|
- For exceptions: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
|
||||||
|
- For collections: `extracting()`, `filteredOn()`, `containsExactly()`
|
||||||
|
|
||||||
|
### Testing Integration
|
||||||
|
- Use `@QuarkusTest` for integration tests
|
||||||
|
- Use `@InjectMock` to mock dependencies in Quarkus tests
|
||||||
|
- Prefer REST Assured for API testing
|
||||||
|
- Use `@TestProfile` for test-specific configuration
|
||||||
|
|
||||||
|
### Event-Driven Testing
|
||||||
|
- Test Camel routes with `AdviceWith` and `MockEndpoint`
|
||||||
|
- Use `@CamelQuarkusTest` annotation (if using standalone Camel tests)
|
||||||
|
- Verify message content, headers, and routing logic
|
||||||
|
- Test error handling routes separately
|
||||||
|
- Mock external systems (RabbitMQ, S3, databases) in unit tests
|
||||||
|
|
||||||
|
### Camel Route Testing
|
||||||
|
- Use `MockEndpoint` for asserting message flow
|
||||||
|
- Use `AdviceWith` to modify routes for testing (replace endpoints with mocks)
|
||||||
|
- Test message transformation and marshalling
|
||||||
|
- Test exception handling and dead letter queues
|
||||||
|
|
||||||
|
### Testing Async Operations
|
||||||
|
- Test CompletableFuture success and failure scenarios
|
||||||
|
- Use `.join()` in tests to wait for async completion
|
||||||
|
- Test exception propagation from CompletableFuture
|
||||||
|
- Verify LogContext propagation to async operations
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Keep tests fast and isolated
|
||||||
|
- Run tests in continuous mode: `mvn quarkus:test`
|
||||||
|
- Use parameterized tests (`@ParameterizedTest`) for input variations
|
||||||
|
- Build reusable test data builders or factory methods
|
||||||
|
|
||||||
|
### Quarkus-Specific
|
||||||
|
- Stay on latest LTS version (Quarkus 3.x)
|
||||||
|
- Test native compilation compatibility periodically
|
||||||
|
- Use Quarkus test profiles for different scenarios
|
||||||
|
- Leverage Quarkus dev services for local testing
|
||||||
|
- Use `@InjectMock` instead of `@MockBean` (Quarkus-specific)
|
||||||
|
|
||||||
|
### Verification Best Practices
|
||||||
|
- Always verify interactions on mocked dependencies
|
||||||
|
- Use `verify(mock, never())` to ensure methods are NOT called in error scenarios
|
||||||
|
- Use `argThat()` for complex argument matching
|
||||||
|
- Verify the order of calls when it matters: `InOrder` from Mockito
|
||||||
481
docs/zh-CN/skills/quarkus-verification/SKILL.md
Normal file
481
docs/zh-CN/skills/quarkus-verification/SKILL.md
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-verification
|
||||||
|
description: "Verification loop for Quarkus projects: build, static analysis, tests with coverage, security scans, native compilation, and diff review before release or PR."
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Verification Loop
|
||||||
|
|
||||||
|
Run before PRs, after major changes, and pre-deploy.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Before opening a pull request for a Quarkus service
|
||||||
|
- After major refactoring or dependency upgrades
|
||||||
|
- Pre-deployment verification for staging or production
|
||||||
|
- Running full build → lint → test → security scan → native compilation pipeline
|
||||||
|
- Validating test coverage meets thresholds (80%+)
|
||||||
|
- Testing native image compatibility
|
||||||
|
|
||||||
|
## Phase 1: Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Maven
|
||||||
|
mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./gradlew clean assemble -x test
|
||||||
|
```
|
||||||
|
|
||||||
|
If build fails, stop and fix compilation errors.
|
||||||
|
|
||||||
|
## Phase 2: Static Analysis
|
||||||
|
|
||||||
|
### Checkstyle, PMD, SpotBugs (Maven)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn checkstyle:check pmd:check spotbugs:check
|
||||||
|
```
|
||||||
|
|
||||||
|
### SonarQube (if configured)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn sonar:sonar \
|
||||||
|
-Dsonar.projectKey=my-quarkus-project \
|
||||||
|
-Dsonar.host.url=http://localhost:9000 \
|
||||||
|
-Dsonar.login=${SONAR_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues to Address
|
||||||
|
|
||||||
|
- Unused imports or variables
|
||||||
|
- Complex methods (high cyclomatic complexity)
|
||||||
|
- Potential null pointer dereferences
|
||||||
|
- Security issues flagged by SpotBugs
|
||||||
|
|
||||||
|
## Phase 3: Tests + Coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
mvn clean test
|
||||||
|
|
||||||
|
# Generate coverage report
|
||||||
|
mvn jacoco:report
|
||||||
|
|
||||||
|
# Enforce coverage threshold (80%)
|
||||||
|
mvn jacoco:check
|
||||||
|
|
||||||
|
# Or with Gradle
|
||||||
|
./gradlew test jacocoTestReport jacocoTestCoverageVerification
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Categories
|
||||||
|
|
||||||
|
#### Unit Tests
|
||||||
|
Test service logic with mocked dependencies:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserServiceTest {
|
||||||
|
@Mock UserRepository userRepository;
|
||||||
|
@InjectMocks UserService userService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_validInput_returnsUser() {
|
||||||
|
var dto = new CreateUserDto("Alice", "alice@example.com");
|
||||||
|
var expected = new User();
|
||||||
|
expected.id = 1L;
|
||||||
|
expected.name = dto.name();
|
||||||
|
|
||||||
|
when(userRepository.persist(any(User.class))).thenReturn(expected);
|
||||||
|
|
||||||
|
User result = userService.create(dto);
|
||||||
|
|
||||||
|
assertThat(result.name).isEqualTo("Alice");
|
||||||
|
verify(userRepository).persist(any(User.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Integration Tests
|
||||||
|
Test with real database (Testcontainers):
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@QuarkusTestResource(PostgresTestResource.class)
|
||||||
|
class UserRepositoryIntegrationTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UserRepository userRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void findByEmail_existingUser_returnsUser() {
|
||||||
|
User user = new User();
|
||||||
|
user.name = "Alice";
|
||||||
|
user.email = "alice@example.com";
|
||||||
|
userRepository.persist(user);
|
||||||
|
|
||||||
|
Optional<User> found = userRepository.findByEmail("alice@example.com");
|
||||||
|
|
||||||
|
assertThat(found).isPresent();
|
||||||
|
assertThat(found.get().name).isEqualTo("Alice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Tests
|
||||||
|
Test REST endpoints with REST Assured:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
class UserResourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_validInput_returns201() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{"name": "Alice", "email": "alice@example.com"}
|
||||||
|
""")
|
||||||
|
.when().post("/api/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.body("name", equalTo("Alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_invalidEmail_returns400() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{"name": "Alice", "email": "invalid"}
|
||||||
|
""")
|
||||||
|
.when().post("/api/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage Report
|
||||||
|
|
||||||
|
Check `target/site/jacoco/index.html` for detailed coverage:
|
||||||
|
- Overall line coverage (target: 80%+)
|
||||||
|
- Branch coverage (target: 70%+)
|
||||||
|
- Identify uncovered critical paths
|
||||||
|
|
||||||
|
## Phase 4: Security Scanning
|
||||||
|
|
||||||
|
### Dependency Vulnerabilities (Maven)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
```
|
||||||
|
|
||||||
|
Review `target/dependency-check-report.html` for CVEs.
|
||||||
|
|
||||||
|
### Quarkus Security Audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check vulnerable extensions
|
||||||
|
mvn quarkus:audit
|
||||||
|
|
||||||
|
# List all extensions
|
||||||
|
mvn quarkus:list-extensions
|
||||||
|
```
|
||||||
|
|
||||||
|
### OWASP ZAP (API Security Testing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -t owasp/zap2docker-stable zap-api-scan.py \
|
||||||
|
-t http://localhost:8080/q/openapi \
|
||||||
|
-f openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Security Checks
|
||||||
|
|
||||||
|
- [ ] All secrets in environment variables (not in code)
|
||||||
|
- [ ] Input validation on all endpoints
|
||||||
|
- [ ] Authentication/authorization configured
|
||||||
|
- [ ] CORS properly configured
|
||||||
|
- [ ] Security headers set
|
||||||
|
- [ ] Passwords hashed with BCrypt
|
||||||
|
- [ ] SQL injection protection (parameterized queries)
|
||||||
|
- [ ] Rate limiting on public endpoints
|
||||||
|
|
||||||
|
## Phase 5: Native Compilation
|
||||||
|
|
||||||
|
Test GraalVM native image compatibility:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build native executable
|
||||||
|
mvn package -Dnative
|
||||||
|
|
||||||
|
# Or with container
|
||||||
|
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||||
|
|
||||||
|
# Test native executable
|
||||||
|
./target/*-runner
|
||||||
|
|
||||||
|
# Run basic smoke tests
|
||||||
|
curl http://localhost:8080/q/health/live
|
||||||
|
curl http://localhost:8080/q/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native Image Troubleshooting
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
- **Reflection**: Add reflection config for dynamic classes
|
||||||
|
- **Resources**: Include resources with `quarkus.native.resources.includes`
|
||||||
|
- **JNI**: Register JNI classes if using native libraries
|
||||||
|
|
||||||
|
Example reflection config:
|
||||||
|
```java
|
||||||
|
@RegisterForReflection(targets = {MyDynamicClass.class})
|
||||||
|
public class ReflectionConfiguration {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 6: Performance Testing
|
||||||
|
|
||||||
|
### Load Testing with K6
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// load-test.js
|
||||||
|
import http from 'k6/http';
|
||||||
|
import { check } from 'k6';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: '30s', target: 50 },
|
||||||
|
{ duration: '1m', target: 100 },
|
||||||
|
{ duration: '30s', target: 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const res = http.get('http://localhost:8080/api/markets');
|
||||||
|
check(res, {
|
||||||
|
'status is 200': (r) => r.status === 200,
|
||||||
|
'response time < 200ms': (r) => r.timings.duration < 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
k6 run load-test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics to Monitor
|
||||||
|
|
||||||
|
- Response time (p50, p95, p99)
|
||||||
|
- Throughput (requests/sec)
|
||||||
|
- Error rate
|
||||||
|
- Memory usage
|
||||||
|
- CPU usage
|
||||||
|
|
||||||
|
## Phase 7: Health Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Liveness
|
||||||
|
curl http://localhost:8080/q/health/live
|
||||||
|
|
||||||
|
# Readiness
|
||||||
|
curl http://localhost:8080/q/health/ready
|
||||||
|
|
||||||
|
# All health checks
|
||||||
|
curl http://localhost:8080/q/health
|
||||||
|
|
||||||
|
# Metrics (if enabled)
|
||||||
|
curl http://localhost:8080/q/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected responses:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "UP",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"name": "Database connection",
|
||||||
|
"status": "UP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 8: Container Image Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build container image
|
||||||
|
mvn package -Dquarkus.container-image.build=true
|
||||||
|
|
||||||
|
# Or with specific registry
|
||||||
|
mvn package \
|
||||||
|
-Dquarkus.container-image.build=true \
|
||||||
|
-Dquarkus.container-image.registry=docker.io \
|
||||||
|
-Dquarkus.container-image.group=myorg \
|
||||||
|
-Dquarkus.container-image.tag=1.0.0
|
||||||
|
|
||||||
|
# Test container
|
||||||
|
docker run -p 8080:8080 myorg/my-quarkus-app:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Security Scan
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trivy
|
||||||
|
trivy image myorg/my-quarkus-app:1.0.0
|
||||||
|
|
||||||
|
# Grype
|
||||||
|
grype myorg/my-quarkus-app:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 9: Configuration Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all configuration properties
|
||||||
|
mvn quarkus:info
|
||||||
|
|
||||||
|
# List all config sources
|
||||||
|
curl http://localhost:8080/q/dev/io.quarkus.quarkus-vertx-http/config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment-Specific Checks
|
||||||
|
|
||||||
|
- [ ] Database URLs configured per environment
|
||||||
|
- [ ] Secrets externalized (Vault, env vars)
|
||||||
|
- [ ] Logging levels appropriate
|
||||||
|
- [ ] CORS origins set correctly
|
||||||
|
- [ ] Rate limiting configured
|
||||||
|
- [ ] Monitoring/tracing enabled
|
||||||
|
|
||||||
|
## Phase 10: Documentation Review
|
||||||
|
|
||||||
|
- [ ] OpenAPI/Swagger docs up to date (`/q/swagger-ui`)
|
||||||
|
- [ ] README has setup instructions
|
||||||
|
- [ ] API changes documented
|
||||||
|
- [ ] Migration guide for breaking changes
|
||||||
|
- [ ] Configuration properties documented
|
||||||
|
|
||||||
|
Generate OpenAPI spec:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/q/openapi -o openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- [ ] Build passes without warnings
|
||||||
|
- [ ] Static analysis clean (no high/medium issues)
|
||||||
|
- [ ] Code follows team conventions
|
||||||
|
- [ ] No commented-out code or TODOs in PR
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Code coverage ≥ 80%
|
||||||
|
- [ ] Integration tests with real database
|
||||||
|
- [ ] Security tests pass
|
||||||
|
- [ ] Performance within acceptable limits
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- [ ] No dependency vulnerabilities
|
||||||
|
- [ ] Authentication/authorization tested
|
||||||
|
- [ ] Input validation complete
|
||||||
|
- [ ] Secrets not in source code
|
||||||
|
- [ ] Security headers configured
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
- [ ] Native compilation successful
|
||||||
|
- [ ] Container image builds
|
||||||
|
- [ ] Health checks respond correctly
|
||||||
|
- [ ] Configuration valid for target environment
|
||||||
|
|
||||||
|
### Native Image
|
||||||
|
- [ ] Native executable builds
|
||||||
|
- [ ] Native tests pass
|
||||||
|
- [ ] Startup time < 100ms
|
||||||
|
- [ ] Memory footprint acceptable
|
||||||
|
|
||||||
|
## Automated Verification Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Phase 1: Build ==="
|
||||||
|
mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
echo "=== Phase 2: Static Analysis ==="
|
||||||
|
mvn checkstyle:check pmd:check spotbugs:check
|
||||||
|
|
||||||
|
echo "=== Phase 3: Tests + Coverage ==="
|
||||||
|
mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
|
echo "=== Phase 4: Security Scan ==="
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
echo "=== Phase 5: Native Compilation ==="
|
||||||
|
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||||
|
|
||||||
|
echo "=== All Phases Complete ==="
|
||||||
|
echo "Review reports:"
|
||||||
|
echo " - Coverage: target/site/jacoco/index.html"
|
||||||
|
echo " - Security: target/dependency-check-report.html"
|
||||||
|
echo " - Native: target/*-runner"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Verification
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '21'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Cache Maven packages
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.m2
|
||||||
|
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
- name: Test with Coverage
|
||||||
|
run: mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
|
- name: Security Scan
|
||||||
|
run: mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
- name: Upload Coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: target/site/jacoco/jacoco.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Run verification loop before every PR
|
||||||
|
- Automate in CI/CD pipeline
|
||||||
|
- Fix issues immediately; don't accumulate debt
|
||||||
|
- Keep coverage above 80%
|
||||||
|
- Update dependencies regularly
|
||||||
|
- Test native compilation periodically
|
||||||
|
- Monitor performance trends
|
||||||
|
- Document breaking changes
|
||||||
|
- Review security scan results
|
||||||
|
- Validate configuration for each environment
|
||||||
@ -147,6 +147,9 @@
|
|||||||
"skills/python-testing",
|
"skills/python-testing",
|
||||||
"skills/rust-patterns",
|
"skills/rust-patterns",
|
||||||
"skills/rust-testing",
|
"skills/rust-testing",
|
||||||
|
"skills/quarkus-patterns",
|
||||||
|
"skills/quarkus-tdd",
|
||||||
|
"skills/quarkus-verification",
|
||||||
"skills/springboot-patterns",
|
"skills/springboot-patterns",
|
||||||
"skills/springboot-tdd",
|
"skills/springboot-tdd",
|
||||||
"skills/springboot-verification"
|
"skills/springboot-verification"
|
||||||
@ -248,6 +251,7 @@
|
|||||||
"skills/security-review",
|
"skills/security-review",
|
||||||
"skills/security-scan",
|
"skills/security-scan",
|
||||||
"skills/security-bounty-hunter",
|
"skills/security-bounty-hunter",
|
||||||
|
"skills/quarkus-security",
|
||||||
"skills/springboot-security",
|
"skills/springboot-security",
|
||||||
"skills/evm-token-decimals",
|
"skills/evm-token-decimals",
|
||||||
"the-security-guide.md"
|
"the-security-guide.md"
|
||||||
|
|||||||
@ -143,4 +143,5 @@ public record ApiResponse<T>(boolean success, T data, String error) {
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
See skill: `springboot-patterns` for Spring Boot architecture patterns.
|
See skill: `springboot-patterns` for Spring Boot architecture patterns.
|
||||||
|
See skill: `quarkus-patterns` for Quarkus architecture patterns with Camel and Panache.
|
||||||
See skill: `jpa-patterns` for entity design and query optimization.
|
See skill: `jpa-patterns` for entity design and query optimization.
|
||||||
|
|||||||
@ -97,4 +97,5 @@ try {
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
See skill: `springboot-security` for Spring Security authentication and authorization patterns.
|
See skill: `springboot-security` for Spring Security authentication and authorization patterns.
|
||||||
|
See skill: `quarkus-security` for Quarkus security with JWT/OIDC, RBAC, and CDI.
|
||||||
See skill: `security-review` for general security checklists.
|
See skill: `security-review` for general security checklists.
|
||||||
|
|||||||
@ -112,6 +112,7 @@ class OrderRepositoryIT {
|
|||||||
```
|
```
|
||||||
|
|
||||||
For Spring Boot integration tests, see skill: `springboot-tdd`.
|
For Spring Boot integration tests, see skill: `springboot-tdd`.
|
||||||
|
For Quarkus integration tests, see skill: `quarkus-tdd`.
|
||||||
|
|
||||||
## Test Naming
|
## Test Naming
|
||||||
|
|
||||||
@ -128,4 +129,5 @@ Use descriptive names with `@DisplayName`:
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
See skill: `springboot-tdd` for Spring Boot TDD patterns with MockMvc and Testcontainers.
|
See skill: `springboot-tdd` for Spring Boot TDD patterns with MockMvc and Testcontainers.
|
||||||
|
See skill: `quarkus-tdd` for Quarkus TDD patterns with REST Assured and Camel testing.
|
||||||
See skill: `java-coding-standards` for testing expectations.
|
See skill: `java-coding-standards` for testing expectations.
|
||||||
|
|||||||
@ -87,7 +87,7 @@ There are 7 selectable category groups below. The detailed confirmation lists th
|
|||||||
```
|
```
|
||||||
Question: "Which skill categories do you want to install?"
|
Question: "Which skill categories do you want to install?"
|
||||||
Options:
|
Options:
|
||||||
- "Framework & Language" — "Django, Laravel, Spring Boot, Go, Python, Java, Frontend, Backend patterns"
|
- "Framework & Language" — "Django, Laravel, Spring Boot, Quarkus, Go, Python, Java, Frontend, Backend patterns"
|
||||||
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns"
|
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns"
|
||||||
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
|
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
|
||||||
- "Research & APIs" — "Deep research, Exa search, Claude API patterns"
|
- "Research & APIs" — "Deep research, Exa search, Claude API patterns"
|
||||||
@ -101,7 +101,7 @@ Options:
|
|||||||
|
|
||||||
For each selected category, print the full list of skills below and ask the user to confirm or deselect specific ones. If the list exceeds 4 items, print the list as text and use `AskUserQuestion` with an "Install all listed" option plus "Other" for the user to paste specific names.
|
For each selected category, print the full list of skills below and ask the user to confirm or deselect specific ones. If the list exceeds 4 items, print the list as text and use `AskUserQuestion` with an "Install all listed" option plus "Other" for the user to paste specific names.
|
||||||
|
|
||||||
**Category: Framework & Language (21 skills)**
|
**Category: Framework & Language (25 skills)**
|
||||||
|
|
||||||
| Skill | Description |
|
| Skill | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
@ -122,6 +122,10 @@ For each selected category, print the full list of skills below and ask the user
|
|||||||
| `java-coding-standards` | Java coding standards for Spring Boot: naming, immutability, Optional, streams |
|
| `java-coding-standards` | Java coding standards for Spring Boot: naming, immutability, Optional, streams |
|
||||||
| `python-patterns` | Pythonic idioms, PEP 8, type hints, best practices |
|
| `python-patterns` | Pythonic idioms, PEP 8, type hints, best practices |
|
||||||
| `python-testing` | Python testing with pytest, TDD, fixtures, mocking, parametrization |
|
| `python-testing` | Python testing with pytest, TDD, fixtures, mocking, parametrization |
|
||||||
|
| `quarkus-patterns` | Quarkus architecture, Camel messaging, CDI services, Panache data access |
|
||||||
|
| `quarkus-security` | Quarkus security: JWT/OIDC, RBAC, input validation, secrets management |
|
||||||
|
| `quarkus-tdd` | Quarkus TDD with JUnit 5, Mockito, REST Assured, Camel testing |
|
||||||
|
| `quarkus-verification` | Quarkus verification: build, static analysis, tests, native compilation |
|
||||||
| `springboot-patterns` | Spring Boot architecture, REST API, layered services, caching, async |
|
| `springboot-patterns` | Spring Boot architecture, REST API, layered services, caching, async |
|
||||||
| `springboot-security` | Spring Security: authn/authz, validation, CSRF, secrets, rate limiting |
|
| `springboot-security` | Spring Security: authn/authz, validation, CSRF, secrets, rate limiting |
|
||||||
| `springboot-tdd` | Spring Boot TDD with JUnit 5, Mockito, MockMvc, Testcontainers |
|
| `springboot-tdd` | Spring Boot TDD with JUnit 5, Mockito, MockMvc, Testcontainers |
|
||||||
@ -263,6 +267,7 @@ grep -rn "skills/" $TARGET/skills/
|
|||||||
Some skills reference others. Verify these dependencies:
|
Some skills reference others. Verify these dependencies:
|
||||||
- `django-tdd` may reference `django-patterns`
|
- `django-tdd` may reference `django-patterns`
|
||||||
- `laravel-tdd` may reference `laravel-patterns`
|
- `laravel-tdd` may reference `laravel-patterns`
|
||||||
|
- `quarkus-tdd` may reference `quarkus-patterns`
|
||||||
- `springboot-tdd` may reference `springboot-patterns`
|
- `springboot-tdd` may reference `springboot-patterns`
|
||||||
- `continuous-learning-v2` references `~/.claude/homunculus/` directory
|
- `continuous-learning-v2` references `~/.claude/homunculus/` directory
|
||||||
- `python-testing` may reference `python-patterns`
|
- `python-testing` may reference `python-patterns`
|
||||||
|
|||||||
@ -1,20 +1,29 @@
|
|||||||
---
|
---
|
||||||
name: java-coding-standards
|
name: java-coding-standards
|
||||||
description: "Java coding standards for Spring Boot services: naming, immutability, Optional usage, streams, exceptions, generics, and project layout."
|
description: "Java coding standards for Spring Boot and Quarkus services: naming, immutability, Optional usage, streams, exceptions, generics, CDI, reactive patterns, and project layout. Automatically applies framework-specific conventions."
|
||||||
origin: ECC
|
origin: ECC
|
||||||
---
|
---
|
||||||
|
|
||||||
# Java Coding Standards
|
# Java Coding Standards
|
||||||
|
|
||||||
Standards for readable, maintainable Java (17+) code in Spring Boot services.
|
Standards for readable, maintainable Java (17+) code in Spring Boot and Quarkus services.
|
||||||
|
|
||||||
|
## Framework Detection
|
||||||
|
|
||||||
|
Before applying standards, determine the framework from the build file:
|
||||||
|
|
||||||
|
- Build file contains `quarkus` → apply **[QUARKUS]** conventions
|
||||||
|
- Build file contains `spring-boot` → apply **[SPRING]** conventions
|
||||||
|
- Neither detected → apply shared conventions only
|
||||||
|
|
||||||
## When to Activate
|
## When to Activate
|
||||||
|
|
||||||
- Writing or reviewing Java code in Spring Boot projects
|
- Writing or reviewing Java code in Spring Boot or Quarkus projects
|
||||||
- Enforcing naming, immutability, or exception handling conventions
|
- Enforcing naming, immutability, or exception handling conventions
|
||||||
- Working with records, sealed classes, or pattern matching (Java 17+)
|
- Working with records, sealed classes, or pattern matching (Java 17+)
|
||||||
- Reviewing use of Optional, streams, or generics
|
- Reviewing use of Optional, streams, or generics
|
||||||
- Structuring packages and project layout
|
- Structuring packages and project layout
|
||||||
|
- **[QUARKUS]**: Working with CDI scopes, Panache entities, or reactive pipelines
|
||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
|
|
||||||
@ -22,6 +31,7 @@ Standards for readable, maintainable Java (17+) code in Spring Boot services.
|
|||||||
- Immutable by default; minimize shared mutable state
|
- Immutable by default; minimize shared mutable state
|
||||||
- Fail fast with meaningful exceptions
|
- Fail fast with meaningful exceptions
|
||||||
- Consistent naming and package structure
|
- Consistent naming and package structure
|
||||||
|
- **[QUARKUS]**: Favor build-time over runtime processing; avoid runtime reflection where possible
|
||||||
|
|
||||||
## Naming
|
## Naming
|
||||||
|
|
||||||
@ -36,6 +46,12 @@ public Market findBySlug(String slug) {}
|
|||||||
|
|
||||||
// PASS: Constants: UPPER_SNAKE_CASE
|
// PASS: Constants: UPPER_SNAKE_CASE
|
||||||
private static final int MAX_PAGE_SIZE = 100;
|
private static final int MAX_PAGE_SIZE = 100;
|
||||||
|
|
||||||
|
// PASS: [QUARKUS] JAX-RS resources named as *Resource, not *Controller
|
||||||
|
public class MarketResource {}
|
||||||
|
|
||||||
|
// PASS: [SPRING] REST controllers named as *Controller
|
||||||
|
public class MarketController {}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Immutability
|
## Immutability
|
||||||
@ -49,14 +65,33 @@ public class Market {
|
|||||||
private final String name;
|
private final String name;
|
||||||
// getters only, no setters
|
// getters only, no setters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PASS: [QUARKUS] Panache active-record entities use public fields (Quarkus convention)
|
||||||
|
@Entity
|
||||||
|
public class Market extends PanacheEntity {
|
||||||
|
public String name;
|
||||||
|
public MarketStatus status;
|
||||||
|
// Panache generates accessors at build time; public fields are idiomatic here
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASS: [QUARKUS] Panache MongoDB entities
|
||||||
|
@MongoEntity(collection = "markets")
|
||||||
|
public class Market extends PanacheMongoEntity {
|
||||||
|
public String name;
|
||||||
|
public MarketStatus status;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Optional Usage
|
## Optional Usage
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// PASS: Return Optional from find* methods
|
// PASS: Return Optional from find* methods
|
||||||
|
// [SPRING]
|
||||||
Optional<Market> market = marketRepository.findBySlug(slug);
|
Optional<Market> market = marketRepository.findBySlug(slug);
|
||||||
|
|
||||||
|
// [QUARKUS] Panache
|
||||||
|
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
|
||||||
|
|
||||||
// PASS: Map/flatMap instead of get()
|
// PASS: Map/flatMap instead of get()
|
||||||
return market
|
return market
|
||||||
.map(MarketResponse::from)
|
.map(MarketResponse::from)
|
||||||
@ -75,6 +110,77 @@ List<String> names = markets.stream()
|
|||||||
// FAIL: Avoid complex nested streams; prefer loops for clarity
|
// FAIL: Avoid complex nested streams; prefer loops for clarity
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
```java
|
||||||
|
// PASS: [SPRING] Constructor injection (preferred over @Autowired on fields)
|
||||||
|
@Service
|
||||||
|
public class MarketService {
|
||||||
|
private final MarketRepository marketRepository;
|
||||||
|
|
||||||
|
public MarketService(MarketRepository marketRepository) {
|
||||||
|
this.marketRepository = marketRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASS: [QUARKUS] Constructor injection
|
||||||
|
@ApplicationScoped
|
||||||
|
public class MarketService {
|
||||||
|
private final MarketRepository marketRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MarketService(MarketRepository marketRepository) {
|
||||||
|
this.marketRepository = marketRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASS: [QUARKUS] Package-private field injection (acceptable in Quarkus — avoids proxy issues)
|
||||||
|
@ApplicationScoped
|
||||||
|
public class MarketService {
|
||||||
|
@Inject
|
||||||
|
MarketRepository marketRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAIL: [SPRING] Field injection with @Autowired
|
||||||
|
@Autowired
|
||||||
|
private MarketRepository marketRepository; // use constructor injection
|
||||||
|
|
||||||
|
// FAIL: [QUARKUS] @Singleton when interception or lazy init is needed
|
||||||
|
@Singleton // non-proxyable — use @ApplicationScoped instead
|
||||||
|
public class MarketService {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reactive Patterns [QUARKUS]
|
||||||
|
|
||||||
|
```java
|
||||||
|
// PASS: Return Uni/Multi from reactive endpoints
|
||||||
|
@GET
|
||||||
|
@Path("/{slug}")
|
||||||
|
public Uni<Market> findBySlug(@PathParam("slug") String slug) {
|
||||||
|
return Market.find("slug", slug)
|
||||||
|
.<Market>firstResult()
|
||||||
|
.onItem().ifNull().failWith(() -> new MarketNotFoundException(slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASS: Non-blocking pipeline composition
|
||||||
|
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
|
||||||
|
return validateOrder(req)
|
||||||
|
.chain(valid -> persistOrder(valid))
|
||||||
|
.chain(order -> notifyFulfillment(order));
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAIL: Blocking call inside a Uni/Multi pipeline
|
||||||
|
public Uni<Market> find(String slug) {
|
||||||
|
Market m = Market.find("slug", slug).firstResult(); // BLOCKING — breaks event loop
|
||||||
|
return Uni.createFrom().item(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAIL: Subscribing more than once to a shared Uni
|
||||||
|
Uni<Market> shared = fetchMarket(slug);
|
||||||
|
shared.subscribe().with(m -> log(m));
|
||||||
|
shared.subscribe().with(m -> cache(m)); // double subscribe — use Uni.memoize()
|
||||||
|
```
|
||||||
|
|
||||||
## Exceptions
|
## Exceptions
|
||||||
|
|
||||||
- Use unchecked exceptions for domain errors; wrap technical exceptions with context
|
- Use unchecked exceptions for domain errors; wrap technical exceptions with context
|
||||||
@ -85,6 +191,34 @@ List<String> names = markets.stream()
|
|||||||
throw new MarketNotFoundException(slug);
|
throw new MarketNotFoundException(slug);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Centralised Exception Handling
|
||||||
|
|
||||||
|
```java
|
||||||
|
// [SPRING]
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
@ExceptionHandler(MarketNotFoundException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handle(MarketNotFoundException ex) {
|
||||||
|
return ResponseEntity.status(404).body(ErrorResponse.from(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [QUARKUS] Option A: ExceptionMapper
|
||||||
|
@Provider
|
||||||
|
public class MarketNotFoundMapper implements ExceptionMapper<MarketNotFoundException> {
|
||||||
|
@Override
|
||||||
|
public Response toResponse(MarketNotFoundException ex) {
|
||||||
|
return Response.status(404).entity(ErrorResponse.from(ex)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [QUARKUS] Option B: @ServerExceptionMapper (RESTEasy Reactive)
|
||||||
|
@ServerExceptionMapper
|
||||||
|
public RestResponse<ErrorResponse> handle(MarketNotFoundException ex) {
|
||||||
|
return RestResponse.status(Status.NOT_FOUND, ErrorResponse.from(ex));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Generics and Type Safety
|
## Generics and Type Safety
|
||||||
|
|
||||||
- Avoid raw types; declare generic parameters
|
- Avoid raw types; declare generic parameters
|
||||||
@ -94,7 +228,9 @@ throw new MarketNotFoundException(slug);
|
|||||||
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
|
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure (Maven/Gradle)
|
## Project Structure
|
||||||
|
|
||||||
|
### [SPRING] Maven/Gradle
|
||||||
|
|
||||||
```
|
```
|
||||||
src/main/java/com/example/app/
|
src/main/java/com/example/app/
|
||||||
@ -110,6 +246,24 @@ src/main/resources/
|
|||||||
src/test/java/... (mirrors main)
|
src/test/java/... (mirrors main)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### [QUARKUS] Maven/Gradle
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/java/com/example/app/
|
||||||
|
config/ # @ConfigMapping, @ConfigProperty beans, Producers
|
||||||
|
resource/ # JAX-RS resources (not "controller")
|
||||||
|
service/
|
||||||
|
repository/ # PanacheRepository implementations (if not using active record)
|
||||||
|
domain/ # JPA/Panache entities, MongoDB entities
|
||||||
|
dto/
|
||||||
|
util/
|
||||||
|
mapper/ # MapStruct mappers (if used)
|
||||||
|
src/main/resources/
|
||||||
|
application.properties # Quarkus convention (YAML supported with quarkus-config-yaml)
|
||||||
|
import.sql # Hibernate auto-import for dev/test
|
||||||
|
src/test/java/... (mirrors main)
|
||||||
|
```
|
||||||
|
|
||||||
## Formatting and Style
|
## Formatting and Style
|
||||||
|
|
||||||
- Use 2 or 4 spaces consistently (project standard)
|
- Use 2 or 4 spaces consistently (project standard)
|
||||||
@ -124,24 +278,98 @@ src/test/java/... (mirrors main)
|
|||||||
- Magic numbers → named constants
|
- Magic numbers → named constants
|
||||||
- Static mutable state → prefer dependency injection
|
- Static mutable state → prefer dependency injection
|
||||||
- Silent catch blocks → log and act or rethrow
|
- Silent catch blocks → log and act or rethrow
|
||||||
|
- **[QUARKUS]**: `@Singleton` where `@ApplicationScoped` is intended — breaks proxying and interception
|
||||||
|
- **[QUARKUS]**: Mixing `quarkus-resteasy-reactive` and `quarkus-resteasy` (classic) — pick one stack
|
||||||
|
- **[QUARKUS]**: Panache active-record + repository pattern in the same bounded context — pick one
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
// [SPRING] SLF4J
|
||||||
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
|
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
|
||||||
log.info("fetch_market slug={}", slug);
|
log.info("fetch_market slug={}", slug);
|
||||||
log.error("failed_fetch_market slug={}", slug, ex);
|
log.error("failed_fetch_market slug={}", slug, ex);
|
||||||
|
|
||||||
|
// [QUARKUS] JBoss Logging (default, zero-cost at build time)
|
||||||
|
private static final Logger log = Logger.getLogger(MarketService.class);
|
||||||
|
log.infof("fetch_market slug=%s", slug);
|
||||||
|
log.errorf(ex, "failed_fetch_market slug=%s", slug);
|
||||||
|
|
||||||
|
// [QUARKUS] Alternative: simplified logging with @Inject
|
||||||
|
@Inject
|
||||||
|
Logger log; // CDI-injected, scoped to declaring class
|
||||||
```
|
```
|
||||||
|
|
||||||
## Null Handling
|
## Null Handling
|
||||||
|
|
||||||
- Accept `@Nullable` only when unavoidable; otherwise use `@NonNull`
|
- Accept `@Nullable` only when unavoidable; otherwise use `@NonNull`
|
||||||
- Use Bean Validation (`@NotNull`, `@NotBlank`) on inputs
|
- Use Bean Validation (`@NotNull`, `@NotBlank`) on inputs
|
||||||
|
- **[QUARKUS]**: Apply `@Valid` on `@BeanParam`, `@RestForm`, and request body parameters
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```java
|
||||||
|
// [SPRING] @ConfigurationProperties
|
||||||
|
@ConfigurationProperties(prefix = "market")
|
||||||
|
public record MarketProperties(int maxPageSize, Duration cacheTtl) {}
|
||||||
|
|
||||||
|
// [QUARKUS] @ConfigMapping (type-safe, build-time validated)
|
||||||
|
@ConfigMapping(prefix = "market")
|
||||||
|
public interface MarketConfig {
|
||||||
|
int maxPageSize();
|
||||||
|
Duration cacheTtl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// [QUARKUS] Simple values with @ConfigProperty
|
||||||
|
@ConfigProperty(name = "market.max-page-size", defaultValue = "100")
|
||||||
|
int maxPageSize;
|
||||||
|
```
|
||||||
|
|
||||||
## Testing Expectations
|
## Testing Expectations
|
||||||
|
|
||||||
|
### Shared
|
||||||
- JUnit 5 + AssertJ for fluent assertions
|
- JUnit 5 + AssertJ for fluent assertions
|
||||||
- Mockito for mocking; avoid partial mocks where possible
|
- Mockito for mocking; avoid partial mocks where possible
|
||||||
- Favor deterministic tests; no hidden sleeps
|
- Favor deterministic tests; no hidden sleeps
|
||||||
|
|
||||||
|
### [SPRING]
|
||||||
|
- `@WebMvcTest` for controller slices, `@DataJpaTest` for repository slices
|
||||||
|
- `@SpringBootTest` reserved for full integration tests
|
||||||
|
- `@MockBean` for replacing beans in Spring context
|
||||||
|
|
||||||
|
### [QUARKUS]
|
||||||
|
- Plain JUnit 5 + Mockito for unit tests (no `@QuarkusTest`)
|
||||||
|
- `@QuarkusTest` reserved for CDI integration tests
|
||||||
|
- `@InjectMock` for replacing CDI beans in integration tests
|
||||||
|
- Dev Services for database/Kafka/Redis — avoid manual Testcontainers setup when Dev Services suffice
|
||||||
|
- `@QuarkusTestResource` for custom external service lifecycle
|
||||||
|
|
||||||
|
```java
|
||||||
|
// [SPRING] Controller test
|
||||||
|
@WebMvcTest(MarketController.class)
|
||||||
|
class MarketControllerTest {
|
||||||
|
@Autowired MockMvc mockMvc;
|
||||||
|
@MockBean MarketService marketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [QUARKUS] Integration test
|
||||||
|
@QuarkusTest
|
||||||
|
class MarketResourceTest {
|
||||||
|
@InjectMock
|
||||||
|
MarketService marketService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_return_404_when_market_not_found() {
|
||||||
|
given().when().get("/markets/unknown").then().statusCode(404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [QUARKUS] Unit test (no CDI, no @QuarkusTest)
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class MarketServiceTest {
|
||||||
|
@Mock MarketRepository marketRepository;
|
||||||
|
@InjectMocks MarketService marketService;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Remember**: Keep code intentional, typed, and observable. Optimize for maintainability over micro-optimizations unless proven necessary.
|
**Remember**: Keep code intentional, typed, and observable. Optimize for maintainability over micro-optimizations unless proven necessary.
|
||||||
|
|||||||
@ -135,6 +135,7 @@ Map intent + scope + tech stack (from Phase 0) to specific ECC components.
|
|||||||
| Python / Django | django-patterns, django-tdd, django-security, django-verification, python-patterns, python-testing | python-reviewer |
|
| Python / Django | django-patterns, django-tdd, django-security, django-verification, python-patterns, python-testing | python-reviewer |
|
||||||
| Go | golang-patterns, golang-testing | go-reviewer, go-build-resolver |
|
| Go | golang-patterns, golang-testing | go-reviewer, go-build-resolver |
|
||||||
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | code-reviewer |
|
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | code-reviewer |
|
||||||
|
| Quarkus / Java | quarkus-patterns, quarkus-tdd, quarkus-security, quarkus-verification, java-coding-standards | java-reviewer |
|
||||||
| Kotlin / Android | kotlin-coroutines-flows, compose-multiplatform-patterns, android-clean-architecture | kotlin-reviewer |
|
| Kotlin / Android | kotlin-coroutines-flows, compose-multiplatform-patterns, android-clean-architecture | kotlin-reviewer |
|
||||||
| TypeScript / React | frontend-patterns, backend-patterns, coding-standards | code-reviewer |
|
| TypeScript / React | frontend-patterns, backend-patterns, coding-standards | code-reviewer |
|
||||||
| Swift / iOS | swiftui-patterns, swift-concurrency-6-2, swift-actor-persistence, swift-protocol-di-testing | code-reviewer |
|
| Swift / iOS | swiftui-patterns, swift-concurrency-6-2, swift-actor-persistence, swift-protocol-di-testing | code-reviewer |
|
||||||
|
|||||||
754
skills/quarkus-patterns/SKILL.md
Normal file
754
skills/quarkus-patterns/SKILL.md
Normal file
@ -0,0 +1,754 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-patterns
|
||||||
|
description: Quarkus 3.x LTS architecture patterns with Camel for messaging, RESTful API design, CDI services, data access with Panache, and async processing. Use for Java Quarkus backend work with event-driven architectures.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Development Patterns
|
||||||
|
|
||||||
|
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Building REST APIs with JAX-RS or RESTEasy Reactive
|
||||||
|
- Structuring resource → service → repository layers
|
||||||
|
- Implementing event-driven patterns with Apache Camel and RabbitMQ
|
||||||
|
- Configuring Hibernate Panache, caching, or reactive streams
|
||||||
|
- Adding validation, exception mapping, or pagination
|
||||||
|
- Setting up profiles for dev/staging/production environments (YAML config)
|
||||||
|
- Custom logging with LogContext and Logback/Logstash encoder
|
||||||
|
- Working with CompletableFuture for async operations
|
||||||
|
- Implementing conditional flow processing
|
||||||
|
- Working with GraalVM native compilation
|
||||||
|
|
||||||
|
## Service Layer with Multiple Dependencies (Lombok)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class As2ProcessingService {
|
||||||
|
|
||||||
|
private final InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
private final EventService eventService;
|
||||||
|
private final DocumentJobService documentJobService;
|
||||||
|
private final BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
private final FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
public void processFile(Path filePath) throws Exception {
|
||||||
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
|
||||||
|
String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID);
|
||||||
|
|
||||||
|
// Conditional flow logic
|
||||||
|
boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW));
|
||||||
|
log.info("Is CHORUS_FLOW message: {}", isChorusFlow);
|
||||||
|
|
||||||
|
ValidationFlowConfig validationFlowConfig = isChorusFlow
|
||||||
|
? ValidationFlowConfig.xsdOnly()
|
||||||
|
: ValidationFlowConfig.allValidations();
|
||||||
|
|
||||||
|
InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator
|
||||||
|
.validateFlowWithConfig(filePath, validationFlowConfig,
|
||||||
|
EInvoiceSyntaxFormat.UBL, logContext);
|
||||||
|
|
||||||
|
FlowProfile flowProfile = isChorusFlow ?
|
||||||
|
FlowProfile.EXTENDED_CTC_FR :
|
||||||
|
this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult,
|
||||||
|
invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile());
|
||||||
|
|
||||||
|
log.info("Invoice validation completed. Message is valid");
|
||||||
|
|
||||||
|
// CompletableFuture async operation
|
||||||
|
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
||||||
|
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
||||||
|
fileStorageService.uploadOriginalFile(inputStream,
|
||||||
|
invoiceValidationResult.getSize(), logContext,
|
||||||
|
invoiceValidationResult.getInvoiceFormat());
|
||||||
|
|
||||||
|
StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join();
|
||||||
|
log.info("File uploaded successfully: {}", documentInfo.getPath());
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(documentInfo.getPath())) {
|
||||||
|
String errorMsg = "File path is empty after upload";
|
||||||
|
log.error(errorMsg);
|
||||||
|
this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg);
|
||||||
|
throw new As2ServerProcessingException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE");
|
||||||
|
|
||||||
|
BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities(
|
||||||
|
documentInfo, originalFileName, structureIdPartner,
|
||||||
|
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||||
|
|
||||||
|
// Async Camel publishing
|
||||||
|
businessRulesPublisher.publishAsync(payload);
|
||||||
|
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Patterns:**
|
||||||
|
- `@RequiredArgsConstructor` for constructor injection via Lombok
|
||||||
|
- `@Slf4j` for Logback logging
|
||||||
|
- Scoped LogContext with try-with-resources
|
||||||
|
- Conditional flow logic based on runtime parameters
|
||||||
|
- CompletableFuture with `.join()` for async operations
|
||||||
|
- Event tracking for success/error scenarios
|
||||||
|
- Async Camel message publishing
|
||||||
|
|
||||||
|
## Custom Logging Context Pattern (Logback)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ProcessingService {
|
||||||
|
|
||||||
|
public void processDocument(Document doc) {
|
||||||
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
// Add context to all log statements
|
||||||
|
logContext.put("documentId", doc.getId().toString());
|
||||||
|
logContext.put("documentType", doc.getType());
|
||||||
|
logContext.put("userId", SecurityContext.getUserId());
|
||||||
|
|
||||||
|
log.info("Starting document processing");
|
||||||
|
|
||||||
|
// All logs within this scope inherit the context
|
||||||
|
processInternal(doc);
|
||||||
|
|
||||||
|
log.info("Document processing completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Document processing failed", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logback Configuration (logback.xml):**
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<configuration>
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||||
|
<includeContext>true</includeContext>
|
||||||
|
<includeMdc>true</includeMdc>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="com.example" level="INFO"/>
|
||||||
|
<root level="WARN">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Service Pattern
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EventService {
|
||||||
|
private final EventRepository eventRepository;
|
||||||
|
|
||||||
|
public void createSuccessEvent(Object payload, String eventType) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setType(eventType);
|
||||||
|
event.setStatus(EventStatus.SUCCESS);
|
||||||
|
event.setPayload(serializePayload(payload));
|
||||||
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
|
eventRepository.persist(event);
|
||||||
|
log.info("Success event created: {}", eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
||||||
|
Event event = new Event();
|
||||||
|
event.setType(eventType);
|
||||||
|
event.setStatus(EventStatus.ERROR);
|
||||||
|
event.setErrorMessage(errorMessage);
|
||||||
|
event.setPayload(serializePayload(payload));
|
||||||
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
|
eventRepository.persist(event);
|
||||||
|
log.error("Error event created: {} - {}", eventType, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializePayload(Object payload) {
|
||||||
|
// JSON serialization
|
||||||
|
return objectMapper.writeValueAsString(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Message Publishing (RabbitMQ)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BusinessRulesPublisher {
|
||||||
|
private final ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
|
String businessRulesQueue;
|
||||||
|
|
||||||
|
public void publishAsync(BusinessRulesPayload payload) {
|
||||||
|
producerTemplate.asyncSendBody(
|
||||||
|
"direct:business-rules-publisher",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
log.info("Message published to business rules queue: {}", payload.getDocumentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishSync(BusinessRulesPayload payload) {
|
||||||
|
producerTemplate.sendBody(
|
||||||
|
"direct:business-rules-publisher",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Camel Route Configuration:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class BusinessRulesRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
|
String businessRulesQueue;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "rabbitmq.host")
|
||||||
|
String rabbitHost;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "rabbitmq.port")
|
||||||
|
Integer rabbitPort;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("direct:business-rules-publisher")
|
||||||
|
.routeId("business-rules-publisher")
|
||||||
|
.log("Publishing message to RabbitMQ: ${body}")
|
||||||
|
.marshal().json(JsonLibrary.Jackson)
|
||||||
|
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
||||||
|
businessRulesQueue, rabbitHost, rabbitPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Direct Routes (In-Memory)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class DocumentProcessingRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
// Error handling
|
||||||
|
onException(ValidationException.class)
|
||||||
|
.handled(true)
|
||||||
|
.to("direct:validation-error-handler")
|
||||||
|
.log("Validation error: ${exception.message}");
|
||||||
|
|
||||||
|
// Main processing route
|
||||||
|
from("direct:process-document")
|
||||||
|
.routeId("document-processing")
|
||||||
|
.log("Processing document: ${header.documentId}")
|
||||||
|
.bean(DocumentValidator.class, "validate")
|
||||||
|
.bean(DocumentTransformer.class, "transform")
|
||||||
|
.choice()
|
||||||
|
.when(header("documentType").isEqualTo("INVOICE"))
|
||||||
|
.to("direct:process-invoice")
|
||||||
|
.when(header("documentType").isEqualTo("CREDIT_NOTE"))
|
||||||
|
.to("direct:process-credit-note")
|
||||||
|
.otherwise()
|
||||||
|
.to("direct:process-generic")
|
||||||
|
.end();
|
||||||
|
|
||||||
|
from("direct:validation-error-handler")
|
||||||
|
.bean(EventService.class, "createErrorEvent")
|
||||||
|
.log("Validation error handled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel File Processing
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class FileMonitoringRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.input.directory")
|
||||||
|
String inputDirectory;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.processed.directory")
|
||||||
|
String processedDirectory;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "file.error.directory")
|
||||||
|
String errorDirectory;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
||||||
|
"&moveFailed=" + errorDirectory + "&delay=5000")
|
||||||
|
.routeId("file-monitor")
|
||||||
|
.log("Processing file: ${header.CamelFileName}")
|
||||||
|
.to("direct:process-file");
|
||||||
|
|
||||||
|
from("direct:process-file")
|
||||||
|
.bean(As2ProcessingService.class, "processFile")
|
||||||
|
.log("File processing completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Camel Bean Invocation
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class InvoiceRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure() {
|
||||||
|
from("direct:invoice-validation")
|
||||||
|
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
||||||
|
.log("Validation result: ${body}");
|
||||||
|
|
||||||
|
from("direct:persist-and-publish")
|
||||||
|
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
||||||
|
.bean(BusinessRulesPublisher.class, "publishAsync")
|
||||||
|
.bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST API Structure
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Path("/api/documents")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentResource {
|
||||||
|
private final DocumentService documentService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response list(
|
||||||
|
@QueryParam("page") @DefaultValue("0") int page,
|
||||||
|
@QueryParam("size") @DefaultValue("20") int size) {
|
||||||
|
PaginatedList<Document> documents = documentService.list(page, size);
|
||||||
|
return Response.ok(documents).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public Response create(@Valid CreateDocumentRequest request, @Context UriInfo uriInfo) {
|
||||||
|
Document document = documentService.create(request);
|
||||||
|
URI location = uriInfo.getAbsolutePathBuilder()
|
||||||
|
.path(String.valueOf(document.id))
|
||||||
|
.build();
|
||||||
|
return Response.created(location).entity(DocumentResponse.from(document)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
public Response getById(@PathParam("id") Long id) {
|
||||||
|
return documentService.findById(id)
|
||||||
|
.map(DocumentResponse::from)
|
||||||
|
.map(Response::ok)
|
||||||
|
.orElse(Response.status(Response.Status.NOT_FOUND))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Pattern (Panache Repository)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class DocumentRepository implements PanacheRepository<Document> {
|
||||||
|
|
||||||
|
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
||||||
|
return find("status = ?1 order by createdAt desc", status)
|
||||||
|
.page(page, size)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
||||||
|
return find("referenceNumber", referenceNumber).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
||||||
|
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Layer with Transactions
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentService {
|
||||||
|
private final DocumentRepository repo;
|
||||||
|
private final EventService eventService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Document create(CreateDocumentRequest request) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setReferenceNumber(request.referenceNumber());
|
||||||
|
document.setDescription(request.description());
|
||||||
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
|
document.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
|
repo.persist(document);
|
||||||
|
|
||||||
|
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findById(Long id) {
|
||||||
|
return repo.findByIdOptional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaginatedList<Document> list(int page, int size) {
|
||||||
|
return repo.findAll()
|
||||||
|
.page(page, size)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DTOs and Validation
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record CreateDocumentRequest(
|
||||||
|
@NotBlank @Size(max = 200) String referenceNumber,
|
||||||
|
@NotBlank @Size(max = 2000) String description,
|
||||||
|
@NotNull @FutureOrPresent Instant validUntil,
|
||||||
|
@NotEmpty List<@NotBlank String> categories) {}
|
||||||
|
|
||||||
|
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
||||||
|
public static DocumentResponse from(Document document) {
|
||||||
|
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
||||||
|
document.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exception Mapping
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
|
||||||
|
@Override
|
||||||
|
public Response toResponse(ConstraintViolationException exception) {
|
||||||
|
String message = exception.getConstraintViolations().stream()
|
||||||
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "validation_error", "message", message))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
@Slf4j
|
||||||
|
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(Exception exception) {
|
||||||
|
log.error("Unhandled exception", exception);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "internal_error", "message", "An unexpected error occurred"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CompletableFuture Async Operations
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FileStorageService {
|
||||||
|
private final S3Client s3Client;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
|
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
||||||
|
InputStream inputStream,
|
||||||
|
long size,
|
||||||
|
LogContext logContext,
|
||||||
|
InvoiceFormat format) {
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
String path = generateStoragePath(format);
|
||||||
|
|
||||||
|
PutObjectRequest request = PutObjectRequest.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.key(path)
|
||||||
|
.contentLength(size)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
||||||
|
|
||||||
|
log.info("File uploaded to S3: {}", path);
|
||||||
|
|
||||||
|
return new StoredDocumentInfo(path, size, Instant.now());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to upload file to S3", e);
|
||||||
|
throw new StorageException("Upload failed", e);
|
||||||
|
}
|
||||||
|
}, executorService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DocumentCacheService {
|
||||||
|
private final DocumentRepository repo;
|
||||||
|
|
||||||
|
@CacheResult(cacheName = "document-cache")
|
||||||
|
public Optional<Document> getById(@CacheKey Long id) {
|
||||||
|
return repo.findByIdOptional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheInvalidate(cacheName = "document-cache")
|
||||||
|
public void evict(@CacheKey Long id) {}
|
||||||
|
|
||||||
|
@CacheInvalidateAll(cacheName = "document-cache")
|
||||||
|
public void evictAll() {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration as YAML
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# application.yml
|
||||||
|
"%dev":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: jdbc:postgresql://localhost:5432/dev_db
|
||||||
|
username: dev_user
|
||||||
|
password: dev_pass
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: drop-and-create
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: localhost
|
||||||
|
port: 5672
|
||||||
|
username: guest
|
||||||
|
password: guest
|
||||||
|
|
||||||
|
"%test":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: jdbc:h2:mem:test
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: drop-and-create
|
||||||
|
|
||||||
|
"%prod":
|
||||||
|
quarkus:
|
||||||
|
datasource:
|
||||||
|
jdbc:
|
||||||
|
url: ${DATABASE_URL}
|
||||||
|
username: ${DB_USER}
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
hibernate-orm:
|
||||||
|
database:
|
||||||
|
generation: validate
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: ${RABBITMQ_HOST}
|
||||||
|
port: ${RABBITMQ_PORT}
|
||||||
|
username: ${RABBITMQ_USER}
|
||||||
|
password: ${RABBITMQ_PASSWORD}
|
||||||
|
|
||||||
|
# Camel configuration
|
||||||
|
camel:
|
||||||
|
rabbitmq:
|
||||||
|
queue:
|
||||||
|
business-rules: business-rules-queue
|
||||||
|
invoice-processing: invoice-processing-queue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Readiness
|
||||||
|
@ApplicationScoped
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DatabaseHealthCheck implements HealthCheck {
|
||||||
|
private final AgroalDataSource dataSource;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
try (Connection conn = dataSource.getConnection()) {
|
||||||
|
boolean valid = conn.isValid(2);
|
||||||
|
return HealthCheckResponse.named("Database connection")
|
||||||
|
.status(valid)
|
||||||
|
.build();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return HealthCheckResponse.down("Database connection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Liveness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class CamelHealthCheck implements HealthCheck {
|
||||||
|
@Inject
|
||||||
|
CamelContext camelContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
boolean isStarted = camelContext.getStatus().isStarted();
|
||||||
|
return HealthCheckResponse.named("Camel Context")
|
||||||
|
.status(isStarted)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies (Maven)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<properties>
|
||||||
|
<quarkus.platform.version>3.27.0</quarkus.platform.version>
|
||||||
|
<lombok.version>1.18.42</lombok.version>
|
||||||
|
<assertj-core.version>3.24.2</assertj-core.version>
|
||||||
|
<jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus.platform</groupId>
|
||||||
|
<artifactId>quarkus-bom</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-bom</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Quarkus Core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-arc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Camel Extensions -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-direct</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-bean</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Logging -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||||
|
<artifactId>quarkus-logging-logback</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.logstash.logback</groupId>
|
||||||
|
<artifactId>logstash-logback-encoder</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- Use `@RequiredArgsConstructor` with Lombok for constructor injection
|
||||||
|
- Keep service layer thin; delegate complex logic to specialized classes
|
||||||
|
- Use Camel routes for message routing and integration patterns
|
||||||
|
- Prefer Panache Repository pattern for data access
|
||||||
|
|
||||||
|
### Event-Driven
|
||||||
|
- Always track operations with EventService (success/error events)
|
||||||
|
- Use Camel `direct:` endpoints for in-memory routing
|
||||||
|
- Use `spring-rabbitmq` component for RabbitMQ integration
|
||||||
|
- Implement async publishing with `ProducerTemplate.asyncSendBody()`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Use Logback with Logstash encoder for structured logging
|
||||||
|
- Propagate LogContext through service calls with `SafeAutoCloseable`
|
||||||
|
- Add contextual information to LogContext for request tracing
|
||||||
|
- Use `@Slf4j` instead of manual logger instantiation
|
||||||
|
|
||||||
|
### Async Operations
|
||||||
|
- Use CompletableFuture for non-blocking I/O operations
|
||||||
|
- Call `.join()` when you need to wait for completion
|
||||||
|
- Handle exceptions from CompletableFuture properly
|
||||||
|
- Pass LogContext to async operations for tracing
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Use YAML configuration (`quarkus-config-yaml`)
|
||||||
|
- Profile-aware configuration for dev/test/prod environments
|
||||||
|
- Externalize sensitive configuration to environment variables
|
||||||
|
- Use `@ConfigProperty` for type-safe config injection
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
- Validate at resource layer with `@Valid`
|
||||||
|
- Use Bean Validation annotations on DTOs
|
||||||
|
- Map exceptions to proper HTTP responses with `@Provider`
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
- Use `@Transactional` on service methods that modify data
|
||||||
|
- Keep transactions short and focused
|
||||||
|
- Avoid calling async operations within transactions
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Use `camel-quarkus-junit5` for route testing
|
||||||
|
- Use AssertJ for assertions
|
||||||
|
- Mock all external dependencies
|
||||||
|
- Test conditional flow logic thoroughly
|
||||||
|
|
||||||
|
### Quarkus-Specific
|
||||||
|
- Stay on latest LTS version (3.x)
|
||||||
|
- Use Quarkus dev mode for hot reload
|
||||||
|
- Add health checks for production readiness
|
||||||
|
- Test native compilation compatibility periodically
|
||||||
453
skills/quarkus-security/SKILL.md
Normal file
453
skills/quarkus-security/SKILL.md
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-security
|
||||||
|
description: Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Security Review
|
||||||
|
|
||||||
|
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Adding authentication (JWT, OIDC, Basic Auth)
|
||||||
|
- Implementing authorization with @RolesAllowed or SecurityIdentity
|
||||||
|
- Validating user input (Bean Validation, custom validators)
|
||||||
|
- Configuring CORS or security headers
|
||||||
|
- Managing secrets (Vault, environment variables, config sources)
|
||||||
|
- Adding rate limiting or brute-force protection
|
||||||
|
- Scanning dependencies for CVEs
|
||||||
|
- Working with MicroProfile JWT or SmallRye JWT
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### JWT Authentication
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Resource protected with JWT
|
||||||
|
@Path("/api/protected")
|
||||||
|
@Authenticated
|
||||||
|
public class ProtectedResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JsonWebToken jwt;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response getData() {
|
||||||
|
String username = jwt.getName();
|
||||||
|
Set<String> roles = jwt.getGroups();
|
||||||
|
return Response.ok(Map.of(
|
||||||
|
"username", username,
|
||||||
|
"roles", roles,
|
||||||
|
"principal", securityIdentity.getPrincipal().getName()
|
||||||
|
)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration (application.properties):
|
||||||
|
```properties
|
||||||
|
mp.jwt.verify.publickey.location=publicKey.pem
|
||||||
|
mp.jwt.verify.issuer=https://auth.example.com
|
||||||
|
|
||||||
|
# OIDC
|
||||||
|
quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm
|
||||||
|
quarkus.oidc.client-id=backend-service
|
||||||
|
quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Authentication Filter
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
|
public class CustomAuthFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity identity;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
// Validate token and set SecurityIdentity
|
||||||
|
if (!validateToken(token)) {
|
||||||
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateToken(String token) {
|
||||||
|
// Token validation logic
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
### Role-Based Access Control
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Path("/api/admin")
|
||||||
|
@RolesAllowed("ADMIN")
|
||||||
|
public class AdminResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/users")
|
||||||
|
public List<UserDto> listUsers() {
|
||||||
|
return userService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/users/{id}")
|
||||||
|
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
||||||
|
public Response deleteUser(@PathParam("id") Long id) {
|
||||||
|
userService.delete(id);
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/api/users")
|
||||||
|
public class UserResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
public Response getUser(@PathParam("id") Long id) {
|
||||||
|
// Check ownership
|
||||||
|
if (!securityIdentity.hasRole("ADMIN") &&
|
||||||
|
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
return Response.ok(userService.findById(id)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOwner(Long userId, String username) {
|
||||||
|
return userService.isOwner(userId, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Security
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SecurityService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public boolean canAccessResource(Long resourceId) {
|
||||||
|
if (securityIdentity.isAnonymous()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (securityIdentity.hasRole("ADMIN")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = securityIdentity.getPrincipal().getName();
|
||||||
|
return resourceRepository.isOwner(resourceId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
### Bean Validation
|
||||||
|
|
||||||
|
```java
|
||||||
|
// BAD: No validation
|
||||||
|
@POST
|
||||||
|
public Response createUser(UserDto dto) {
|
||||||
|
return Response.ok(userService.create(dto)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Validated DTO
|
||||||
|
public record CreateUserDto(
|
||||||
|
@NotBlank @Size(max = 100) String name,
|
||||||
|
@NotBlank @Email String email,
|
||||||
|
@NotNull @Min(18) @Max(150) Integer age,
|
||||||
|
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/users")
|
||||||
|
public Response createUser(@Valid CreateUserDto dto) {
|
||||||
|
User user = userService.create(dto);
|
||||||
|
return Response.status(Response.Status.CREATED).entity(user).build();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Validators
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Constraint(validatedBy = UsernameValidator.class)
|
||||||
|
public @interface ValidUsername {
|
||||||
|
String message() default "Invalid username format";
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||||
|
if (value == null) return false;
|
||||||
|
return value.matches("^[a-zA-Z0-9_-]{3,20}$");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
public record CreateUserDto(
|
||||||
|
@ValidUsername String username,
|
||||||
|
@NotBlank @Email String email
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQL Injection Prevention
|
||||||
|
|
||||||
|
### Panache Active Record (Safe by Default)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// GOOD: Parameterized queries with Panache
|
||||||
|
List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
||||||
|
|
||||||
|
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||||
|
|
||||||
|
// GOOD: Named parameters
|
||||||
|
List<User> users = User.list("email = :email and age > :minAge",
|
||||||
|
Parameters.with("email", email).and("minAge", 18));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native Queries (Use Parameters)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// BAD: String concatenation
|
||||||
|
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||||
|
|
||||||
|
// GOOD: Parameterized native query
|
||||||
|
@Entity
|
||||||
|
public class User extends PanacheEntity {
|
||||||
|
public static List<User> findByEmailNative(String email) {
|
||||||
|
return getEntityManager()
|
||||||
|
.createNativeQuery("SELECT * FROM users WHERE email = :email", User.class)
|
||||||
|
.setParameter("email", email)
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Hashing
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class PasswordService {
|
||||||
|
|
||||||
|
public String hash(String plainPassword) {
|
||||||
|
return BcryptUtil.bcryptHash(plainPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(String plainPassword, String hashedPassword) {
|
||||||
|
return BcryptUtil.matches(plainPassword, hashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In service
|
||||||
|
@ApplicationScoped
|
||||||
|
public class UserService {
|
||||||
|
@Inject
|
||||||
|
PasswordService passwordService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User register(CreateUserDto dto) {
|
||||||
|
String hashedPassword = passwordService.hash(dto.password());
|
||||||
|
User user = new User();
|
||||||
|
user.email = dto.email();
|
||||||
|
user.password = hashedPassword;
|
||||||
|
user.persist();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authenticate(String email, String password) {
|
||||||
|
return User.find("email", email)
|
||||||
|
.firstResultOptional()
|
||||||
|
.map(u -> passwordService.verify(password, u.password))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS Configuration
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# application.properties
|
||||||
|
quarkus.http.cors=true
|
||||||
|
quarkus.http.cors.origins=https://app.example.com,https://admin.example.com
|
||||||
|
quarkus.http.cors.methods=GET,POST,PUT,DELETE
|
||||||
|
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
|
||||||
|
quarkus.http.cors.exposed-headers=Content-Disposition
|
||||||
|
quarkus.http.cors.access-control-max-age=24H
|
||||||
|
quarkus.http.cors.access-control-allow-credentials=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets Management
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# application.properties - NO SECRETS HERE
|
||||||
|
|
||||||
|
# Use environment variables
|
||||||
|
quarkus.datasource.username=${DB_USER}
|
||||||
|
quarkus.datasource.password=${DB_PASSWORD}
|
||||||
|
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
||||||
|
|
||||||
|
# Or use Vault
|
||||||
|
quarkus.vault.url=https://vault.example.com
|
||||||
|
quarkus.vault.authentication.kubernetes.role=my-role
|
||||||
|
```
|
||||||
|
|
||||||
|
### HashiCorp Vault Integration
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SecretService {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "api-key")
|
||||||
|
String apiKey; // Fetched from Vault
|
||||||
|
|
||||||
|
public String getSecret(String key) {
|
||||||
|
return ConfigProvider.getConfig().getValue(key, String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RateLimitFilter implements ContainerRequestFilter {
|
||||||
|
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
|
String clientId = getClientIdentifier(requestContext);
|
||||||
|
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||||
|
k -> RateLimiter.create(100.0)); // 100 requests per second
|
||||||
|
|
||||||
|
if (!limiter.tryAcquire()) {
|
||||||
|
requestContext.abortWith(
|
||||||
|
Response.status(429)
|
||||||
|
.entity(Map.of("error", "Too many requests"))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClientIdentifier(ContainerRequestContext ctx) {
|
||||||
|
// Use IP, API key, or user ID
|
||||||
|
return ctx.getHeaderString("X-Forwarded-For");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Headers
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Provider
|
||||||
|
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||||
|
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||||
|
|
||||||
|
// Prevent clickjacking
|
||||||
|
headers.putSingle("X-Frame-Options", "DENY");
|
||||||
|
|
||||||
|
// XSS protection
|
||||||
|
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||||
|
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||||
|
|
||||||
|
// HSTS
|
||||||
|
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
|
||||||
|
// CSP
|
||||||
|
headers.putSingle("Content-Security-Policy",
|
||||||
|
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Logging
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuditService {
|
||||||
|
private static final Logger LOG = Logger.getLogger(AuditService.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public void logAccess(String resource, String action) {
|
||||||
|
String user = securityIdentity.isAnonymous()
|
||||||
|
? "anonymous"
|
||||||
|
: securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
|
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||||
|
user, action, resource, Instant.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in resource
|
||||||
|
@Path("/api/sensitive")
|
||||||
|
public class SensitiveResource {
|
||||||
|
@Inject
|
||||||
|
AuditService auditService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@RolesAllowed("ADMIN")
|
||||||
|
public Response getData() {
|
||||||
|
auditService.logAccess("sensitive-data", "READ");
|
||||||
|
return Response.ok(data).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Security Scanning
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Maven
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./gradlew dependencyCheckAnalyze
|
||||||
|
|
||||||
|
# Check Quarkus extensions
|
||||||
|
quarkus extension list --installable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Always use HTTPS in production
|
||||||
|
- Enable JWT or OIDC for stateless authentication
|
||||||
|
- Use `@RolesAllowed` for declarative authorization
|
||||||
|
- Validate all input with Bean Validation
|
||||||
|
- Hash passwords with BCrypt (never plaintext)
|
||||||
|
- Store secrets in Vault or environment variables
|
||||||
|
- Use parameterized queries to prevent SQL injection
|
||||||
|
- Add security headers to all responses
|
||||||
|
- Implement rate limiting for public endpoints
|
||||||
|
- Audit sensitive operations
|
||||||
|
- Keep dependencies updated and scan for CVEs
|
||||||
|
- Use SecurityIdentity for programmatic checks
|
||||||
|
- Set appropriate CORS policies
|
||||||
|
- Test authentication and authorization paths
|
||||||
908
skills/quarkus-tdd/SKILL.md
Normal file
908
skills/quarkus-tdd/SKILL.md
Normal file
@ -0,0 +1,908 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-tdd
|
||||||
|
description: Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito, REST Assured, Camel testing, and JaCoCo. Use when adding features, fixing bugs, or refactoring event-driven services.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus TDD Workflow
|
||||||
|
|
||||||
|
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- New features or REST endpoints
|
||||||
|
- Bug fixes or refactors
|
||||||
|
- Adding data access logic, security rules, or reactive streams
|
||||||
|
- Testing Apache Camel routes and event handlers
|
||||||
|
- Testing event-driven services with RabbitMQ
|
||||||
|
- Testing conditional flow logic
|
||||||
|
- Validating CompletableFuture async operations
|
||||||
|
- Testing LogContext propagation
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Write tests first (they should fail)
|
||||||
|
2. Implement minimal code to pass
|
||||||
|
3. Refactor with tests green
|
||||||
|
4. Enforce coverage with JaCoCo (80%+ target)
|
||||||
|
|
||||||
|
## Unit Tests with @Nested Organization
|
||||||
|
|
||||||
|
Follow this structured approach for comprehensive, readable tests:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("As2ProcessingService Unit Tests")
|
||||||
|
class As2ProcessingServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EventService eventService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DocumentJobService documentJobService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private As2ProcessingService as2ProcessingService;
|
||||||
|
|
||||||
|
private Path testFilePath;
|
||||||
|
private LogContext testLogContext;
|
||||||
|
private InvoiceValidationResult validationResult;
|
||||||
|
private StoredDocumentInfo documentInfo;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE - Common test data
|
||||||
|
testFilePath = Path.of("/tmp/test-invoice.xml");
|
||||||
|
|
||||||
|
testLogContext = new LogContext();
|
||||||
|
testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001");
|
||||||
|
testLogContext.put(As2Constants.FILE_NAME, "invoice.xml");
|
||||||
|
testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001");
|
||||||
|
|
||||||
|
validationResult = new InvoiceValidationResult();
|
||||||
|
validationResult.setValid(true);
|
||||||
|
validationResult.setSize(1024L);
|
||||||
|
validationResult.setDocumentHash("abc123");
|
||||||
|
|
||||||
|
documentInfo = new StoredDocumentInfo();
|
||||||
|
documentInfo.setPath("s3://bucket/path/invoice.xml");
|
||||||
|
documentInfo.setSize(1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for processFile")
|
||||||
|
class ProcessFile {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully process non-CHORUS file with all validations")
|
||||||
|
void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class)))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any()))
|
||||||
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class));
|
||||||
|
|
||||||
|
verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class),
|
||||||
|
eq("PERSISTENCE_BLOB_EVENT_TYPE"));
|
||||||
|
verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class),
|
||||||
|
eq("BUSINESS_RULES_MESSAGE_SENT"));
|
||||||
|
verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should bypass schematron validation for CHORUS_FLOW")
|
||||||
|
void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class)))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(),
|
||||||
|
eq(FlowProfile.EXTENDED_CTC_FR), any()))
|
||||||
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
|
eq(testFilePath),
|
||||||
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
|
any(LogContext.class));
|
||||||
|
|
||||||
|
verify(documentJobService).createDocumentAndJobEntities(
|
||||||
|
any(), any(), any(),
|
||||||
|
eq(FlowProfile.EXTENDED_CTC_FR),
|
||||||
|
any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create error event when file upload fails")
|
||||||
|
void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
documentInfo.setPath(""); // Blank path triggers error
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
As2ServerProcessingException exception = assertThrows(
|
||||||
|
As2ServerProcessingException.class,
|
||||||
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage())
|
||||||
|
.contains("File path is empty after upload");
|
||||||
|
|
||||||
|
verify(eventService).createErrorEvent(
|
||||||
|
eq(documentInfo),
|
||||||
|
eq("FILE_UPLOAD_FAILED"),
|
||||||
|
contains("File path is empty"));
|
||||||
|
|
||||||
|
verify(businessRulesPublisher, never()).publishAsync(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle CompletableFuture.join() failure")
|
||||||
|
void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
|
CompletableFuture<StoredDocumentInfo> failedFuture =
|
||||||
|
CompletableFuture.failedFuture(new StorageException("S3 connection failed"));
|
||||||
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
|
.thenReturn(failedFuture);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
assertThrows(
|
||||||
|
CompletionException.class,
|
||||||
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when file path is null")
|
||||||
|
void givenNullFilePath_whenProcessFile_thenThrowsException() {
|
||||||
|
// ARRANGE
|
||||||
|
Path nullPath = null;
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
NullPointerException exception = assertThrows(
|
||||||
|
NullPointerException.class,
|
||||||
|
() -> as2ProcessingService.processFile(nullPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Testing Patterns
|
||||||
|
|
||||||
|
1. **@Nested Classes**: Group tests by method being tested
|
||||||
|
2. **@DisplayName**: Provide readable test descriptions for test reports
|
||||||
|
3. **Naming Convention**: `givenX_whenY_thenZ` for clarity
|
||||||
|
4. **AAA Pattern**: Explicit `// ARRANGE`, `// ACT`, `// ASSERT` comments
|
||||||
|
5. **@BeforeEach**: Setup common test data to reduce duplication
|
||||||
|
6. **assertDoesNotThrow**: Test success scenarios without catching exceptions
|
||||||
|
7. **assertThrows**: Test exception scenarios with message validation using AssertJ
|
||||||
|
8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions
|
||||||
|
9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly
|
||||||
|
10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios
|
||||||
|
|
||||||
|
## Testing Camel Routes
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("Business Rules Camel Route Tests")
|
||||||
|
class BusinessRulesRouteTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CamelContext camelContext;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
EventService eventService;
|
||||||
|
|
||||||
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE - Test data
|
||||||
|
testPayload = new BusinessRulesPayload();
|
||||||
|
testPayload.setDocumentId(1L);
|
||||||
|
testPayload.setFlowProfile(FlowProfile.BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for business-rules-publisher route")
|
||||||
|
class BusinessRulesPublisher {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully publish message to RabbitMQ")
|
||||||
|
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||||
|
mockRabbitMQ.expectedMessageCount(1);
|
||||||
|
mockRabbitMQ.expectedBodiesReceived(testPayload);
|
||||||
|
|
||||||
|
// Replace real endpoint with mock for testing
|
||||||
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
|
advice.replaceFromWith("direct:business-rules-publisher");
|
||||||
|
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockRabbitMQ.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
||||||
|
assertThat(mockRabbitMQ.getExchanges().get(0).getIn().getBody(BusinessRulesPayload.class))
|
||||||
|
.isEqualTo(testPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle marshalling to JSON")
|
||||||
|
void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
||||||
|
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
||||||
|
mockMarshal.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
|
advice.weaveAddLast().to("mock:marshal");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockMarshal.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
|
assertThat(body).contains("\"documentId\":1");
|
||||||
|
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for document-processing route")
|
||||||
|
class DocumentProcessing {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should route invoice to correct processor")
|
||||||
|
void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
||||||
|
mockInvoice.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
|
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBodyAndHeader("direct:process-document",
|
||||||
|
testPayload, "documentType", "INVOICE");
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockInvoice.assertIsSatisfied(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle validation errors gracefully")
|
||||||
|
void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
||||||
|
mockError.expectedMessageCount(1);
|
||||||
|
|
||||||
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
|
advice.weaveByToString(".*direct:validation-error-handler.*")
|
||||||
|
.replace().to("mock:error");
|
||||||
|
});
|
||||||
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
|
// Mock validator to throw exception
|
||||||
|
when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
producerTemplate.sendBody("direct:process-document", testPayload);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
mockError.assertIsSatisfied(5000);
|
||||||
|
|
||||||
|
Exception exception = mockError.getExchanges().get(0).getException();
|
||||||
|
assertThat(exception).isInstanceOf(ValidationException.class);
|
||||||
|
assertThat(exception.getMessage()).contains("Invalid document");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Event Services
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("EventService Unit Tests")
|
||||||
|
class EventServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private EventService eventService;
|
||||||
|
|
||||||
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE
|
||||||
|
testPayload = new BusinessRulesPayload();
|
||||||
|
testPayload.setDocumentId(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for createSuccessEvent")
|
||||||
|
class CreateSuccessEvent {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create success event with correct attributes")
|
||||||
|
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() ->
|
||||||
|
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(eventRepository).persist(argThat(event ->
|
||||||
|
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||||
|
event.getStatus() == EventStatus.SUCCESS &&
|
||||||
|
event.getPayload().equals("{\"documentId\":1}") &&
|
||||||
|
event.getTimestamp() != null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when payload is null")
|
||||||
|
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||||
|
// ARRANGE
|
||||||
|
Object nullPayload = null;
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
||||||
|
verify(eventRepository, never()).persist(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for createErrorEvent")
|
||||||
|
class CreateErrorEvent {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create error event with error message")
|
||||||
|
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
String errorMessage = "Processing failed";
|
||||||
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
assertDoesNotThrow(() ->
|
||||||
|
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
verify(eventRepository).persist(argThat(event ->
|
||||||
|
event.getType().equals("PROCESSING_ERROR") &&
|
||||||
|
event.getStatus() == EventStatus.ERROR &&
|
||||||
|
event.getErrorMessage().equals(errorMessage) &&
|
||||||
|
event.getPayload().equals("{\"documentId\":1}")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@DisplayName("Should reject invalid error messages")
|
||||||
|
@ValueSource(strings = {"", " "})
|
||||||
|
void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
|
||||||
|
// ACT & ASSERT
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing CompletableFuture
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("FileStorageService Unit Tests")
|
||||||
|
class FileStorageServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private S3Client s3Client;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExecutorService executorService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
private InputStream testInputStream;
|
||||||
|
private LogContext testLogContext;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// ARRANGE
|
||||||
|
testInputStream = new ByteArrayInputStream("test content".getBytes());
|
||||||
|
testLogContext = new LogContext();
|
||||||
|
testLogContext.put("traceId", "trace-123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for uploadOriginalFile")
|
||||||
|
class UploadOriginalFile {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should successfully upload file and return document info")
|
||||||
|
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
Callable<?> callable = invocation.getArgument(0);
|
||||||
|
return CompletableFuture.completedFuture(callable.call());
|
||||||
|
});
|
||||||
|
|
||||||
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
|
.thenReturn(PutObjectResponse.builder().build());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
|
StoredDocumentInfo result = future.join();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getPath()).isNotBlank();
|
||||||
|
assertThat(result.getSize()).isEqualTo(1024L);
|
||||||
|
assertThat(result.getUploadedAt()).isNotNull();
|
||||||
|
|
||||||
|
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle S3 upload failure")
|
||||||
|
void givenS3Failure_whenUpload_thenCompletableFutureFails() {
|
||||||
|
// ARRANGE
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
return CompletableFuture.failedFuture(new StorageException("S3 unavailable"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThatThrownBy(() -> future.join())
|
||||||
|
.isInstanceOf(CompletionException.class)
|
||||||
|
.hasCauseInstanceOf(StorageException.class)
|
||||||
|
.hasMessageContaining("S3 unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should propagate LogContext to async operation")
|
||||||
|
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||||
|
// ARRANGE
|
||||||
|
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
||||||
|
|
||||||
|
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||||
|
Callable<?> callable = invocation.getArgument(0);
|
||||||
|
capturedContext.set(CustomLog.getCurrentContext());
|
||||||
|
return CompletableFuture.completedFuture(callable.call());
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
|
testLogContext, InvoiceFormat.UBL).join();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
assertThat(capturedContext.get()).isNotNull();
|
||||||
|
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource Layer Tests (REST Assured)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("DocumentResource API Tests")
|
||||||
|
class DocumentResourceTest {
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
DocumentService documentService;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for GET /api/documents")
|
||||||
|
class ListDocuments {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return list of documents")
|
||||||
|
void givenDocumentsExist_whenList_thenReturnsOk() {
|
||||||
|
// ARRANGE
|
||||||
|
List<Document> documents = List.of(createDocument(1L, "DOC-001"));
|
||||||
|
when(documentService.list(0, 20)).thenReturn(documents);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.when().get("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$.size()", is(1))
|
||||||
|
.body("[0].referenceNumber", equalTo("DOC-001"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Tests for POST /api/documents")
|
||||||
|
class CreateDocument {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create document and return 201")
|
||||||
|
void givenValidRequest_whenCreate_thenReturns201() {
|
||||||
|
// ARRANGE
|
||||||
|
Document document = createDocument(1L, "DOC-001");
|
||||||
|
when(documentService.create(any())).thenReturn(document);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "DOC-001",
|
||||||
|
"description": "Test document",
|
||||||
|
"validUntil": "2030-01-01T00:00:00Z",
|
||||||
|
"categories": ["test"]
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.header("Location", containsString("/api/documents/1"))
|
||||||
|
.body("referenceNumber", equalTo("DOC-001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return 400 for invalid input")
|
||||||
|
void givenInvalidRequest_whenCreate_thenReturns400() {
|
||||||
|
// ACT & ASSERT
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "",
|
||||||
|
"description": "Test"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document createDocument(Long id, String referenceNumber) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setId(id);
|
||||||
|
document.setReferenceNumber(referenceNumber);
|
||||||
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Tests with Real Database
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@TestProfile(IntegrationTestProfile.class)
|
||||||
|
@DisplayName("Document Integration Tests")
|
||||||
|
class DocumentIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
@DisplayName("Should create and retrieve document via API")
|
||||||
|
void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
|
||||||
|
// ACT - Create via API
|
||||||
|
Long id = given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"referenceNumber": "INT-001",
|
||||||
|
"description": "Integration test",
|
||||||
|
"validUntil": "2030-01-01T00:00:00Z",
|
||||||
|
"categories": ["test"]
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when().post("/api/documents")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.extract().path("id");
|
||||||
|
|
||||||
|
// ASSERT - Retrieve via API
|
||||||
|
given()
|
||||||
|
.when().get("/api/documents/" + id)
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("referenceNumber", equalTo("INT-001"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage with JaCoCo
|
||||||
|
|
||||||
|
### Maven Configuration (Complete)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.13</version>
|
||||||
|
<executions>
|
||||||
|
<!-- Prepare agent for test execution -->
|
||||||
|
<execution>
|
||||||
|
<id>prepare-agent</id>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<!-- Generate coverage report -->
|
||||||
|
<execution>
|
||||||
|
<id>report</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
|
||||||
|
<!-- Enforce coverage thresholds -->
|
||||||
|
<execution>
|
||||||
|
<id>check</id>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.80</minimum>
|
||||||
|
</limit>
|
||||||
|
<limit>
|
||||||
|
<counter>BRANCH</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.70</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests with coverage:
|
||||||
|
```bash
|
||||||
|
mvn clean test
|
||||||
|
mvn jacoco:report
|
||||||
|
mvn jacoco:check
|
||||||
|
|
||||||
|
# Report at: target/site/jacoco/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Dependencies
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependencies>
|
||||||
|
<!-- Quarkus Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Mockito -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- AssertJ (preferred over JUnit assertions) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.24.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- REST Assured -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.rest-assured</groupId>
|
||||||
|
<artifactId>rest-assured</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Camel Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
<artifactId>camel-quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
- Use `@Nested` classes to group tests by method being tested
|
||||||
|
- Use `@DisplayName` for readable test descriptions visible in reports
|
||||||
|
- Follow `givenX_whenY_thenZ` naming convention for test methods
|
||||||
|
- Use `@BeforeEach` for common test data setup to reduce duplication
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
- Follow AAA pattern with explicit comments (`// ARRANGE`, `// ACT`, `// ASSERT`)
|
||||||
|
- Use `assertDoesNotThrow` for success scenarios
|
||||||
|
- Use `assertThrows` for exception scenarios with message validation
|
||||||
|
- Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()`
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
- Test happy paths for all public methods
|
||||||
|
- Test null input handling
|
||||||
|
- Test edge cases (empty collections, boundary values, negative IDs, blank strings)
|
||||||
|
- Test exception scenarios comprehensively
|
||||||
|
- Mock all external dependencies (repositories, services, Camel endpoints)
|
||||||
|
- Aim for 80%+ line coverage, 70%+ branch coverage
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
- **Always use AssertJ** (`assertThat`) instead of JUnit assertions
|
||||||
|
- Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)`
|
||||||
|
- For exceptions: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
|
||||||
|
- For collections: `extracting()`, `filteredOn()`, `containsExactly()`
|
||||||
|
|
||||||
|
### Testing Integration
|
||||||
|
- Use `@QuarkusTest` for integration tests
|
||||||
|
- Use `@InjectMock` to mock dependencies in Quarkus tests
|
||||||
|
- Prefer REST Assured for API testing
|
||||||
|
- Use `@TestProfile` for test-specific configuration
|
||||||
|
|
||||||
|
### Event-Driven Testing
|
||||||
|
- Test Camel routes with `AdviceWith` and `MockEndpoint`
|
||||||
|
- Use `@CamelQuarkusTest` annotation (if using standalone Camel tests)
|
||||||
|
- Verify message content, headers, and routing logic
|
||||||
|
- Test error handling routes separately
|
||||||
|
- Mock external systems (RabbitMQ, S3, databases) in unit tests
|
||||||
|
|
||||||
|
### Camel Route Testing
|
||||||
|
- Use `MockEndpoint` for asserting message flow
|
||||||
|
- Use `AdviceWith` to modify routes for testing (replace endpoints with mocks)
|
||||||
|
- Test message transformation and marshalling
|
||||||
|
- Test exception handling and dead letter queues
|
||||||
|
|
||||||
|
### Testing Async Operations
|
||||||
|
- Test CompletableFuture success and failure scenarios
|
||||||
|
- Use `.join()` in tests to wait for async completion
|
||||||
|
- Test exception propagation from CompletableFuture
|
||||||
|
- Verify LogContext propagation to async operations
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Keep tests fast and isolated
|
||||||
|
- Run tests in continuous mode: `mvn quarkus:test`
|
||||||
|
- Use parameterized tests (`@ParameterizedTest`) for input variations
|
||||||
|
- Build reusable test data builders or factory methods
|
||||||
|
|
||||||
|
### Quarkus-Specific
|
||||||
|
- Stay on latest LTS version (Quarkus 3.x)
|
||||||
|
- Test native compilation compatibility periodically
|
||||||
|
- Use Quarkus test profiles for different scenarios
|
||||||
|
- Leverage Quarkus dev services for local testing
|
||||||
|
- Use `@InjectMock` instead of `@MockBean` (Quarkus-specific)
|
||||||
|
|
||||||
|
### Verification Best Practices
|
||||||
|
- Always verify interactions on mocked dependencies
|
||||||
|
- Use `verify(mock, never())` to ensure methods are NOT called in error scenarios
|
||||||
|
- Use `argThat()` for complex argument matching
|
||||||
|
- Verify the order of calls when it matters: `InOrder` from Mockito
|
||||||
481
skills/quarkus-verification/SKILL.md
Normal file
481
skills/quarkus-verification/SKILL.md
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
---
|
||||||
|
name: quarkus-verification
|
||||||
|
description: "Verification loop for Quarkus projects: build, static analysis, tests with coverage, security scans, native compilation, and diff review before release or PR."
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quarkus Verification Loop
|
||||||
|
|
||||||
|
Run before PRs, after major changes, and pre-deploy.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Before opening a pull request for a Quarkus service
|
||||||
|
- After major refactoring or dependency upgrades
|
||||||
|
- Pre-deployment verification for staging or production
|
||||||
|
- Running full build → lint → test → security scan → native compilation pipeline
|
||||||
|
- Validating test coverage meets thresholds (80%+)
|
||||||
|
- Testing native image compatibility
|
||||||
|
|
||||||
|
## Phase 1: Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Maven
|
||||||
|
mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./gradlew clean assemble -x test
|
||||||
|
```
|
||||||
|
|
||||||
|
If build fails, stop and fix compilation errors.
|
||||||
|
|
||||||
|
## Phase 2: Static Analysis
|
||||||
|
|
||||||
|
### Checkstyle, PMD, SpotBugs (Maven)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn checkstyle:check pmd:check spotbugs:check
|
||||||
|
```
|
||||||
|
|
||||||
|
### SonarQube (if configured)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn sonar:sonar \
|
||||||
|
-Dsonar.projectKey=my-quarkus-project \
|
||||||
|
-Dsonar.host.url=http://localhost:9000 \
|
||||||
|
-Dsonar.login=${SONAR_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues to Address
|
||||||
|
|
||||||
|
- Unused imports or variables
|
||||||
|
- Complex methods (high cyclomatic complexity)
|
||||||
|
- Potential null pointer dereferences
|
||||||
|
- Security issues flagged by SpotBugs
|
||||||
|
|
||||||
|
## Phase 3: Tests + Coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
mvn clean test
|
||||||
|
|
||||||
|
# Generate coverage report
|
||||||
|
mvn jacoco:report
|
||||||
|
|
||||||
|
# Enforce coverage threshold (80%)
|
||||||
|
mvn jacoco:check
|
||||||
|
|
||||||
|
# Or with Gradle
|
||||||
|
./gradlew test jacocoTestReport jacocoTestCoverageVerification
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Categories
|
||||||
|
|
||||||
|
#### Unit Tests
|
||||||
|
Test service logic with mocked dependencies:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserServiceTest {
|
||||||
|
@Mock UserRepository userRepository;
|
||||||
|
@InjectMocks UserService userService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_validInput_returnsUser() {
|
||||||
|
var dto = new CreateUserDto("Alice", "alice@example.com");
|
||||||
|
var expected = new User();
|
||||||
|
expected.id = 1L;
|
||||||
|
expected.name = dto.name();
|
||||||
|
|
||||||
|
when(userRepository.persist(any(User.class))).thenReturn(expected);
|
||||||
|
|
||||||
|
User result = userService.create(dto);
|
||||||
|
|
||||||
|
assertThat(result.name).isEqualTo("Alice");
|
||||||
|
verify(userRepository).persist(any(User.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Integration Tests
|
||||||
|
Test with real database (Testcontainers):
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
@QuarkusTestResource(PostgresTestResource.class)
|
||||||
|
class UserRepositoryIntegrationTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UserRepository userRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void findByEmail_existingUser_returnsUser() {
|
||||||
|
User user = new User();
|
||||||
|
user.name = "Alice";
|
||||||
|
user.email = "alice@example.com";
|
||||||
|
userRepository.persist(user);
|
||||||
|
|
||||||
|
Optional<User> found = userRepository.findByEmail("alice@example.com");
|
||||||
|
|
||||||
|
assertThat(found).isPresent();
|
||||||
|
assertThat(found.get().name).isEqualTo("Alice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Tests
|
||||||
|
Test REST endpoints with REST Assured:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@QuarkusTest
|
||||||
|
class UserResourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_validInput_returns201() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{"name": "Alice", "email": "alice@example.com"}
|
||||||
|
""")
|
||||||
|
.when().post("/api/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.body("name", equalTo("Alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUser_invalidEmail_returns400() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{"name": "Alice", "email": "invalid"}
|
||||||
|
""")
|
||||||
|
.when().post("/api/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage Report
|
||||||
|
|
||||||
|
Check `target/site/jacoco/index.html` for detailed coverage:
|
||||||
|
- Overall line coverage (target: 80%+)
|
||||||
|
- Branch coverage (target: 70%+)
|
||||||
|
- Identify uncovered critical paths
|
||||||
|
|
||||||
|
## Phase 4: Security Scanning
|
||||||
|
|
||||||
|
### Dependency Vulnerabilities (Maven)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
```
|
||||||
|
|
||||||
|
Review `target/dependency-check-report.html` for CVEs.
|
||||||
|
|
||||||
|
### Quarkus Security Audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check vulnerable extensions
|
||||||
|
mvn quarkus:audit
|
||||||
|
|
||||||
|
# List all extensions
|
||||||
|
mvn quarkus:list-extensions
|
||||||
|
```
|
||||||
|
|
||||||
|
### OWASP ZAP (API Security Testing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -t owasp/zap2docker-stable zap-api-scan.py \
|
||||||
|
-t http://localhost:8080/q/openapi \
|
||||||
|
-f openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Security Checks
|
||||||
|
|
||||||
|
- [ ] All secrets in environment variables (not in code)
|
||||||
|
- [ ] Input validation on all endpoints
|
||||||
|
- [ ] Authentication/authorization configured
|
||||||
|
- [ ] CORS properly configured
|
||||||
|
- [ ] Security headers set
|
||||||
|
- [ ] Passwords hashed with BCrypt
|
||||||
|
- [ ] SQL injection protection (parameterized queries)
|
||||||
|
- [ ] Rate limiting on public endpoints
|
||||||
|
|
||||||
|
## Phase 5: Native Compilation
|
||||||
|
|
||||||
|
Test GraalVM native image compatibility:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build native executable
|
||||||
|
mvn package -Dnative
|
||||||
|
|
||||||
|
# Or with container
|
||||||
|
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||||
|
|
||||||
|
# Test native executable
|
||||||
|
./target/*-runner
|
||||||
|
|
||||||
|
# Run basic smoke tests
|
||||||
|
curl http://localhost:8080/q/health/live
|
||||||
|
curl http://localhost:8080/q/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native Image Troubleshooting
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
- **Reflection**: Add reflection config for dynamic classes
|
||||||
|
- **Resources**: Include resources with `quarkus.native.resources.includes`
|
||||||
|
- **JNI**: Register JNI classes if using native libraries
|
||||||
|
|
||||||
|
Example reflection config:
|
||||||
|
```java
|
||||||
|
@RegisterForReflection(targets = {MyDynamicClass.class})
|
||||||
|
public class ReflectionConfiguration {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 6: Performance Testing
|
||||||
|
|
||||||
|
### Load Testing with K6
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// load-test.js
|
||||||
|
import http from 'k6/http';
|
||||||
|
import { check } from 'k6';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: '30s', target: 50 },
|
||||||
|
{ duration: '1m', target: 100 },
|
||||||
|
{ duration: '30s', target: 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const res = http.get('http://localhost:8080/api/markets');
|
||||||
|
check(res, {
|
||||||
|
'status is 200': (r) => r.status === 200,
|
||||||
|
'response time < 200ms': (r) => r.timings.duration < 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
k6 run load-test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics to Monitor
|
||||||
|
|
||||||
|
- Response time (p50, p95, p99)
|
||||||
|
- Throughput (requests/sec)
|
||||||
|
- Error rate
|
||||||
|
- Memory usage
|
||||||
|
- CPU usage
|
||||||
|
|
||||||
|
## Phase 7: Health Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Liveness
|
||||||
|
curl http://localhost:8080/q/health/live
|
||||||
|
|
||||||
|
# Readiness
|
||||||
|
curl http://localhost:8080/q/health/ready
|
||||||
|
|
||||||
|
# All health checks
|
||||||
|
curl http://localhost:8080/q/health
|
||||||
|
|
||||||
|
# Metrics (if enabled)
|
||||||
|
curl http://localhost:8080/q/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected responses:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "UP",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"name": "Database connection",
|
||||||
|
"status": "UP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 8: Container Image Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build container image
|
||||||
|
mvn package -Dquarkus.container-image.build=true
|
||||||
|
|
||||||
|
# Or with specific registry
|
||||||
|
mvn package \
|
||||||
|
-Dquarkus.container-image.build=true \
|
||||||
|
-Dquarkus.container-image.registry=docker.io \
|
||||||
|
-Dquarkus.container-image.group=myorg \
|
||||||
|
-Dquarkus.container-image.tag=1.0.0
|
||||||
|
|
||||||
|
# Test container
|
||||||
|
docker run -p 8080:8080 myorg/my-quarkus-app:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Security Scan
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trivy
|
||||||
|
trivy image myorg/my-quarkus-app:1.0.0
|
||||||
|
|
||||||
|
# Grype
|
||||||
|
grype myorg/my-quarkus-app:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 9: Configuration Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all configuration properties
|
||||||
|
mvn quarkus:info
|
||||||
|
|
||||||
|
# List all config sources
|
||||||
|
curl http://localhost:8080/q/dev/io.quarkus.quarkus-vertx-http/config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment-Specific Checks
|
||||||
|
|
||||||
|
- [ ] Database URLs configured per environment
|
||||||
|
- [ ] Secrets externalized (Vault, env vars)
|
||||||
|
- [ ] Logging levels appropriate
|
||||||
|
- [ ] CORS origins set correctly
|
||||||
|
- [ ] Rate limiting configured
|
||||||
|
- [ ] Monitoring/tracing enabled
|
||||||
|
|
||||||
|
## Phase 10: Documentation Review
|
||||||
|
|
||||||
|
- [ ] OpenAPI/Swagger docs up to date (`/q/swagger-ui`)
|
||||||
|
- [ ] README has setup instructions
|
||||||
|
- [ ] API changes documented
|
||||||
|
- [ ] Migration guide for breaking changes
|
||||||
|
- [ ] Configuration properties documented
|
||||||
|
|
||||||
|
Generate OpenAPI spec:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/q/openapi -o openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- [ ] Build passes without warnings
|
||||||
|
- [ ] Static analysis clean (no high/medium issues)
|
||||||
|
- [ ] Code follows team conventions
|
||||||
|
- [ ] No commented-out code or TODOs in PR
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Code coverage ≥ 80%
|
||||||
|
- [ ] Integration tests with real database
|
||||||
|
- [ ] Security tests pass
|
||||||
|
- [ ] Performance within acceptable limits
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- [ ] No dependency vulnerabilities
|
||||||
|
- [ ] Authentication/authorization tested
|
||||||
|
- [ ] Input validation complete
|
||||||
|
- [ ] Secrets not in source code
|
||||||
|
- [ ] Security headers configured
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
- [ ] Native compilation successful
|
||||||
|
- [ ] Container image builds
|
||||||
|
- [ ] Health checks respond correctly
|
||||||
|
- [ ] Configuration valid for target environment
|
||||||
|
|
||||||
|
### Native Image
|
||||||
|
- [ ] Native executable builds
|
||||||
|
- [ ] Native tests pass
|
||||||
|
- [ ] Startup time < 100ms
|
||||||
|
- [ ] Memory footprint acceptable
|
||||||
|
|
||||||
|
## Automated Verification Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Phase 1: Build ==="
|
||||||
|
mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
echo "=== Phase 2: Static Analysis ==="
|
||||||
|
mvn checkstyle:check pmd:check spotbugs:check
|
||||||
|
|
||||||
|
echo "=== Phase 3: Tests + Coverage ==="
|
||||||
|
mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
|
echo "=== Phase 4: Security Scan ==="
|
||||||
|
mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
echo "=== Phase 5: Native Compilation ==="
|
||||||
|
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||||
|
|
||||||
|
echo "=== All Phases Complete ==="
|
||||||
|
echo "Review reports:"
|
||||||
|
echo " - Coverage: target/site/jacoco/index.html"
|
||||||
|
echo " - Security: target/dependency-check-report.html"
|
||||||
|
echo " - Native: target/*-runner"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Verification
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '21'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Cache Maven packages
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.m2
|
||||||
|
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: mvn clean verify -DskipTests
|
||||||
|
|
||||||
|
- name: Test with Coverage
|
||||||
|
run: mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
|
- name: Security Scan
|
||||||
|
run: mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
|
- name: Upload Coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: target/site/jacoco/jacoco.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Run verification loop before every PR
|
||||||
|
- Automate in CI/CD pipeline
|
||||||
|
- Fix issues immediately; don't accumulate debt
|
||||||
|
- Keep coverage above 80%
|
||||||
|
- Update dependencies regularly
|
||||||
|
- Test native compilation periodically
|
||||||
|
- Monitor performance trends
|
||||||
|
- Document breaking changes
|
||||||
|
- Review security scan results
|
||||||
|
- Validate configuration for each environment
|
||||||
Loading…
x
Reference in New Issue
Block a user