add quarkus to java part

This commit is contained in:
AlexisLeDain
2026-04-08 16:24:27 +02:00
parent 098b773c11
commit c44d37e931
43 changed files with 10908 additions and 60 deletions

View File

@@ -228,6 +228,10 @@ everything-claude-code/
| |-- django-verification/ # Django 検証ループ(新規)
| |-- python-patterns/ # 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-security/ # Spring Boot セキュリティ(新規)
| |-- springboot-tdd/ # Spring Boot TDD新規

View File

@@ -19,6 +19,10 @@
- `django-patterns/` - Django ベストプラクティス
- `django-tdd/` - 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-tdd/` - Spring Boot テスト
- `springboot-security/` - Spring Boot セキュリティ

View File

@@ -65,7 +65,7 @@ mkdir -p $TARGET/skills $TARGET/rules
### 2a: スキルカテゴリの選択
27個のスキルが4つのカテゴリに分類されています。`multiSelect: true``AskUserQuestion` を使用します:
31個のスキルが4つのカテゴリに分類されています。`multiSelect: true``AskUserQuestion` を使用します:
```
Question: "どのスキルカテゴリをインストールしますか?"
@@ -80,7 +80,7 @@ Options:
選択された各カテゴリについて、以下の完全なスキルリストを表示し、ユーザーに確認または特定のものの選択解除を依頼します。リストが4項目を超える場合、リストをテキストとして表示し、`AskUserQuestion` で「リストされたすべてをインストール」オプションと、ユーザーが特定の名前を貼り付けるための「その他」オプションを使用します。
**カテゴリ: Framework & Language16スキル)**
**カテゴリ: Framework & Language20スキル)**
| スキル | 説明 |
|-------|-------------|
@@ -96,6 +96,10 @@ Options:
| `java-coding-standards` | Spring Boot 用 Java コーディング標準: 命名、不変性、Optional、ストリーム |
| `python-patterns` | Pythonic なイディオム、PEP 8、型ヒント、ベストプラクティス |
| `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-security` | Spring Security: 認証/認可、検証、CSRF、シークレット、レート制限 |
| `springboot-tdd` | JUnit 5、Mockito、MockMvc、Testcontainers による Spring Boot TDD |

View 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

View 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

View 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

View 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

View File

@@ -150,4 +150,6 @@ Remaining errors: 1
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

View File

@@ -89,4 +89,6 @@ grep -rn "FetchType.EAGER" src/main/java --include="*.java"
- **Uyarı**: Sadece MEDIUM sorunlar
- **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

View 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

View 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

View 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

View 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

View File

@@ -333,6 +333,10 @@ everything-claude-code/
| |-- laravel-verification/ # Laravel 验证循环(新增)
| |-- python-patterns/ # 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-security/ # Spring Boot 安全(新增)
| |-- 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/
# 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/
# done
```

View File

@@ -151,4 +151,6 @@ grep -A5 "annotationProcessorPaths\|annotationProcessor" pom.xml build.gradle
最终:`Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
有关详细的 Java 和 Spring Boot 模式,请参阅 `skill: springboot-patterns`
有关详细的模式和示例:
* **[SPRING]**:请参阅 `skill: springboot-patterns`
* **[QUARKUS]**:请参阅 `skill: quarkus-patterns`

View File

@@ -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`

View File

@@ -144,4 +144,5 @@ public record ApiResponse<T>(boolean success, T data, String error) {
## 参考
有关 Spring Boot 架构模式,请参见技能:`springboot-patterns`
有关使用 Camel 和 Panache 的 Quarkus 架构模式,请参见技能:`quarkus-patterns`
有关实体设计和查询优化,请参见技能:`jpa-patterns`

View File

@@ -98,4 +98,5 @@ try {
## 参考
关于 Spring Security 认证与授权模式,请参见技能:`springboot-security`
关于使用 JWT/OIDC、RBAC 和 CDI 的 Quarkus 安全模式,请参见技能:`quarkus-security`
关于通用安全检查清单,请参见技能:`security-review`

View File

@@ -113,6 +113,7 @@ class OrderRepositoryIT {
```
关于 Spring Boot 集成测试,请参阅技能:`springboot-tdd`
关于 Quarkus 集成测试,请参阅技能:`quarkus-tdd`
## 测试命名
@@ -130,4 +131,5 @@ class OrderRepositoryIT {
## 参考
关于使用 MockMvc 和 Testcontainers 的 Spring Boot TDD 模式,请参阅技能:`springboot-tdd`
关于使用 REST Assured 和 Camel 测试的 Quarkus TDD 模式,请参阅技能:`quarkus-tdd`
关于测试期望,请参阅技能:`java-coding-standards`

View File

@@ -126,6 +126,10 @@ mkdir -p $TARGET/skills $TARGET/rules
| `java-coding-standards` | Spring Boot 的 Java 编码标准命名、不可变性、Optional、流 |
| `python-patterns` | Pythonic 惯用法、PEP 8、类型提示、最佳实践 |
| `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-security` | Spring Security认证/授权、验证、CSRF、密钥、速率限制 |
| `springboot-tdd` | 使用 JUnit 5、Mockito、MockMvc、Testcontainers 进行 Spring Boot TDD |
@@ -274,6 +278,7 @@ grep -rn "skills/" $TARGET/skills/
* `django-tdd` 可能会引用 `django-patterns`
* `laravel-tdd` 可能会引用 `laravel-patterns`
* `quarkus-tdd` 可能会引用 `quarkus-patterns`
* `springboot-tdd` 可能会引用 `springboot-patterns`
* `continuous-learning-v2` 引用 `~/.claude/homunculus/` 目录
* `python-testing` 可能会引用 `python-patterns`

View File

@@ -117,6 +117,7 @@ metadata:
| 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 |
| 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 |
| 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 |

View 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

View 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

View 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

View 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