mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-11 02:33:10 +08:00
translate properly docs/
This commit is contained in:
@@ -1,29 +1,27 @@
|
||||
---
|
||||
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.
|
||||
description: Quarkus 3.x LTSアーキテクチャパターン、Camelメッセージング、RESTful API設計、CDIサービス、Panacheデータアクセス、非同期処理。イベント駆動アーキテクチャを持つJava Quarkusバックエンド作業に使用。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Note / 注意**: このファイルはまだ日本語に翻訳されていません。現在は英語の原文です。翻訳PRを歓迎します。
|
||||
# Quarkus 開発パターン
|
||||
|
||||
# Quarkus Development Patterns
|
||||
Apache Camelを使用したクラウドネイティブなイベント駆動サービスのためのQuarkus 3.xアーキテクチャとAPIパターン。
|
||||
|
||||
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
|
||||
## いつアクティブにするか
|
||||
|
||||
## When to Activate
|
||||
- JAX-RSまたはRESTEasy ReactiveでREST APIを構築する
|
||||
- リソース → サービス → リポジトリレイヤーを構造化する
|
||||
- Apache CamelとRabbitMQでイベント駆動パターンを実装する
|
||||
- Hibernate Panache、キャッシング、またはリアクティブストリームを構成する
|
||||
- バリデーション、例外マッピング、またはページネーションを追加する
|
||||
- dev/staging/production環境のプロファイルを設定する(YAML構成)
|
||||
- LogContextとLogback/Logstashエンコーダーでカスタムロギング
|
||||
- CompletableFutureで非同期操作を行う
|
||||
- 条件付きフロー処理を実装する
|
||||
- GraalVMネイティブコンパイルで作業する
|
||||
|
||||
- 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)
|
||||
## 複数依存関係を持つサービスレイヤー(Lombok)
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
@@ -43,7 +41,7 @@ public class As2ProcessingService {
|
||||
|
||||
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);
|
||||
|
||||
@@ -62,7 +60,7 @@ public class As2ProcessingService {
|
||||
|
||||
log.info("Invoice validation completed. Message is valid");
|
||||
|
||||
// CompletableFuture async operation
|
||||
// CompletableFuture非同期操作
|
||||
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
||||
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
||||
fileStorageService.uploadOriginalFile(inputStream,
|
||||
@@ -85,7 +83,7 @@ public class As2ProcessingService {
|
||||
documentInfo, originalFileName, structureIdPartner,
|
||||
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||
|
||||
// Async Camel publishing
|
||||
// 非同期Camelパブリッシング
|
||||
businessRulesPublisher.publishAsync(payload);
|
||||
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||
}
|
||||
@@ -94,16 +92,16 @@ public class As2ProcessingService {
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
**主要パターン:**
|
||||
- Lombokによるコンストラクタインジェクション用の`@RequiredArgsConstructor`
|
||||
- Logbackロギング用の`@Slf4j`
|
||||
- try-with-resourcesによるスコープ付きLogContext
|
||||
- ランタイムパラメータに基づく条件付きフローロジック
|
||||
- 非同期操作用の`.join()`付きCompletableFuture
|
||||
- 成功/エラーシナリオのイベントトラッキング
|
||||
- 非同期Camelメッセージパブリッシング
|
||||
|
||||
## Custom Logging Context Pattern (Logback)
|
||||
## カスタムロギングコンテキストパターン(Logback)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -112,14 +110,14 @@ 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");
|
||||
@@ -131,7 +129,7 @@ public class ProcessingService {
|
||||
}
|
||||
```
|
||||
|
||||
**Logback Configuration (logback.xml):**
|
||||
**Logback構成(logback.xml):**
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
@@ -149,7 +147,7 @@ public class ProcessingService {
|
||||
</configuration>
|
||||
```
|
||||
|
||||
## Event Service Pattern
|
||||
## イベントサービスパターン
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -181,13 +179,13 @@ public class EventService {
|
||||
}
|
||||
|
||||
private String serializePayload(Object payload) {
|
||||
// JSON serialization
|
||||
// JSONシリアライゼーション
|
||||
return objectMapper.writeValueAsString(payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Message Publishing (RabbitMQ)
|
||||
## Camelメッセージパブリッシング(RabbitMQ)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -215,7 +213,7 @@ public class BusinessRulesPublisher {
|
||||
}
|
||||
```
|
||||
|
||||
**Camel Route Configuration:**
|
||||
**Camelルート構成:**
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -242,7 +240,7 @@ public class BusinessRulesRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Direct Routes (In-Memory)
|
||||
## Camel Directルート(インメモリ)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -250,13 +248,13 @@ 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}")
|
||||
@@ -278,7 +276,7 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## Camel File Processing
|
||||
## Camelファイル処理
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -308,7 +306,7 @@ public class FileMonitoringRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Bean Invocation
|
||||
## Camel Bean呼び出し
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -328,7 +326,7 @@ public class InvoiceRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## REST API Structure
|
||||
## REST API構造
|
||||
|
||||
```java
|
||||
@Path("/api/documents")
|
||||
@@ -367,7 +365,7 @@ public class DocumentResource {
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern (Panache Repository)
|
||||
## リポジトリパターン(Panache Repository)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -389,7 +387,7 @@ public class DocumentRepository implements PanacheRepository<Document> {
|
||||
}
|
||||
```
|
||||
|
||||
## Service Layer with Transactions
|
||||
## トランザクション付きサービスレイヤー
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -425,7 +423,7 @@ public class DocumentService {
|
||||
}
|
||||
```
|
||||
|
||||
## DTOs and Validation
|
||||
## DTOとバリデーション
|
||||
|
||||
```java
|
||||
public record CreateDocumentRequest(
|
||||
@@ -442,7 +440,7 @@ public record DocumentResponse(Long id, String referenceNumber, DocumentStatus s
|
||||
}
|
||||
```
|
||||
|
||||
## Exception Mapping
|
||||
## 例外マッピング
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -473,7 +471,7 @@ public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
||||
}
|
||||
```
|
||||
|
||||
## CompletableFuture Async Operations
|
||||
## CompletableFuture非同期操作
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -512,7 +510,7 @@ public class FileStorageService {
|
||||
}
|
||||
```
|
||||
|
||||
## Caching
|
||||
## キャッシング
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -533,7 +531,7 @@ public class DocumentCacheService {
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration as YAML
|
||||
## YAML構成
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
@@ -580,7 +578,7 @@ public class DocumentCacheService {
|
||||
username: ${RABBITMQ_USER}
|
||||
password: ${RABBITMQ_PASSWORD}
|
||||
|
||||
# Camel configuration
|
||||
# Camel構成
|
||||
camel:
|
||||
rabbitmq:
|
||||
queue:
|
||||
@@ -588,7 +586,7 @@ camel:
|
||||
invoice-processing: invoice-processing-queue
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
## ヘルスチェック
|
||||
|
||||
```java
|
||||
@Readiness
|
||||
@@ -626,7 +624,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies (Maven)
|
||||
## 依存関係(Maven)
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
@@ -657,7 +655,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Quarkus Core -->
|
||||
<!-- Quarkusコア -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
@@ -667,7 +665,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
<artifactId>quarkus-config-yaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Camel Extensions -->
|
||||
<!-- Camelエクステンション -->
|
||||
<dependency>
|
||||
<groupId>org.apache.camel.quarkus</groupId>
|
||||
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
||||
@@ -689,7 +687,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<!-- ロギング -->
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||
<artifactId>quarkus-logging-logback</artifactId>
|
||||
@@ -701,56 +699,56 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
</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
|
||||
### アーキテクチャ
|
||||
- コンストラクタインジェクション用にLombokの`@RequiredArgsConstructor`を使用
|
||||
- サービスレイヤーは薄く保ち、複雑なロジックは専門クラスに委譲
|
||||
- メッセージルーティングと統合パターンにCamelルートを使用
|
||||
- データアクセスにはPanache Repositoryパターンを優先
|
||||
|
||||
### 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()`
|
||||
### イベント駆動
|
||||
- 常にEventServiceで操作をトラッキング(成功/エラーイベント)
|
||||
- インメモリルーティングにCamelの`direct:`エンドポイントを使用
|
||||
- RabbitMQ統合に`spring-rabbitmq`コンポーネントを使用
|
||||
- `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
|
||||
### ロギング
|
||||
- 構造化ロギング用にLogstashエンコーダー付きLogbackを使用
|
||||
- `SafeAutoCloseable`でサービスコール間でLogContextを伝播
|
||||
- リクエストトレーシング用にLogContextにコンテキスト情報を追加
|
||||
- 手動ロガーインスタンス化の代わりに`@Slf4j`を使用
|
||||
|
||||
### 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
|
||||
### 非同期操作
|
||||
- ノンブロッキングI/O操作にCompletableFutureを使用
|
||||
- 完了を待つ必要がある場合は`.join()`を呼び出す
|
||||
- CompletableFutureからの例外を適切にハンドリング
|
||||
- トレーシング用に非同期操作にLogContextを渡す
|
||||
|
||||
### 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
|
||||
### 構成
|
||||
- YAML構成を使用(`quarkus-config-yaml`)
|
||||
- dev/test/prod環境のプロファイル対応構成
|
||||
- 機密構成を環境変数に外部化
|
||||
- 型安全な構成インジェクション用に`@ConfigProperty`を使用
|
||||
|
||||
### Validation
|
||||
- Validate at resource layer with `@Valid`
|
||||
- Use Bean Validation annotations on DTOs
|
||||
- Map exceptions to proper HTTP responses with `@Provider`
|
||||
### バリデーション
|
||||
- リソースレイヤーで`@Valid`によるバリデーション
|
||||
- DTOにBean Validationアノテーションを使用
|
||||
- `@Provider`で例外を適切なHTTPレスポンスにマッピング
|
||||
|
||||
### Transactions
|
||||
- Use `@Transactional` on service methods that modify data
|
||||
- Keep transactions short and focused
|
||||
- Avoid calling async operations within transactions
|
||||
### トランザクション
|
||||
- データを変更するサービスメソッドに`@Transactional`を使用
|
||||
- トランザクションは短く焦点を絞る
|
||||
- トランザクション内で非同期操作を呼び出さない
|
||||
|
||||
### Testing
|
||||
- Use `camel-quarkus-junit5` for route testing
|
||||
- Use AssertJ for assertions
|
||||
- Mock all external dependencies
|
||||
- Test conditional flow logic thoroughly
|
||||
### テスト
|
||||
- ルートテストに`camel-quarkus-junit5`を使用
|
||||
- アサーションにAssertJを使用
|
||||
- すべての外部依存関係をモック
|
||||
- 条件付きフローロジックを徹底的にテスト
|
||||
|
||||
### 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
|
||||
### Quarkus固有
|
||||
- 最新のLTSバージョン(3.x)を維持
|
||||
- ホットリロード用にQuarkus devモードを使用
|
||||
- 本番準備のためにヘルスチェックを追加
|
||||
- ネイティブコンパイル互換性を定期的にテスト
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
---
|
||||
name: quarkus-security
|
||||
description: Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
|
||||
description: Quarkusセキュリティのベストプラクティス:認証、認可、JWT/OIDC、RBAC、入力バリデーション、CSRF、シークレット管理、依存関係セキュリティ。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Note / 注意**: このファイルはまだ日本語に翻訳されていません。現在は英語の原文です。翻訳PRを歓迎します。
|
||||
# Quarkus セキュリティレビュー
|
||||
|
||||
# Quarkus Security Review
|
||||
認証、認可、入力バリデーションによるQuarkusアプリケーションのセキュリティベストプラクティス。
|
||||
|
||||
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
|
||||
## いつアクティブにするか
|
||||
|
||||
## When to Activate
|
||||
- 認証の追加(JWT、OIDC、Basic Auth)
|
||||
- @RolesAllowedまたはSecurityIdentityによる認可の実装
|
||||
- ユーザー入力のバリデーション(Bean Validation、カスタムバリデータ)
|
||||
- CORSまたはセキュリティヘッダーの構成
|
||||
- シークレット管理(Vault、環境変数、構成ソース)
|
||||
- レート制限またはブルートフォース保護の追加
|
||||
- 依存関係のCVEスキャン
|
||||
- MicroProfile JWTまたはSmallRye JWTの使用
|
||||
|
||||
- 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
|
||||
### JWT認証
|
||||
|
||||
```java
|
||||
// Resource protected with JWT
|
||||
@Path("/api/protected")
|
||||
@Authenticated
|
||||
public class ProtectedResource {
|
||||
@@ -50,7 +47,7 @@ public class ProtectedResource {
|
||||
}
|
||||
```
|
||||
|
||||
Configuration (application.properties):
|
||||
構成(application.properties):
|
||||
```properties
|
||||
mp.jwt.verify.publickey.location=publicKey.pem
|
||||
mp.jwt.verify.issuer=https://auth.example.com
|
||||
@@ -61,7 +58,7 @@ quarkus.oidc.client-id=backend-service
|
||||
quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
||||
```
|
||||
|
||||
### Custom Authentication Filter
|
||||
### カスタム認証フィルター
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -77,7 +74,6 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -85,15 +81,15 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
||||
}
|
||||
|
||||
private boolean validateToken(String token) {
|
||||
// Token validation logic
|
||||
// トークンバリデーションロジック
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authorization
|
||||
## 認可
|
||||
|
||||
### Role-Based Access Control
|
||||
### ロールベースアクセス制御
|
||||
|
||||
```java
|
||||
@Path("/api/admin")
|
||||
@@ -125,7 +121,7 @@ public class UserResource {
|
||||
@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();
|
||||
@@ -139,7 +135,7 @@ public class UserResource {
|
||||
}
|
||||
```
|
||||
|
||||
### Programmatic Security
|
||||
### プログラマティックセキュリティ
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -163,18 +159,18 @@ public class SecurityService {
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
## 入力バリデーション
|
||||
|
||||
### Bean Validation
|
||||
|
||||
```java
|
||||
// BAD: No validation
|
||||
// BAD: バリデーションなし
|
||||
@POST
|
||||
public Response createUser(UserDto dto) {
|
||||
return Response.ok(userService.create(dto)).build();
|
||||
}
|
||||
|
||||
// GOOD: Validated DTO
|
||||
// GOOD: バリデーション付きDTO
|
||||
public record CreateUserDto(
|
||||
@NotBlank @Size(max = 100) String name,
|
||||
@NotBlank @Email String email,
|
||||
@@ -190,7 +186,7 @@ public Response createUser(@Valid CreateUserDto dto) {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Validators
|
||||
### カスタムバリデータ
|
||||
|
||||
```java
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@@ -209,36 +205,30 @@ public class UsernameValidator implements ConstraintValidator<ValidUsername, Str
|
||||
return value.matches("^[a-zA-Z0-9_-]{3,20}$");
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
public record CreateUserDto(
|
||||
@ValidUsername String username,
|
||||
@NotBlank @Email String email
|
||||
) {}
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
## SQLインジェクション防止
|
||||
|
||||
### Panache Active Record (Safe by Default)
|
||||
### Panache Active Record(デフォルトで安全)
|
||||
|
||||
```java
|
||||
// GOOD: Parameterized queries with Panache
|
||||
// GOOD: Panacheによるパラメータ化クエリ
|
||||
List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
||||
|
||||
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||
|
||||
// GOOD: Named parameters
|
||||
// GOOD: 名前付きパラメータ
|
||||
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
|
||||
// BAD: 文字列連結
|
||||
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||
|
||||
// GOOD: Parameterized native query
|
||||
// GOOD: パラメータ化ネイティブクエリ
|
||||
@Entity
|
||||
public class User extends PanacheEntity {
|
||||
public static List<User> findByEmailNative(String email) {
|
||||
@@ -250,7 +240,7 @@ public class User extends PanacheEntity {
|
||||
}
|
||||
```
|
||||
|
||||
## Password Hashing
|
||||
## パスワードハッシュ
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -264,33 +254,9 @@ public class PasswordService {
|
||||
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
|
||||
## CORS構成
|
||||
|
||||
```properties
|
||||
# application.properties
|
||||
@@ -303,37 +269,22 @@ quarkus.http.cors.access-control-max-age=24H
|
||||
quarkus.http.cors.access-control-allow-credentials=true
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
## シークレット管理
|
||||
|
||||
```properties
|
||||
# application.properties - NO SECRETS HERE
|
||||
# application.properties — ここにシークレットを置かない
|
||||
|
||||
# Use environment variables
|
||||
# 環境変数を使用
|
||||
quarkus.datasource.username=${DB_USER}
|
||||
quarkus.datasource.password=${DB_PASSWORD}
|
||||
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
||||
|
||||
# Or use Vault
|
||||
# または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
|
||||
@@ -344,7 +295,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
String clientId = getClientIdentifier(requestContext);
|
||||
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||
k -> RateLimiter.create(100.0)); // 100 requests per second
|
||||
k -> RateLimiter.create(100.0)); // 1秒あたり100リクエスト
|
||||
|
||||
if (!limiter.tryAcquire()) {
|
||||
requestContext.abortWith(
|
||||
@@ -356,13 +307,13 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
||||
}
|
||||
|
||||
private String getClientIdentifier(ContainerRequestContext ctx) {
|
||||
// Use IP, API key, or user ID
|
||||
// IP、APIキー、またはユーザーIDを使用
|
||||
return ctx.getHeaderString("X-Forwarded-For");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Headers
|
||||
## セキュリティヘッダー
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -372,10 +323,10 @@ public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||
|
||||
// Prevent clickjacking
|
||||
// クリックジャッキング防止
|
||||
headers.putSingle("X-Frame-Options", "DENY");
|
||||
|
||||
// XSS protection
|
||||
// XSS保護
|
||||
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||
|
||||
@@ -389,7 +340,7 @@ public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||
}
|
||||
```
|
||||
|
||||
## Audit Logging
|
||||
## 監査ロギング
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -408,23 +359,9 @@ public class AuditService {
|
||||
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
|
||||
@@ -433,23 +370,23 @@ mvn org.owasp:dependency-check-maven:check
|
||||
# Gradle
|
||||
./gradlew dependencyCheckAnalyze
|
||||
|
||||
# Check Quarkus extensions
|
||||
# Quarkusエクステンション確認
|
||||
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
|
||||
- 本番環境では常にHTTPSを使用
|
||||
- ステートレス認証にJWTまたはOIDCを有効化
|
||||
- 宣言的認可に`@RolesAllowed`を使用
|
||||
- Bean Validationですべての入力をバリデーション
|
||||
- BCryptでパスワードをハッシュ(平文禁止)
|
||||
- シークレットはVaultまたは環境変数に保存
|
||||
- SQLインジェクション防止にパラメータ化クエリを使用
|
||||
- すべてのレスポンスにセキュリティヘッダーを追加
|
||||
- パブリックエンドポイントにレート制限を実装
|
||||
- 機密操作を監査
|
||||
- 依存関係を最新に保ちCVEをスキャン
|
||||
- プログラマティックチェックにSecurityIdentityを使用
|
||||
- 適切なCORSポリシーを設定
|
||||
- 認証・認可パスをテスト
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
---
|
||||
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.
|
||||
description: Quarkus 3.x LTS向けテスト駆動開発。JUnit 5、Mockito、REST Assured、Camelテスト、JaCoCoを使用。機能追加、バグ修正、イベント駆動サービスのリファクタリングに使用。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Note / 注意**: このファイルはまだ日本語に翻訳されていません。現在は英語の原文です。翻訳PRを歓迎します。
|
||||
# Quarkus TDDワークフロー
|
||||
|
||||
# Quarkus TDD Workflow
|
||||
80%以上のカバレッジ(ユニット+統合)を目指すQuarkus 3.xサービスのTDDガイダンス。Apache Camelによるイベント駆動アーキテクチャに最適化。
|
||||
|
||||
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
|
||||
## いつ使用するか
|
||||
|
||||
## When to Use
|
||||
- 新機能またはRESTエンドポイント
|
||||
- バグ修正またはリファクタリング
|
||||
- データアクセスロジック、セキュリティルール、またはリアクティブストリームの追加
|
||||
- Apache Camelルートとイベントハンドラーのテスト
|
||||
- RabbitMQによるイベント駆動サービスのテスト
|
||||
- 条件付きフローロジックのテスト
|
||||
- CompletableFuture非同期操作のバリデーション
|
||||
- LogContext伝播のテスト
|
||||
|
||||
- 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. まずテストを書く(失敗するはず)
|
||||
2. テストを通過する最小限のコードを実装
|
||||
3. テストがグリーンの状態でリファクタリング
|
||||
4. JaCoCoでカバレッジを強制(80%以上が目標)
|
||||
|
||||
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)
|
||||
## @Nestedによるユニットテスト構成
|
||||
|
||||
## Unit Tests with @Nested Organization
|
||||
|
||||
Follow this structured approach for comprehensive, readable tests:
|
||||
包括的で読みやすいテストのための構造化アプローチ:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -62,7 +60,7 @@ class As2ProcessingServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// ARRANGE - Common test data
|
||||
// ARRANGE - 共通テストデータ
|
||||
testFilePath = Path.of("/tmp/test-invoice.xml");
|
||||
|
||||
testLogContext = new LogContext();
|
||||
@@ -119,48 +117,9 @@ class As2ProcessingServiceTest {
|
||||
|
||||
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 {
|
||||
@@ -174,7 +133,7 @@ class As2ProcessingServiceTest {
|
||||
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||
.thenReturn(FlowProfile.BASIC);
|
||||
|
||||
documentInfo.setPath(""); // Blank path triggers error
|
||||
documentInfo.setPath(""); // 空パスでエラーをトリガー
|
||||
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||
|
||||
@@ -187,71 +146,26 @@ class As2ProcessingServiceTest {
|
||||
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
|
||||
1. **@Nestedクラス**: テスト対象メソッドごとにテストをグループ化
|
||||
2. **@DisplayName**: テストレポート用の読みやすい説明
|
||||
3. **命名規則**: 明確さのために`givenX_whenY_thenZ`
|
||||
4. **AAAパターン**: 明示的な`// ARRANGE`、`// ACT`、`// ASSERT`コメント
|
||||
5. **@BeforeEach**: 重複削減のための共通テストデータセットアップ
|
||||
6. **assertDoesNotThrow**: 例外をキャッチせずに成功シナリオをテスト
|
||||
7. **assertThrows**: メッセージバリデーション付きの例外シナリオテスト
|
||||
8. **包括的カバレッジ**: ハッピーパス、null入力、エッジケース、例外をテスト
|
||||
9. **インタラクション検証**: Mockitoの`verify()`でメソッドが正しく呼ばれることを確認
|
||||
10. **Never検証**: エラーシナリオでメソッドが呼ばれないことを`never()`で確認
|
||||
|
||||
## Testing Camel Routes
|
||||
## Camelルートのテスト
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -267,338 +181,30 @@ class BusinessRulesRouteTest {
|
||||
@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() {
|
||||
@Test
|
||||
@DisplayName("Should successfully publish message to RabbitMQ")
|
||||
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
|
||||
// ARRANGE
|
||||
testPayload = new BusinessRulesPayload();
|
||||
testPayload.setDocumentId(1L);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for createSuccessEvent")
|
||||
class CreateSuccessEvent {
|
||||
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||
mockRabbitMQ.expectedMessageCount(1);
|
||||
|
||||
@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 {
|
||||
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");
|
||||
|
||||
@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");
|
||||
}
|
||||
// ACT
|
||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||
|
||||
// ASSERT
|
||||
mockRabbitMQ.assertIsSatisfied(5000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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)
|
||||
## リソースレイヤーテスト(REST Assured)
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -608,27 +214,6 @@ 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 {
|
||||
@@ -654,14 +239,12 @@ class DocumentResourceTest {
|
||||
.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("""
|
||||
@@ -675,58 +258,12 @@ class DocumentResourceTest {
|
||||
.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
|
||||
## JaCoCoカバレッジ
|
||||
|
||||
```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)
|
||||
### Maven構成
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
@@ -734,29 +271,18 @@ class DocumentIntegrationTest {
|
||||
<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>
|
||||
<goals><goal>prepare-agent</goal></goals>
|
||||
</execution>
|
||||
|
||||
<!-- Generate coverage report -->
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<goals><goal>report</goal></goals>
|
||||
</execution>
|
||||
|
||||
<!-- Enforce coverage thresholds -->
|
||||
<execution>
|
||||
<id>check</id>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<goals><goal>check</goal></goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
@@ -767,11 +293,6 @@ class DocumentIntegrationTest {
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.80</minimum>
|
||||
</limit>
|
||||
<limit>
|
||||
<counter>BRANCH</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.70</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
@@ -781,20 +302,19 @@ class DocumentIntegrationTest {
|
||||
</plugin>
|
||||
```
|
||||
|
||||
Run tests with coverage:
|
||||
カバレッジ付きテスト実行:
|
||||
```bash
|
||||
mvn clean test
|
||||
mvn jacoco:report
|
||||
mvn jacoco:check
|
||||
|
||||
# Report at: target/site/jacoco/index.html
|
||||
# レポート: target/site/jacoco/index.html
|
||||
```
|
||||
|
||||
## Test Dependencies
|
||||
## テスト依存関係
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Quarkus Testing -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
@@ -805,30 +325,17 @@ mvn jacoco:check
|
||||
<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>
|
||||
@@ -837,74 +344,33 @@ mvn jacoco:check
|
||||
</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
|
||||
### テスト構成
|
||||
- テスト対象メソッドごとに`@Nested`クラスでグループ化
|
||||
- レポートに表示される読みやすい説明に`@DisplayName`を使用
|
||||
- テストメソッドに`givenX_whenY_thenZ`命名規則を使用
|
||||
|
||||
### 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()`
|
||||
### テスト構造
|
||||
- 明示的コメント付きAAAパターン(`// ARRANGE`、`// ACT`、`// ASSERT`)
|
||||
- 成功シナリオに`assertDoesNotThrow`を使用
|
||||
- メッセージバリデーション付き例外シナリオに`assertThrows`を使用
|
||||
|
||||
### 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
|
||||
### アサーション
|
||||
- JUnitアサーションの代わりに**常にAssertJ**(`assertThat`)を使用
|
||||
- 読みやすさのためにAssertJのfluent APIを使用
|
||||
- 例外: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
|
||||
|
||||
### 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()`
|
||||
### イベント駆動テスト
|
||||
- `AdviceWith`と`MockEndpoint`でCamelルートをテスト
|
||||
- メッセージコンテンツ、ヘッダー、ルーティングロジックを検証
|
||||
- エラーハンドリングルートを個別にテスト
|
||||
- ユニットテストで外部システム(RabbitMQ、S3、データベース)をモック
|
||||
|
||||
### 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
|
||||
### Quarkus固有
|
||||
- 最新のLTSバージョン(Quarkus 3.x)を維持
|
||||
- ネイティブコンパイル互換性を定期的にテスト
|
||||
- 異なるシナリオにQuarkusテストプロファイルを使用
|
||||
- `@MockBean`の代わりに`@InjectMock`を使用(Quarkus固有)
|
||||
|
||||
### 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
|
||||
**覚えておいてください**: テストは高速、分離、決定的に保ちます。実装の詳細ではなく動作をテストしてください。
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
---
|
||||
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."
|
||||
description: "Quarkusプロジェクトの検証ループ: ビルド、静的解析、カバレッジ付きテスト、セキュリティスキャン、ネイティブコンパイル、リリースまたはPR前のdiffレビュー。"
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Note / 注意**: このファイルはまだ日本語に翻訳されていません。現在は英語の原文です。翻訳PRを歓迎します。
|
||||
# Quarkus 検証ループ
|
||||
|
||||
# Quarkus Verification Loop
|
||||
PR前、大きな変更後、デプロイ前に実行。
|
||||
|
||||
Run before PRs, after major changes, and pre-deploy.
|
||||
## いつアクティブにするか
|
||||
|
||||
## When to Activate
|
||||
- Quarkusサービスのプルリクエストを開く前
|
||||
- 大規模なリファクタリングまたは依存関係アップグレード後
|
||||
- ステージングまたは本番のデプロイ前検証
|
||||
- 完全なビルド → lint → テスト → セキュリティスキャン → ネイティブコンパイルパイプラインの実行
|
||||
- テストカバレッジが閾値を満たしていることの検証(80%以上)
|
||||
- ネイティブイメージ互換性のテスト
|
||||
|
||||
- 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
|
||||
## フェーズ1: ビルド
|
||||
|
||||
```bash
|
||||
# Maven
|
||||
@@ -29,17 +27,17 @@ mvn clean verify -DskipTests
|
||||
./gradlew clean assemble -x test
|
||||
```
|
||||
|
||||
If build fails, stop and fix compilation errors.
|
||||
ビルドが失敗した場合、停止してコンパイルエラーを修正。
|
||||
|
||||
## Phase 2: Static Analysis
|
||||
## フェーズ2: 静的解析
|
||||
|
||||
### Checkstyle, PMD, SpotBugs (Maven)
|
||||
### Checkstyle、PMD、SpotBugs(Maven)
|
||||
|
||||
```bash
|
||||
mvn checkstyle:check pmd:check spotbugs:check
|
||||
```
|
||||
|
||||
### SonarQube (if configured)
|
||||
### SonarQube(構成されている場合)
|
||||
|
||||
```bash
|
||||
mvn sonar:sonar \
|
||||
@@ -48,33 +46,33 @@ mvn sonar:sonar \
|
||||
-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
|
||||
- 未使用のimportまたは変数
|
||||
- 複雑なメソッド(高い循環的複雑度)
|
||||
- 潜在的なnullポインター参照
|
||||
- SpotBugsが検出したセキュリティ問題
|
||||
|
||||
## Phase 3: Tests + Coverage
|
||||
## フェーズ3: テスト + カバレッジ
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
# すべてのテストを実行
|
||||
mvn clean test
|
||||
|
||||
# Generate coverage report
|
||||
# カバレッジレポートを生成
|
||||
mvn jacoco:report
|
||||
|
||||
# Enforce coverage threshold (80%)
|
||||
# カバレッジ閾値を強制(80%)
|
||||
mvn jacoco:check
|
||||
|
||||
# Or with Gradle
|
||||
# またはGradleで
|
||||
./gradlew test jacocoTestReport jacocoTestCoverageVerification
|
||||
```
|
||||
|
||||
### Test Categories
|
||||
### テストカテゴリ
|
||||
|
||||
#### Unit Tests
|
||||
Test service logic with mocked dependencies:
|
||||
#### ユニットテスト
|
||||
モック化された依存関係でサービスロジックをテスト:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -99,8 +97,8 @@ class UserServiceTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### Integration Tests
|
||||
Test with real database (Testcontainers):
|
||||
#### 統合テスト
|
||||
実データベース(Testcontainers)でテスト:
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -126,8 +124,8 @@ class UserRepositoryIntegrationTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### API Tests
|
||||
Test REST endpoints with REST Assured:
|
||||
#### APIテスト
|
||||
REST AssuredでRESTエンドポイントをテスト:
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -160,124 +158,77 @@ class UserResourceTest {
|
||||
}
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
### カバレッジレポート
|
||||
|
||||
Check `target/site/jacoco/index.html` for detailed coverage:
|
||||
- Overall line coverage (target: 80%+)
|
||||
- Branch coverage (target: 70%+)
|
||||
- Identify uncovered critical paths
|
||||
詳細カバレッジは`target/site/jacoco/index.html`を確認:
|
||||
- 全体行カバレッジ(目標: 80%以上)
|
||||
- ブランチカバレッジ(目標: 70%以上)
|
||||
- カバーされていない重要パスを特定
|
||||
|
||||
## Phase 4: Security Scanning
|
||||
## フェーズ4: セキュリティスキャン
|
||||
|
||||
### Dependency Vulnerabilities (Maven)
|
||||
### 依存関係脆弱性(Maven)
|
||||
|
||||
```bash
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
```
|
||||
|
||||
Review `target/dependency-check-report.html` for CVEs.
|
||||
CVEについて`target/dependency-check-report.html`を確認。
|
||||
|
||||
### Quarkus Security Audit
|
||||
### Quarkusセキュリティ監査
|
||||
|
||||
```bash
|
||||
# Check vulnerable extensions
|
||||
# 脆弱なエクステンションを確認
|
||||
mvn quarkus:audit
|
||||
|
||||
# List all extensions
|
||||
# すべてのエクステンションをリスト
|
||||
mvn quarkus:list-extensions
|
||||
```
|
||||
|
||||
### OWASP ZAP (API Security Testing)
|
||||
### 一般的なセキュリティチェック
|
||||
|
||||
- [ ] すべてのシークレットが環境変数に(コード内ではなく)
|
||||
- [ ] すべてのエンドポイントで入力バリデーション
|
||||
- [ ] 認証/認可が構成済み
|
||||
- [ ] CORSが適切に構成済み
|
||||
- [ ] セキュリティヘッダーが設定済み
|
||||
- [ ] パスワードがBCryptでハッシュ済み
|
||||
- [ ] SQLインジェクション保護(パラメータ化クエリ)
|
||||
- [ ] パブリックエンドポイントでレート制限
|
||||
|
||||
## フェーズ5: ネイティブコンパイル
|
||||
|
||||
GraalVMネイティブイメージ互換性をテスト:
|
||||
|
||||
```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
|
||||
一般的な問題:
|
||||
- **Reflection**: 動的クラス用のreflection構成を追加
|
||||
- **Resources**: `quarkus.native.resources.includes`でリソースを含める
|
||||
- **JNI**: ネイティブライブラリ使用時にJNIクラスを登録
|
||||
|
||||
Example reflection config:
|
||||
例のreflection構成:
|
||||
```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
|
||||
## フェーズ6: ヘルスチェック
|
||||
|
||||
```bash
|
||||
# Liveness
|
||||
@@ -286,198 +237,78 @@ 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
|
||||
### コード品質
|
||||
- [ ] ビルドが警告なしで通過
|
||||
- [ ] 静的解析クリーン(高/中の問題なし)
|
||||
- [ ] コードがチーム規約に従う
|
||||
- [ ] PRにコメントアウトされたコードやTODOがない
|
||||
|
||||
```bash
|
||||
# Build container image
|
||||
mvn package -Dquarkus.container-image.build=true
|
||||
### テスト
|
||||
- [ ] すべてのテストが通過
|
||||
- [ ] コードカバレッジ ≥ 80%
|
||||
- [ ] 実データベースとの統合テスト
|
||||
- [ ] セキュリティテストが通過
|
||||
- [ ] パフォーマンスが許容範囲内
|
||||
|
||||
# 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 ==="
|
||||
echo "=== フェーズ1: ビルド ==="
|
||||
mvn clean verify -DskipTests
|
||||
|
||||
echo "=== Phase 2: Static Analysis ==="
|
||||
echo "=== フェーズ2: 静的解析 ==="
|
||||
mvn checkstyle:check pmd:check spotbugs:check
|
||||
|
||||
echo "=== Phase 3: Tests + Coverage ==="
|
||||
echo "=== フェーズ3: テスト + カバレッジ ==="
|
||||
mvn test jacoco:report jacoco:check
|
||||
|
||||
echo "=== Phase 4: Security Scan ==="
|
||||
echo "=== フェーズ4: セキュリティスキャン ==="
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
|
||||
echo "=== Phase 5: Native Compilation ==="
|
||||
echo "=== フェーズ5: ネイティブコンパイル ==="
|
||||
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"
|
||||
echo "=== 全フェーズ完了 ==="
|
||||
echo "レポートを確認:"
|
||||
echo " - カバレッジ: target/site/jacoco/index.html"
|
||||
echo " - セキュリティ: target/dependency-check-report.html"
|
||||
echo " - ネイティブ: 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
|
||||
- すべてのPR前に検証ループを実行
|
||||
- CI/CDパイプラインで自動化
|
||||
- 問題を即座に修正し、技術的負債を蓄積しない
|
||||
- カバレッジを80%以上に維持
|
||||
- 依存関係を定期的に更新
|
||||
- ネイティブコンパイルを定期的にテスト
|
||||
- パフォーマンストレンドを監視
|
||||
- 破壊的変更を文書化
|
||||
- セキュリティスキャン結果をレビュー
|
||||
- 各環境の構成を検証
|
||||
|
||||
@@ -4,26 +4,24 @@ description: Quarkus 3.x LTS architecture patterns with Camel for messaging, RES
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Not / Note**: Bu dosya henuz Turkceye cevrilmemistir, su anda Ingilizce orijinaldir. Ceviri PR'lari memnuniyetle karsilanir.
|
||||
# Quarkus Geliştirme Desenleri
|
||||
|
||||
# Quarkus Development Patterns
|
||||
Apache Camel ile bulut-native, event-driven servisler için Quarkus 3.x mimari ve API desenleri.
|
||||
|
||||
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
|
||||
## Ne Zaman Aktif Edilir
|
||||
|
||||
## When to Activate
|
||||
- JAX-RS veya RESTEasy Reactive ile REST API'leri oluşturma
|
||||
- Resource → service → repository katmanlarını yapılandırma
|
||||
- Apache Camel ve RabbitMQ ile event-driven desenler uygulama
|
||||
- Hibernate Panache, caching veya reaktif akışları yapılandırma
|
||||
- Validation, exception mapping veya sayfalama ekleme
|
||||
- Dev/staging/production ortamları için profiller kurma (YAML yapılandırma)
|
||||
- LogContext ve Logback/Logstash encoder ile özel loglama
|
||||
- Async işlemler için CompletableFuture ile çalışma
|
||||
- Koşullu akış işleme uygulama
|
||||
- GraalVM native derleme ile çalışma
|
||||
|
||||
- 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)
|
||||
## Birden Fazla Bağımlılıklı Service Katmanı (Lombok)
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
@@ -43,7 +41,7 @@ public class As2ProcessingService {
|
||||
|
||||
String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID);
|
||||
|
||||
// Conditional flow logic
|
||||
// Koşullu akış mantığı
|
||||
boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW));
|
||||
log.info("Is CHORUS_FLOW message: {}", isChorusFlow);
|
||||
|
||||
@@ -62,7 +60,7 @@ public class As2ProcessingService {
|
||||
|
||||
log.info("Invoice validation completed. Message is valid");
|
||||
|
||||
// CompletableFuture async operation
|
||||
// CompletableFuture async işlemi
|
||||
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
||||
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
||||
fileStorageService.uploadOriginalFile(inputStream,
|
||||
@@ -85,7 +83,7 @@ public class As2ProcessingService {
|
||||
documentInfo, originalFileName, structureIdPartner,
|
||||
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||
|
||||
// Async Camel publishing
|
||||
// Async Camel yayınlama
|
||||
businessRulesPublisher.publishAsync(payload);
|
||||
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||
}
|
||||
@@ -94,16 +92,16 @@ public class As2ProcessingService {
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
**Temel Desenler:**
|
||||
- Constructor injection için Lombok üzerinden `@RequiredArgsConstructor`
|
||||
- Logback loglama için `@Slf4j`
|
||||
- try-with-resources ile kapsamlı LogContext
|
||||
- Runtime parametrelerine dayalı koşullu akış mantığı
|
||||
- Async işlemler için `.join()` ile CompletableFuture
|
||||
- Başarı/hata senaryoları için event takibi
|
||||
- Async Camel mesaj yayınlama
|
||||
|
||||
## Custom Logging Context Pattern (Logback)
|
||||
## Özel Loglama Bağlamı Deseni (Logback)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -112,14 +110,14 @@ public class ProcessingService {
|
||||
public void processDocument(Document doc) {
|
||||
LogContext logContext = CustomLog.getCurrentContext();
|
||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||
// Add context to all log statements
|
||||
// Tüm log ifadelerine bağlam ekle
|
||||
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
|
||||
// Bu kapsam içindeki tüm loglar bağlamı devralır
|
||||
processInternal(doc);
|
||||
|
||||
log.info("Document processing completed");
|
||||
@@ -131,7 +129,7 @@ public class ProcessingService {
|
||||
}
|
||||
```
|
||||
|
||||
**Logback Configuration (logback.xml):**
|
||||
**Logback Yapılandırması (logback.xml):**
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
@@ -149,7 +147,7 @@ public class ProcessingService {
|
||||
</configuration>
|
||||
```
|
||||
|
||||
## Event Service Pattern
|
||||
## Event Service Deseni
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -181,13 +179,13 @@ public class EventService {
|
||||
}
|
||||
|
||||
private String serializePayload(Object payload) {
|
||||
// JSON serialization
|
||||
// JSON serileştirme
|
||||
return objectMapper.writeValueAsString(payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Message Publishing (RabbitMQ)
|
||||
## Camel Mesaj Yayınlama (RabbitMQ)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -215,7 +213,7 @@ public class BusinessRulesPublisher {
|
||||
}
|
||||
```
|
||||
|
||||
**Camel Route Configuration:**
|
||||
**Camel Route Yapılandırması:**
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -242,7 +240,7 @@ public class BusinessRulesRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Direct Routes (In-Memory)
|
||||
## Camel Direct Route'ları (Bellek İçi)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -250,13 +248,13 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
||||
|
||||
@Override
|
||||
public void configure() {
|
||||
// Error handling
|
||||
// Hata yönetimi
|
||||
onException(ValidationException.class)
|
||||
.handled(true)
|
||||
.to("direct:validation-error-handler")
|
||||
.log("Validation error: ${exception.message}");
|
||||
|
||||
// Main processing route
|
||||
// Ana işleme route'u
|
||||
from("direct:process-document")
|
||||
.routeId("document-processing")
|
||||
.log("Processing document: ${header.documentId}")
|
||||
@@ -278,7 +276,7 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## Camel File Processing
|
||||
## Camel Dosya İşleme
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -308,7 +306,7 @@ public class FileMonitoringRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Bean Invocation
|
||||
## Camel Bean Çağrısı
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -328,7 +326,7 @@ public class InvoiceRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## REST API Structure
|
||||
## REST API Yapısı
|
||||
|
||||
```java
|
||||
@Path("/api/documents")
|
||||
@@ -367,7 +365,7 @@ public class DocumentResource {
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern (Panache Repository)
|
||||
## Repository Deseni (Panache Repository)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -389,7 +387,7 @@ public class DocumentRepository implements PanacheRepository<Document> {
|
||||
}
|
||||
```
|
||||
|
||||
## Service Layer with Transactions
|
||||
## Transaction'lı Service Katmanı
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -425,7 +423,7 @@ public class DocumentService {
|
||||
}
|
||||
```
|
||||
|
||||
## DTOs and Validation
|
||||
## DTO'lar ve Validation
|
||||
|
||||
```java
|
||||
public record CreateDocumentRequest(
|
||||
@@ -442,7 +440,7 @@ public record DocumentResponse(Long id, String referenceNumber, DocumentStatus s
|
||||
}
|
||||
```
|
||||
|
||||
## Exception Mapping
|
||||
## Exception Eşleme
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -473,7 +471,7 @@ public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
||||
}
|
||||
```
|
||||
|
||||
## CompletableFuture Async Operations
|
||||
## CompletableFuture Async İşlemleri
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -533,10 +531,10 @@ public class DocumentCacheService {
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration as YAML
|
||||
## YAML Yapılandırması
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
# application.yml (uygulama yapılandırması)
|
||||
"%dev":
|
||||
quarkus:
|
||||
datasource:
|
||||
@@ -580,7 +578,7 @@ public class DocumentCacheService {
|
||||
username: ${RABBITMQ_USER}
|
||||
password: ${RABBITMQ_PASSWORD}
|
||||
|
||||
# Camel configuration
|
||||
# Camel yapılandırması
|
||||
camel:
|
||||
rabbitmq:
|
||||
queue:
|
||||
@@ -588,7 +586,7 @@ camel:
|
||||
invoice-processing: invoice-processing-queue
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
## Sağlık Kontrolleri
|
||||
|
||||
```java
|
||||
@Readiness
|
||||
@@ -626,7 +624,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies (Maven)
|
||||
## Bağımlılıklar (Maven)
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
@@ -657,7 +655,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Quarkus Core -->
|
||||
<!-- Quarkus Çekirdek -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
@@ -667,7 +665,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
<artifactId>quarkus-config-yaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Camel Extensions -->
|
||||
<!-- Camel Uzantıları -->
|
||||
<dependency>
|
||||
<groupId>org.apache.camel.quarkus</groupId>
|
||||
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
||||
@@ -689,7 +687,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<!-- Loglama -->
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||
<artifactId>quarkus-logging-logback</artifactId>
|
||||
@@ -701,56 +699,56 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
## En İyi Uygulamalar
|
||||
|
||||
### 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
|
||||
### Mimari
|
||||
- Constructor injection için Lombok üzerinden `@RequiredArgsConstructor` kullanın
|
||||
- Service katmanını ince tutun; karmaşık mantığı uzmanlaşmış sınıflara devredin
|
||||
- Mesaj yönlendirme ve entegrasyon desenleri için Camel route'larını kullanın
|
||||
- Veri erişimi için Panache Repository desenini tercih edin
|
||||
|
||||
### 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()`
|
||||
- EventService ile işlemleri her zaman takip edin (başarı/hata eventleri)
|
||||
- Bellek içi yönlendirme için Camel `direct:` endpoint'leri kullanın
|
||||
- RabbitMQ entegrasyonu için `spring-rabbitmq` bileşenini kullanın
|
||||
- `ProducerTemplate.asyncSendBody()` ile async yayınlama uygulayın
|
||||
|
||||
### 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
|
||||
### Loglama
|
||||
- Yapılandırılmış loglama için Logstash encoder ile Logback kullanın
|
||||
- LogContext'i `SafeAutoCloseable` ile servis çağrıları boyunca yayın
|
||||
- İstek takibi için LogContext'e bağlamsal bilgi ekleyin
|
||||
- Manuel logger oluşturma yerine `@Slf4j` kullanın
|
||||
|
||||
### 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
|
||||
### Async İşlemler
|
||||
- Bloklamayan I/O işlemleri için CompletableFuture kullanın
|
||||
- Tamamlanmayı beklemek gerektiğinde `.join()` çağırın
|
||||
- CompletableFuture'dan gelen exception'ları düzgün şekilde ele alın
|
||||
- Takip için async işlemlere LogContext geçirin
|
||||
|
||||
### 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
|
||||
### Yapılandırma
|
||||
- YAML yapılandırmasını kullanın (`quarkus-config-yaml`)
|
||||
- Dev/test/prod ortamları için profil-duyarlı yapılandırma
|
||||
- Hassas yapılandırmayı ortam değişkenlerine dışsallaştırın
|
||||
- Tip-güvenli yapılandırma injection için `@ConfigProperty` kullanın
|
||||
|
||||
### Validation
|
||||
- Validate at resource layer with `@Valid`
|
||||
- Use Bean Validation annotations on DTOs
|
||||
- Map exceptions to proper HTTP responses with `@Provider`
|
||||
- Resource katmanında `@Valid` ile doğrulayın
|
||||
- DTO'larda Bean Validation annotasyonları kullanın
|
||||
- Exception'ları `@Provider` ile uygun HTTP yanıtlarına eşleyin
|
||||
|
||||
### Transactions
|
||||
- Use `@Transactional` on service methods that modify data
|
||||
- Keep transactions short and focused
|
||||
- Avoid calling async operations within transactions
|
||||
### Transaction'lar
|
||||
- Veri değiştiren service metodlarında `@Transactional` kullanın
|
||||
- Transaction'ları kısa ve odaklı tutun
|
||||
- Transaction'lar içinden async işlem çağırmaktan kaçının
|
||||
|
||||
### Testing
|
||||
- Use `camel-quarkus-junit5` for route testing
|
||||
- Use AssertJ for assertions
|
||||
- Mock all external dependencies
|
||||
- Test conditional flow logic thoroughly
|
||||
### Test
|
||||
- Route testi için `camel-quarkus-junit5` kullanın
|
||||
- Assertion'lar için AssertJ kullanın
|
||||
- Tüm harici bağımlılıkları mock'layın
|
||||
- Koşullu akış mantığını kapsamlı biçimde test edin
|
||||
|
||||
### 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
|
||||
### Quarkus'a Özgü
|
||||
- En son LTS sürümünde kalın (3.x)
|
||||
- Hot reload için Quarkus dev modunu kullanın
|
||||
- Production hazırlığı için sağlık kontrolleri ekleyin
|
||||
- Native derleme uyumluluğunu periyodik olarak test edin
|
||||
|
||||
@@ -4,29 +4,27 @@ description: Quarkus Security best practices for authentication, authorization,
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Not / Note**: Bu dosya henuz Turkceye cevrilmemistir, su anda Ingilizce orijinaldir. Ceviri PR'lari memnuniyetle karsilanir.
|
||||
# Quarkus Güvenlik İncelemesi
|
||||
|
||||
# Quarkus Security Review
|
||||
Kimlik doğrulama, yetkilendirme ve girdi doğrulama ile Quarkus uygulamalarını güvenli hale getirmek için en iyi uygulamalar.
|
||||
|
||||
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
|
||||
## Ne Zaman Aktif Edilir
|
||||
|
||||
## When to Activate
|
||||
- Kimlik doğrulama ekleme (JWT, OIDC, Basic Auth)
|
||||
- `@RolesAllowed` veya SecurityIdentity ile yetkilendirme uygulama
|
||||
- Kullanıcı girişini doğrulama (Bean Validation, özel doğrulayıcılar)
|
||||
- CORS veya güvenlik başlıklarını yapılandırma
|
||||
- Gizli bilgileri yönetme (Vault, ortam değişkenleri, config kaynakları)
|
||||
- Rate limiting veya brute-force koruması ekleme
|
||||
- Bağımlılıkları CVE için tarama
|
||||
- MicroProfile JWT veya SmallRye JWT ile çalışma
|
||||
|
||||
- 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
|
||||
## Kimlik Doğrulama
|
||||
|
||||
## Authentication
|
||||
|
||||
### JWT Authentication
|
||||
### JWT Kimlik Doğrulama
|
||||
|
||||
```java
|
||||
// Resource protected with JWT
|
||||
// JWT ile korunan resource
|
||||
@Path("/api/protected")
|
||||
@Authenticated
|
||||
public class ProtectedResource {
|
||||
@@ -50,7 +48,7 @@ public class ProtectedResource {
|
||||
}
|
||||
```
|
||||
|
||||
Configuration (application.properties):
|
||||
Yapılandırma (application.properties):
|
||||
```properties
|
||||
mp.jwt.verify.publickey.location=publicKey.pem
|
||||
mp.jwt.verify.issuer=https://auth.example.com
|
||||
@@ -61,7 +59,7 @@ quarkus.oidc.client-id=backend-service
|
||||
quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
||||
```
|
||||
|
||||
### Custom Authentication Filter
|
||||
### Özel Kimlik Doğrulama Filtresi
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -77,7 +75,7 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
||||
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
// Validate token and set SecurityIdentity
|
||||
// Token'ı doğrula ve SecurityIdentity'yi ayarla
|
||||
if (!validateToken(token)) {
|
||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||
}
|
||||
@@ -85,15 +83,15 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
||||
}
|
||||
|
||||
private boolean validateToken(String token) {
|
||||
// Token validation logic
|
||||
// Token doğrulama mantığı
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authorization
|
||||
## Yetkilendirme
|
||||
|
||||
### Role-Based Access Control
|
||||
### Rol Tabanlı Erişim Kontrolü
|
||||
|
||||
```java
|
||||
@Path("/api/admin")
|
||||
@@ -125,7 +123,7 @@ public class UserResource {
|
||||
@Path("/{id}")
|
||||
@RolesAllowed("USER")
|
||||
public Response getUser(@PathParam("id") Long id) {
|
||||
// Check ownership
|
||||
// Sahipliği kontrol et
|
||||
if (!securityIdentity.hasRole("ADMIN") &&
|
||||
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||
return Response.status(Response.Status.FORBIDDEN).build();
|
||||
@@ -139,7 +137,7 @@ public class UserResource {
|
||||
}
|
||||
```
|
||||
|
||||
### Programmatic Security
|
||||
### Programatik Güvenlik
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -163,18 +161,18 @@ public class SecurityService {
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
## Girdi Doğrulama
|
||||
|
||||
### Bean Validation
|
||||
|
||||
```java
|
||||
// BAD: No validation
|
||||
// KÖTÜ: Validation yok
|
||||
@POST
|
||||
public Response createUser(UserDto dto) {
|
||||
return Response.ok(userService.create(dto)).build();
|
||||
}
|
||||
|
||||
// GOOD: Validated DTO
|
||||
// İYİ: Doğrulanmış DTO
|
||||
public record CreateUserDto(
|
||||
@NotBlank @Size(max = 100) String name,
|
||||
@NotBlank @Email String email,
|
||||
@@ -190,7 +188,7 @@ public Response createUser(@Valid CreateUserDto dto) {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Validators
|
||||
### Özel Doğrulayıcılar
|
||||
|
||||
```java
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@@ -210,35 +208,35 @@ public class UsernameValidator implements ConstraintValidator<ValidUsername, Str
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
// Kullanım
|
||||
public record CreateUserDto(
|
||||
@ValidUsername String username,
|
||||
@NotBlank @Email String email
|
||||
) {}
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
## SQL Injection Önleme
|
||||
|
||||
### Panache Active Record (Safe by Default)
|
||||
### Panache Active Record (Varsayılan Olarak Güvenli)
|
||||
|
||||
```java
|
||||
// GOOD: Parameterized queries with Panache
|
||||
// İYİ: Panache ile parametreli sorgular
|
||||
List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
||||
|
||||
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||
|
||||
// GOOD: Named parameters
|
||||
// İYİ: İsimlendirilmiş parametreler
|
||||
List<User> users = User.list("email = :email and age > :minAge",
|
||||
Parameters.with("email", email).and("minAge", 18));
|
||||
```
|
||||
|
||||
### Native Queries (Use Parameters)
|
||||
### Native Sorgular (Parametre Kullanın)
|
||||
|
||||
```java
|
||||
// BAD: String concatenation
|
||||
// KÖTÜ: String birleştirme
|
||||
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||
|
||||
// GOOD: Parameterized native query
|
||||
// İYİ: Parametreli native sorgu
|
||||
@Entity
|
||||
public class User extends PanacheEntity {
|
||||
public static List<User> findByEmailNative(String email) {
|
||||
@@ -250,7 +248,7 @@ public class User extends PanacheEntity {
|
||||
}
|
||||
```
|
||||
|
||||
## Password Hashing
|
||||
## Parola Hash'leme
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -265,7 +263,7 @@ public class PasswordService {
|
||||
}
|
||||
}
|
||||
|
||||
// In service
|
||||
// Servis içinde
|
||||
@ApplicationScoped
|
||||
public class UserService {
|
||||
@Inject
|
||||
@@ -290,7 +288,7 @@ public class UserService {
|
||||
}
|
||||
```
|
||||
|
||||
## CORS Configuration
|
||||
## CORS Yapılandırması
|
||||
|
||||
```properties
|
||||
# application.properties
|
||||
@@ -303,12 +301,12 @@ quarkus.http.cors.access-control-max-age=24H
|
||||
quarkus.http.cors.access-control-allow-credentials=true
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
## Gizli Bilgi Yönetimi
|
||||
|
||||
```properties
|
||||
# application.properties - NO SECRETS HERE
|
||||
# application.properties - BURADA GİZLİ BİLGİ YOK
|
||||
|
||||
# Use environment variables
|
||||
# Ortam değişkenlerini kullanın
|
||||
quarkus.datasource.username=${DB_USER}
|
||||
quarkus.datasource.password=${DB_PASSWORD}
|
||||
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
||||
@@ -318,14 +316,14 @@ quarkus.vault.url=https://vault.example.com
|
||||
quarkus.vault.authentication.kubernetes.role=my-role
|
||||
```
|
||||
|
||||
### HashiCorp Vault Integration
|
||||
### HashiCorp Vault Entegrasyonu
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class SecretService {
|
||||
|
||||
@ConfigProperty(name = "api-key")
|
||||
String apiKey; // Fetched from Vault
|
||||
String apiKey; // Vault'tan alınır
|
||||
|
||||
public String getSecret(String key) {
|
||||
return ConfigProvider.getConfig().getValue(key, String.class);
|
||||
@@ -333,7 +331,7 @@ public class SecretService {
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
## Rate Limiting (Hız Sınırlama)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -344,7 +342,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
String clientId = getClientIdentifier(requestContext);
|
||||
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||
k -> RateLimiter.create(100.0)); // 100 requests per second
|
||||
k -> RateLimiter.create(100.0)); // Saniyede 100 istek
|
||||
|
||||
if (!limiter.tryAcquire()) {
|
||||
requestContext.abortWith(
|
||||
@@ -356,13 +354,13 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
||||
}
|
||||
|
||||
private String getClientIdentifier(ContainerRequestContext ctx) {
|
||||
// Use IP, API key, or user ID
|
||||
// IP, API anahtarı veya kullanıcı ID'si kullanın
|
||||
return ctx.getHeaderString("X-Forwarded-For");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Headers
|
||||
## Güvenlik Başlıkları
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -372,10 +370,10 @@ public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||
|
||||
// Prevent clickjacking
|
||||
// Clickjacking'i önle
|
||||
headers.putSingle("X-Frame-Options", "DENY");
|
||||
|
||||
// XSS protection
|
||||
// XSS koruması
|
||||
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||
|
||||
@@ -389,7 +387,7 @@ public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||
}
|
||||
```
|
||||
|
||||
## Audit Logging
|
||||
## Denetim Loglama
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -409,7 +407,7 @@ public class AuditService {
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in resource
|
||||
// Resource içinde kullanım
|
||||
@Path("/api/sensitive")
|
||||
public class SensitiveResource {
|
||||
@Inject
|
||||
@@ -424,7 +422,7 @@ public class SensitiveResource {
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Security Scanning
|
||||
## Bağımlılık Güvenliği Taraması
|
||||
|
||||
```bash
|
||||
# Maven
|
||||
@@ -437,19 +435,19 @@ mvn org.owasp:dependency-check-maven:check
|
||||
quarkus extension list --installable
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
## En İyi Uygulamalar
|
||||
|
||||
- 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
|
||||
- Production'da her zaman HTTPS kullanın
|
||||
- Stateless kimlik doğrulama için JWT veya OIDC etkinleştirin
|
||||
- Bildirimsel yetkilendirme için `@RolesAllowed` kullanın
|
||||
- Bean Validation ile tüm girişleri doğrulayın
|
||||
- Parolaları BCrypt ile hash'leyin (asla düz metin saklamayın)
|
||||
- Gizli bilgileri Vault veya ortam değişkenlerinde saklayın
|
||||
- SQL injection'ı önlemek için parametreli sorgular kullanın
|
||||
- Tüm yanıtlara güvenlik başlıkları ekleyin
|
||||
- Genel endpoint'lerde rate limiting uygulayın
|
||||
- Hassas işlemleri denetleyin
|
||||
- Bağımlılıkları güncel tutun ve CVE için tarayın
|
||||
- Programatik kontroller için SecurityIdentity kullanın
|
||||
- Uygun CORS politikaları belirleyin
|
||||
- Kimlik doğrulama ve yetkilendirme yollarını test edin
|
||||
|
||||
@@ -4,33 +4,31 @@ description: Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito,
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Not / Note**: Bu dosya henuz Turkceye cevrilmemistir, su anda Ingilizce orijinaldir. Ceviri PR'lari memnuniyetle karsilanir.
|
||||
# Quarkus TDD İş Akışı
|
||||
|
||||
# Quarkus TDD Workflow
|
||||
80%+ kapsam (unit + integration) ile Quarkus 3.x servisleri için TDD rehberi. Apache Camel ile event-driven mimariler için optimize edilmiştir.
|
||||
|
||||
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
## When to Use
|
||||
- Yeni özellikler veya REST endpoint'leri
|
||||
- Bug düzeltmeleri veya refactoring'ler
|
||||
- Veri erişim mantığı, güvenlik kuralları veya reaktif akışlar ekleme
|
||||
- Apache Camel route'larını ve event handler'larını test etme
|
||||
- RabbitMQ ile event-driven servisleri test etme
|
||||
- Koşullu akış mantığını test etme
|
||||
- CompletableFuture async işlemlerini doğrulama
|
||||
- LogContext yayılımını test etme
|
||||
|
||||
- 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
|
||||
## İş Akışı
|
||||
|
||||
## Workflow
|
||||
1. Önce testleri yazın (başarısız olmalılar)
|
||||
2. Geçmek için minimal kod uygulayın
|
||||
3. Testleri yeşil tutarken refactor edin
|
||||
4. JaCoCo ile kapsamı zorlayın (%80+ hedef)
|
||||
|
||||
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)
|
||||
## @Nested Organizasyonlu Unit Testler
|
||||
|
||||
## Unit Tests with @Nested Organization
|
||||
|
||||
Follow this structured approach for comprehensive, readable tests:
|
||||
Kapsamlı ve okunabilir testler için bu yapılandırılmış yaklaşımı izleyin:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -62,7 +60,7 @@ class As2ProcessingServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// ARRANGE - Common test data
|
||||
// ARRANGE - Ortak test verisi
|
||||
testFilePath = Path.of("/tmp/test-invoice.xml");
|
||||
|
||||
testLogContext = new LogContext();
|
||||
@@ -81,11 +79,11 @@ class As2ProcessingServiceTest {
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for processFile")
|
||||
@DisplayName("processFile için testler")
|
||||
class ProcessFile {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully process non-CHORUS file with all validations")
|
||||
@DisplayName("CHORUS olmayan dosyayı tüm validasyonlarla başarıyla işlemeli")
|
||||
void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception {
|
||||
// ARRANGE
|
||||
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||
@@ -125,7 +123,7 @@ class As2ProcessingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should bypass schematron validation for CHORUS_FLOW")
|
||||
@DisplayName("CHORUS_FLOW için schematron validasyonu atlanmalı")
|
||||
void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception {
|
||||
// ARRANGE
|
||||
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
|
||||
@@ -162,7 +160,7 @@ class As2ProcessingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create error event when file upload fails")
|
||||
@DisplayName("Dosya yükleme başarısız olduğunda hata eventi oluşturulmalı")
|
||||
void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception {
|
||||
// ARRANGE
|
||||
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||
@@ -174,7 +172,7 @@ class As2ProcessingServiceTest {
|
||||
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||
.thenReturn(FlowProfile.BASIC);
|
||||
|
||||
documentInfo.setPath(""); // Blank path triggers error
|
||||
documentInfo.setPath(""); // Boş path hatayı tetikler
|
||||
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||
|
||||
@@ -196,7 +194,7 @@ class As2ProcessingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle CompletableFuture.join() failure")
|
||||
@DisplayName("CompletableFuture.join() başarısızlığı ele alınmalı")
|
||||
void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception {
|
||||
// ARRANGE
|
||||
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||
@@ -221,7 +219,7 @@ class As2ProcessingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when file path is null")
|
||||
@DisplayName("Dosya yolu null olduğunda exception fırlatılmalı")
|
||||
void givenNullFilePath_whenProcessFile_thenThrowsException() {
|
||||
// ARRANGE
|
||||
Path nullPath = null;
|
||||
@@ -238,20 +236,20 @@ class As2ProcessingServiceTest {
|
||||
}
|
||||
```
|
||||
|
||||
### Key Testing Patterns
|
||||
### Temel Test Desenleri
|
||||
|
||||
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
|
||||
1. **@Nested Sınıflar**: Testleri test edilen metoda göre gruplandırın
|
||||
2. **@DisplayName**: Test raporlarında okunabilir açıklamalar sağlayın
|
||||
3. **İsimlendirme Kuralı**: Netlik için `givenX_whenY_thenZ`
|
||||
4. **AAA Deseni**: Açık `// ARRANGE`, `// ACT`, `// ASSERT` yorumları
|
||||
5. **@BeforeEach**: Tekrarı azaltmak için ortak test verisi kurulumu
|
||||
6. **assertDoesNotThrow**: Exception yakalamadan başarı senaryolarını test edin
|
||||
7. **assertThrows**: AssertJ kullanarak mesaj doğrulamalı exception senaryolarını test edin
|
||||
8. **Kapsamlı Kapsam**: Mutlu yolları, null girdileri, edge case'leri, exception'ları test edin
|
||||
9. **Etkileşimleri Doğrulama**: Metodların doğru çağrıldığından emin olmak için Mockito `verify()` kullanın
|
||||
10. **Hiçbir Zaman Doğrulama**: Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `never()` kullanın
|
||||
|
||||
## Testing Camel Routes
|
||||
## Camel Route Testi
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -271,25 +269,25 @@ class BusinessRulesRouteTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// ARRANGE - Test data
|
||||
// ARRANGE - Test verisi
|
||||
testPayload = new BusinessRulesPayload();
|
||||
testPayload.setDocumentId(1L);
|
||||
testPayload.setFlowProfile(FlowProfile.BASIC);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for business-rules-publisher route")
|
||||
@DisplayName("business-rules-publisher route için testler")
|
||||
class BusinessRulesPublisher {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully publish message to RabbitMQ")
|
||||
@DisplayName("Mesajı başarıyla RabbitMQ'ya yayınlamalı")
|
||||
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
|
||||
// Test için gerçek endpoint'i mock ile değiştir
|
||||
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||
advice.replaceFromWith("direct:business-rules-publisher");
|
||||
@@ -309,7 +307,7 @@ class BusinessRulesRouteTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle marshalling to JSON")
|
||||
@DisplayName("JSON'a marshalling'i ele almalı")
|
||||
void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {
|
||||
// ARRANGE
|
||||
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
||||
@@ -335,11 +333,11 @@ class BusinessRulesRouteTest {
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for document-processing route")
|
||||
@DisplayName("document-processing route için testler")
|
||||
class DocumentProcessing {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should route invoice to correct processor")
|
||||
@DisplayName("Faturayı doğru işlemciye yönlendirmeli")
|
||||
void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {
|
||||
// ARRANGE
|
||||
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
||||
@@ -360,7 +358,7 @@ class BusinessRulesRouteTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle validation errors gracefully")
|
||||
@DisplayName("Validasyon hatalarını zarif şekilde ele almalı")
|
||||
void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {
|
||||
// ARRANGE
|
||||
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
||||
@@ -373,7 +371,7 @@ class BusinessRulesRouteTest {
|
||||
});
|
||||
camelContext.getRouteController().startRoute("document-processing");
|
||||
|
||||
// Mock validator to throw exception
|
||||
// Validator'ı exception fırlatacak şekilde mock'la
|
||||
when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
||||
|
||||
// ACT
|
||||
@@ -390,7 +388,7 @@ class BusinessRulesRouteTest {
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Event Services
|
||||
## Event Service Testi
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -416,11 +414,11 @@ class EventServiceTest {
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for createSuccessEvent")
|
||||
@DisplayName("createSuccessEvent için testler")
|
||||
class CreateSuccessEvent {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create success event with correct attributes")
|
||||
@DisplayName("Doğru niteliklerle başarı eventi oluşturulmalı")
|
||||
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||
// ARRANGE
|
||||
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||
@@ -439,7 +437,7 @@ class EventServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when payload is null")
|
||||
@DisplayName("Payload null olduğunda exception fırlatılmalı")
|
||||
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||
// ARRANGE
|
||||
Object nullPayload = null;
|
||||
@@ -456,11 +454,11 @@ class EventServiceTest {
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for createErrorEvent")
|
||||
@DisplayName("createErrorEvent için testler")
|
||||
class CreateErrorEvent {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create error event with error message")
|
||||
@DisplayName("Hata mesajıyla hata eventi oluşturulmalı")
|
||||
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
||||
// ARRANGE
|
||||
String errorMessage = "Processing failed";
|
||||
@@ -480,7 +478,7 @@ class EventServiceTest {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("Should reject invalid error messages")
|
||||
@DisplayName("Geçersiz hata mesajları reddedilmeli")
|
||||
@ValueSource(strings = {"", " "})
|
||||
void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
|
||||
// ACT & ASSERT
|
||||
@@ -495,7 +493,7 @@ class EventServiceTest {
|
||||
}
|
||||
```
|
||||
|
||||
## Testing CompletableFuture
|
||||
## CompletableFuture Testi
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -523,11 +521,11 @@ class FileStorageServiceTest {
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for uploadOriginalFile")
|
||||
@DisplayName("uploadOriginalFile için testler")
|
||||
class UploadOriginalFile {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully upload file and return document info")
|
||||
@DisplayName("Dosyayı başarıyla yüklemeli ve belge bilgisi döndürmeli")
|
||||
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
||||
// ARRANGE
|
||||
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||
@@ -555,7 +553,7 @@ class FileStorageServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle S3 upload failure")
|
||||
@DisplayName("S3 yükleme başarısızlığını ele almalı")
|
||||
void givenS3Failure_whenUpload_thenCompletableFutureFails() {
|
||||
// ARRANGE
|
||||
when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> {
|
||||
@@ -575,7 +573,7 @@ class FileStorageServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should propagate LogContext to async operation")
|
||||
@DisplayName("LogContext'i async işleme yaymalı")
|
||||
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||
// ARRANGE
|
||||
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
||||
@@ -598,7 +596,7 @@ class FileStorageServiceTest {
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Layer Tests (REST Assured)
|
||||
## Resource Katmanı Testleri (REST Assured)
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -609,11 +607,11 @@ class DocumentResourceTest {
|
||||
DocumentService documentService;
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for GET /api/documents")
|
||||
@DisplayName("GET /api/documents için testler")
|
||||
class ListDocuments {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return list of documents")
|
||||
@DisplayName("Belge listesini döndürmeli")
|
||||
void givenDocumentsExist_whenList_thenReturnsOk() {
|
||||
// ARRANGE
|
||||
List<Document> documents = List.of(createDocument(1L, "DOC-001"));
|
||||
@@ -630,11 +628,11 @@ class DocumentResourceTest {
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for POST /api/documents")
|
||||
@DisplayName("POST /api/documents için testler")
|
||||
class CreateDocument {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create document and return 201")
|
||||
@DisplayName("Belge oluşturmalı ve 201 döndürmeli")
|
||||
void givenValidRequest_whenCreate_thenReturns201() {
|
||||
// ARRANGE
|
||||
Document document = createDocument(1L, "DOC-001");
|
||||
@@ -659,7 +657,7 @@ class DocumentResourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return 400 for invalid input")
|
||||
@DisplayName("Geçersiz girdi için 400 döndürmeli")
|
||||
void givenInvalidRequest_whenCreate_thenReturns400() {
|
||||
// ACT & ASSERT
|
||||
given()
|
||||
@@ -686,7 +684,7 @@ class DocumentResourceTest {
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Tests with Real Database
|
||||
## Gerçek Veritabanıyla Entegrasyon Testleri
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -696,9 +694,9 @@ class DocumentIntegrationTest {
|
||||
|
||||
@Test
|
||||
@Transactional
|
||||
@DisplayName("Should create and retrieve document via API")
|
||||
@DisplayName("Belge oluşturulmalı ve API üzerinden alınabilmeli")
|
||||
void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
|
||||
// ACT - Create via API
|
||||
// ACT - API üzerinden oluştur
|
||||
Long id = given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
@@ -714,7 +712,7 @@ class DocumentIntegrationTest {
|
||||
.statusCode(201)
|
||||
.extract().path("id");
|
||||
|
||||
// ASSERT - Retrieve via API
|
||||
// ASSERT - API üzerinden al
|
||||
given()
|
||||
.when().get("/api/documents/" + id)
|
||||
.then()
|
||||
@@ -724,9 +722,9 @@ class DocumentIntegrationTest {
|
||||
}
|
||||
```
|
||||
|
||||
## Coverage with JaCoCo
|
||||
## JaCoCo ile Kapsam
|
||||
|
||||
### Maven Configuration (Complete)
|
||||
### Maven Yapılandırması (Tam)
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
@@ -734,7 +732,7 @@ class DocumentIntegrationTest {
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.13</version>
|
||||
<executions>
|
||||
<!-- Prepare agent for test execution -->
|
||||
<!-- Test yürütmesi için agent'ı hazırla -->
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
@@ -742,7 +740,7 @@ class DocumentIntegrationTest {
|
||||
</goals>
|
||||
</execution>
|
||||
|
||||
<!-- Generate coverage report -->
|
||||
<!-- Kapsam raporu oluştur -->
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>verify</phase>
|
||||
@@ -751,7 +749,7 @@ class DocumentIntegrationTest {
|
||||
</goals>
|
||||
</execution>
|
||||
|
||||
<!-- Enforce coverage thresholds -->
|
||||
<!-- Kapsam eşiklerini zorla -->
|
||||
<execution>
|
||||
<id>check</id>
|
||||
<goals>
|
||||
@@ -781,20 +779,20 @@ class DocumentIntegrationTest {
|
||||
</plugin>
|
||||
```
|
||||
|
||||
Run tests with coverage:
|
||||
Kapsam ile testleri çalıştırın:
|
||||
```bash
|
||||
mvn clean test
|
||||
mvn jacoco:report
|
||||
mvn jacoco:check
|
||||
|
||||
# Report at: target/site/jacoco/index.html
|
||||
# Rapor: target/site/jacoco/index.html
|
||||
```
|
||||
|
||||
## Test Dependencies
|
||||
## Test Bağımlılıkları
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Quarkus Testing -->
|
||||
<!-- Quarkus Test -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
@@ -813,7 +811,7 @@ mvn jacoco:check
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- AssertJ (preferred over JUnit assertions) -->
|
||||
<!-- AssertJ (JUnit assertion'larına tercih edilir) -->
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
@@ -828,7 +826,7 @@ mvn jacoco:check
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Camel Testing -->
|
||||
<!-- Camel Test -->
|
||||
<dependency>
|
||||
<groupId>org.apache.camel.quarkus</groupId>
|
||||
<artifactId>camel-quarkus-junit5</artifactId>
|
||||
@@ -837,74 +835,74 @@ mvn jacoco:check
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
## En İyi Uygulamalar
|
||||
|
||||
### 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 Organizasyonu
|
||||
- Testleri test edilen metoda göre gruplandırmak için `@Nested` sınıflar kullanın
|
||||
- Raporlarda görünür okunabilir açıklamalar için `@DisplayName` kullanın
|
||||
- Test metodları için `givenX_whenY_thenZ` isimlendirme kuralını izleyin
|
||||
- Tekrarı azaltmak için ortak test verisi kurulumunda `@BeforeEach` kullanın
|
||||
|
||||
### 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 Yapısı
|
||||
- Açık yorumlarla AAA desenini izleyin (`// ARRANGE`, `// ACT`, `// ASSERT`)
|
||||
- Başarı senaryoları için `assertDoesNotThrow` kullanın
|
||||
- Mesaj doğrulamalı exception senaryoları için `assertThrows` kullanın
|
||||
- AssertJ `contains()` veya `isEqualTo()` kullanarak exception mesajlarının beklenen değerlerle eşleştiğini doğrulayın
|
||||
|
||||
### 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
|
||||
### Test Kapsamı
|
||||
- Tüm public metodlar için mutlu yolları test edin
|
||||
- Null girdi işlemeyi test edin
|
||||
- Edge case'leri test edin (boş koleksiyonlar, sınır değerleri, negatif ID'ler, boş string'ler)
|
||||
- Exception senaryolarını kapsamlı biçimde test edin
|
||||
- Tüm harici bağımlılıkları mock'layın (repository'ler, servisler, Camel endpoint'leri)
|
||||
- %80+ satır kapsamı, %70+ branch kapsamı hedefleyin
|
||||
|
||||
### 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()`
|
||||
### Assertion'lar
|
||||
- JUnit assertion'ları yerine **her zaman AssertJ** (`assertThat`) kullanın
|
||||
- Okunabilirlik için akıcı AssertJ API'si kullanın: `assertThat(list).hasSize(3).contains(item)`
|
||||
- Exception'lar için: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
|
||||
- Koleksiyonlar için: `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
|
||||
### Entegrasyon Testi
|
||||
- Entegrasyon testleri için `@QuarkusTest` kullanın
|
||||
- Quarkus testlerinde bağımlılıkları mock'lamak için `@InjectMock` kullanın
|
||||
- API testi için REST Assured'ı tercih edin
|
||||
- Test'e özel yapılandırma için `@TestProfile` kullanın
|
||||
|
||||
### 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
|
||||
### Event-Driven Test
|
||||
- `AdviceWith` ve `MockEndpoint` ile Camel route'larını test edin
|
||||
- `@CamelQuarkusTest` annotasyonu kullanın (bağımsız Camel testleri kullanıyorsanız)
|
||||
- Mesaj içeriğini, başlıklarını ve yönlendirme mantığını doğrulayın
|
||||
- Hata işleme route'larını ayrı ayrı test edin
|
||||
- Unit testlerde harici sistemleri (RabbitMQ, S3, veritabanları) mock'layın
|
||||
|
||||
### 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
|
||||
### Camel Route Testi
|
||||
- Mesaj akışını doğrulamak için `MockEndpoint` kullanın
|
||||
- Test için route'ları değiştirmek üzere `AdviceWith` kullanın (endpoint'leri mock'larla değiştirin)
|
||||
- Mesaj dönüşümünü ve marshalling'i test edin
|
||||
- Exception işleme ve dead letter queue'ları test edin
|
||||
|
||||
### 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
|
||||
### Async İşlem Testi
|
||||
- CompletableFuture başarı ve başarısızlık senaryolarını test edin
|
||||
- Async tamamlanmayı beklemek için testlerde `.join()` kullanın
|
||||
- CompletableFuture'dan exception yayılımını test edin
|
||||
- LogContext yayılımını async işlemlere doğrulayın
|
||||
|
||||
### 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
|
||||
### Performans
|
||||
- Testleri hızlı ve izole tutun
|
||||
- Testleri sürekli modda çalıştırın: `mvn quarkus:test`
|
||||
- Girdi varyasyonları için parametreli testler (`@ParameterizedTest`) kullanın
|
||||
- Yeniden kullanılabilir test verisi builder'ları veya factory metodları oluşturun
|
||||
|
||||
### 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)
|
||||
### Quarkus'a Özgü
|
||||
- En son LTS sürümünde kalın (Quarkus 3.x)
|
||||
- Native derleme uyumluluğunu periyodik olarak test edin
|
||||
- Farklı senaryolar için Quarkus test profillerini kullanın
|
||||
- Yerel test için Quarkus dev servislerinden yararlanın
|
||||
- `@MockBean` yerine `@InjectMock` kullanın (Quarkus'a özgü)
|
||||
|
||||
### 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
|
||||
### Doğrulama En İyi Uygulamaları
|
||||
- Mock'lanmış bağımlılıklardaki etkileşimleri her zaman doğrulayın
|
||||
- Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `verify(mock, never())` kullanın
|
||||
- Karmaşık argüman eşleştirme için `argThat()` kullanın
|
||||
- Önem taşıdığında çağrı sırasını doğrulayın: Mockito'dan `InOrder`
|
||||
|
||||
@@ -4,22 +4,20 @@ description: "Verification loop for Quarkus projects: build, static analysis, te
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Not / Note**: Bu dosya henuz Turkceye cevrilmemistir, su anda Ingilizce orijinaldir. Ceviri PR'lari memnuniyetle karsilanir.
|
||||
# Quarkus Doğrulama Döngüsü
|
||||
|
||||
# Quarkus Verification Loop
|
||||
PR'lardan önce, büyük değişikliklerden sonra ve deployment öncesi çalıştırın.
|
||||
|
||||
Run before PRs, after major changes, and pre-deploy.
|
||||
## Ne Zaman Aktif Edilir
|
||||
|
||||
## When to Activate
|
||||
- Quarkus servisi için pull request açmadan önce
|
||||
- Büyük refactoring veya bağımlılık yükseltmelerinden sonra
|
||||
- Staging veya production için deployment öncesi doğrulama
|
||||
- Tam build → lint → test → güvenlik taraması → native derleme pipeline'ı çalıştırma
|
||||
- Test kapsamının eşikleri karşıladığını doğrulama (%80+)
|
||||
- Native image uyumluluğunu test etme
|
||||
|
||||
- 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
|
||||
## Faz 1: Build
|
||||
|
||||
```bash
|
||||
# Maven
|
||||
@@ -29,9 +27,9 @@ mvn clean verify -DskipTests
|
||||
./gradlew clean assemble -x test
|
||||
```
|
||||
|
||||
If build fails, stop and fix compilation errors.
|
||||
Build başarısız olursa, durdurun ve derleme hatalarını düzeltin.
|
||||
|
||||
## Phase 2: Static Analysis
|
||||
## Faz 2: Static Analiz
|
||||
|
||||
### Checkstyle, PMD, SpotBugs (Maven)
|
||||
|
||||
@@ -39,7 +37,7 @@ If build fails, stop and fix compilation errors.
|
||||
mvn checkstyle:check pmd:check spotbugs:check
|
||||
```
|
||||
|
||||
### SonarQube (if configured)
|
||||
### SonarQube (yapılandırılmışsa)
|
||||
|
||||
```bash
|
||||
mvn sonar:sonar \
|
||||
@@ -48,33 +46,33 @@ mvn sonar:sonar \
|
||||
-Dsonar.login=${SONAR_TOKEN}
|
||||
```
|
||||
|
||||
### Common Issues to Address
|
||||
### Ele Alınacak Yaygın Sorunlar
|
||||
|
||||
- Unused imports or variables
|
||||
- Complex methods (high cyclomatic complexity)
|
||||
- Potential null pointer dereferences
|
||||
- Security issues flagged by SpotBugs
|
||||
- Kullanılmayan import'lar veya değişkenler
|
||||
- Karmaşık metodlar (yüksek cyclomatic complexity)
|
||||
- Potansiyel null pointer dereference'ları
|
||||
- SpotBugs tarafından işaretlenen güvenlik sorunları
|
||||
|
||||
## Phase 3: Tests + Coverage
|
||||
## Faz 3: Testler + Kapsam
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
# Tüm testleri çalıştır
|
||||
mvn clean test
|
||||
|
||||
# Generate coverage report
|
||||
# Kapsam raporu oluştur
|
||||
mvn jacoco:report
|
||||
|
||||
# Enforce coverage threshold (80%)
|
||||
# Kapsam eşiğini zorla (%80)
|
||||
mvn jacoco:check
|
||||
|
||||
# Or with Gradle
|
||||
# Veya Gradle ile
|
||||
./gradlew test jacocoTestReport jacocoTestCoverageVerification
|
||||
```
|
||||
|
||||
### Test Categories
|
||||
### Test Kategorileri
|
||||
|
||||
#### Unit Tests
|
||||
Test service logic with mocked dependencies:
|
||||
#### Unit Testler
|
||||
Mock'lanmış bağımlılıklarla servis mantığını test edin:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -99,8 +97,8 @@ class UserServiceTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### Integration Tests
|
||||
Test with real database (Testcontainers):
|
||||
#### Entegrasyon Testleri
|
||||
Gerçek veritabanıyla (Testcontainers) test edin:
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -126,8 +124,8 @@ class UserRepositoryIntegrationTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### API Tests
|
||||
Test REST endpoints with REST Assured:
|
||||
#### API Testleri
|
||||
REST Assured ile REST endpoint'lerini test edin:
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -160,34 +158,34 @@ class UserResourceTest {
|
||||
}
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
### Kapsam Raporu
|
||||
|
||||
Check `target/site/jacoco/index.html` for detailed coverage:
|
||||
- Overall line coverage (target: 80%+)
|
||||
- Branch coverage (target: 70%+)
|
||||
- Identify uncovered critical paths
|
||||
Ayrıntılı kapsam için `target/site/jacoco/index.html` sayfasını kontrol edin:
|
||||
- Genel satır kapsamı (hedef: %80+)
|
||||
- Branch kapsamı (hedef: %70+)
|
||||
- Kapsanmamış kritik yolları belirleyin
|
||||
|
||||
## Phase 4: Security Scanning
|
||||
## Faz 4: Güvenlik Taraması
|
||||
|
||||
### Dependency Vulnerabilities (Maven)
|
||||
### Bağımlılık Güvenlik Açıkları (Maven)
|
||||
|
||||
```bash
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
```
|
||||
|
||||
Review `target/dependency-check-report.html` for CVEs.
|
||||
CVE'ler için `target/dependency-check-report.html` raporunu inceleyin.
|
||||
|
||||
### Quarkus Security Audit
|
||||
### Quarkus Güvenlik Denetimi
|
||||
|
||||
```bash
|
||||
# Check vulnerable extensions
|
||||
# Güvenlik açığı olan extension'ları kontrol et
|
||||
mvn quarkus:audit
|
||||
|
||||
# List all extensions
|
||||
# Tüm extension'ları listele
|
||||
mvn quarkus:list-extensions
|
||||
```
|
||||
|
||||
### OWASP ZAP (API Security Testing)
|
||||
### OWASP ZAP (API Güvenlik Testi)
|
||||
|
||||
```bash
|
||||
docker run -t owasp/zap2docker-stable zap-api-scan.py \
|
||||
@@ -195,52 +193,52 @@ docker run -t owasp/zap2docker-stable zap-api-scan.py \
|
||||
-f openapi
|
||||
```
|
||||
|
||||
### Common Security Checks
|
||||
### Yaygın Güvenlik Kontrolleri
|
||||
|
||||
- [ ] 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
|
||||
- [ ] Tüm gizli bilgiler ortam değişkenlerinde (kodda değil)
|
||||
- [ ] Tüm endpoint'lerde girdi doğrulama
|
||||
- [ ] Kimlik doğrulama/yetkilendirme yapılandırılmış
|
||||
- [ ] CORS düzgün yapılandırılmış
|
||||
- [ ] Güvenlik başlıkları ayarlanmış
|
||||
- [ ] Parolalar BCrypt ile hash'lenmiş
|
||||
- [ ] SQL injection koruması (parametreli sorgular)
|
||||
- [ ] Genel endpoint'lerde rate limiting
|
||||
|
||||
## Phase 5: Native Compilation
|
||||
## Faz 5: Native Derleme
|
||||
|
||||
Test GraalVM native image compatibility:
|
||||
GraalVM native image uyumluluğunu test edin:
|
||||
|
||||
```bash
|
||||
# Build native executable
|
||||
# Native executable oluştur
|
||||
mvn package -Dnative
|
||||
|
||||
# Or with container
|
||||
# Veya container ile
|
||||
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||
|
||||
# Test native executable
|
||||
# Native executable'ı test et
|
||||
./target/*-runner
|
||||
|
||||
# Run basic smoke tests
|
||||
# Temel smoke testleri çalıştır
|
||||
curl http://localhost:8080/q/health/live
|
||||
curl http://localhost:8080/q/health/ready
|
||||
```
|
||||
|
||||
### Native Image Troubleshooting
|
||||
### Native Image Sorun Giderme
|
||||
|
||||
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
|
||||
Yaygın sorunlar:
|
||||
- **Reflection**: Dinamik sınıflar için reflection yapılandırması ekleyin
|
||||
- **Resources**: `quarkus.native.resources.includes` ile kaynakları dahil edin
|
||||
- **JNI**: Native kütüphaneler kullanıyorsanız JNI sınıflarını kaydedin
|
||||
|
||||
Example reflection config:
|
||||
Örnek reflection yapılandırması:
|
||||
```java
|
||||
@RegisterForReflection(targets = {MyDynamicClass.class})
|
||||
public class ReflectionConfiguration {}
|
||||
```
|
||||
|
||||
## Phase 6: Performance Testing
|
||||
## Faz 6: Performans Testi
|
||||
|
||||
### Load Testing with K6
|
||||
### K6 ile Yük Testi
|
||||
|
||||
```javascript
|
||||
// load-test.js
|
||||
@@ -264,20 +262,20 @@ export default function () {
|
||||
}
|
||||
```
|
||||
|
||||
Run:
|
||||
Çalıştırın:
|
||||
```bash
|
||||
k6 run load-test.js
|
||||
```
|
||||
|
||||
### Metrics to Monitor
|
||||
### İzlenecek Metrikler
|
||||
|
||||
- Response time (p50, p95, p99)
|
||||
- Throughput (requests/sec)
|
||||
- Error rate
|
||||
- Memory usage
|
||||
- CPU usage
|
||||
- Yanıt süresi (p50, p95, p99)
|
||||
- Throughput (istek/saniye)
|
||||
- Hata oranı
|
||||
- Bellek kullanımı
|
||||
- CPU kullanımı
|
||||
|
||||
## Phase 7: Health Checks
|
||||
## Faz 7: Sağlık Kontrolleri
|
||||
|
||||
```bash
|
||||
# Liveness
|
||||
@@ -286,14 +284,14 @@ curl http://localhost:8080/q/health/live
|
||||
# Readiness
|
||||
curl http://localhost:8080/q/health/ready
|
||||
|
||||
# All health checks
|
||||
# Tüm sağlık kontrolleri
|
||||
curl http://localhost:8080/q/health
|
||||
|
||||
# Metrics (if enabled)
|
||||
# Metrikler (etkinleştirilmişse)
|
||||
curl http://localhost:8080/q/metrics
|
||||
```
|
||||
|
||||
Expected responses:
|
||||
Beklenen yanıtlar:
|
||||
```json
|
||||
{
|
||||
"status": "UP",
|
||||
@@ -306,24 +304,24 @@ Expected responses:
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 8: Container Image Build
|
||||
## Faz 8: Container Image Build
|
||||
|
||||
```bash
|
||||
# Build container image
|
||||
# Container image oluştur
|
||||
mvn package -Dquarkus.container-image.build=true
|
||||
|
||||
# Or with specific registry
|
||||
# Veya belirli registry ile
|
||||
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
|
||||
# Container'ı test et
|
||||
docker run -p 8080:8080 myorg/my-quarkus-app:1.0.0
|
||||
```
|
||||
|
||||
### Container Security Scan
|
||||
### Container Güvenlik Taraması
|
||||
|
||||
```bash
|
||||
# Trivy
|
||||
@@ -333,103 +331,103 @@ trivy image myorg/my-quarkus-app:1.0.0
|
||||
grype myorg/my-quarkus-app:1.0.0
|
||||
```
|
||||
|
||||
## Phase 9: Configuration Validation
|
||||
## Faz 9: Yapılandırma Doğrulama
|
||||
|
||||
```bash
|
||||
# Check all configuration properties
|
||||
# Tüm yapılandırma özelliklerini kontrol et
|
||||
mvn quarkus:info
|
||||
|
||||
# List all config sources
|
||||
# Tüm yapılandırma kaynaklarını listele
|
||||
curl http://localhost:8080/q/dev/io.quarkus.quarkus-vertx-http/config
|
||||
```
|
||||
|
||||
### Environment-Specific Checks
|
||||
### Ortama Özgü Kontroller
|
||||
|
||||
- [ ] Database URLs configured per environment
|
||||
- [ ] Secrets externalized (Vault, env vars)
|
||||
- [ ] Logging levels appropriate
|
||||
- [ ] CORS origins set correctly
|
||||
- [ ] Rate limiting configured
|
||||
- [ ] Monitoring/tracing enabled
|
||||
- [ ] Veritabanı URL'leri ortam başına yapılandırılmış
|
||||
- [ ] Gizli bilgiler dışsallaştırılmış (Vault, ortam değişkenleri)
|
||||
- [ ] Loglama seviyeleri uygun
|
||||
- [ ] CORS origin'leri doğru ayarlanmış
|
||||
- [ ] Rate limiting yapılandırılmış
|
||||
- [ ] İzleme/tracing etkinleştirilmiş
|
||||
|
||||
## Phase 10: Documentation Review
|
||||
## Faz 10: Dokümantasyon İncelemesi
|
||||
|
||||
- [ ] OpenAPI/Swagger docs up to date (`/q/swagger-ui`)
|
||||
- [ ] README has setup instructions
|
||||
- [ ] API changes documented
|
||||
- [ ] Migration guide for breaking changes
|
||||
- [ ] Configuration properties documented
|
||||
- [ ] OpenAPI/Swagger dokümanları güncel (`/q/swagger-ui`)
|
||||
- [ ] README kurulum talimatlarını içeriyor
|
||||
- [ ] API değişiklikleri belgelenmiş
|
||||
- [ ] Breaking change'ler için migration rehberi
|
||||
- [ ] Yapılandırma özellikleri belgelenmiş
|
||||
|
||||
Generate OpenAPI spec:
|
||||
OpenAPI spec oluşturun:
|
||||
```bash
|
||||
curl http://localhost:8080/q/openapi -o openapi.json
|
||||
```
|
||||
|
||||
## Verification Checklist
|
||||
## Doğrulama Kontrol Listesi
|
||||
|
||||
### 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
|
||||
### Kod Kalitesi
|
||||
- [ ] Build uyarısız geçiyor
|
||||
- [ ] Static analiz temiz (yüksek/orta sorun yok)
|
||||
- [ ] Kod ekip kurallarını takip ediyor
|
||||
- [ ] PR'da yorum satırına alınmış kod veya TODO yok
|
||||
|
||||
### Testing
|
||||
- [ ] All tests pass
|
||||
- [ ] Code coverage ≥ 80%
|
||||
- [ ] Integration tests with real database
|
||||
- [ ] Security tests pass
|
||||
- [ ] Performance within acceptable limits
|
||||
### Test
|
||||
- [ ] Tüm testler geçiyor
|
||||
- [ ] Kod kapsamı ≥ %80
|
||||
- [ ] Gerçek veritabanıyla entegrasyon testleri
|
||||
- [ ] Güvenlik testleri geçiyor
|
||||
- [ ] Performans kabul edilebilir sınırlar içinde
|
||||
|
||||
### Security
|
||||
- [ ] No dependency vulnerabilities
|
||||
- [ ] Authentication/authorization tested
|
||||
- [ ] Input validation complete
|
||||
- [ ] Secrets not in source code
|
||||
- [ ] Security headers configured
|
||||
### Güvenlik
|
||||
- [ ] Bağımlılık güvenlik açığı yok
|
||||
- [ ] Kimlik doğrulama/yetkilendirme test edilmiş
|
||||
- [ ] Girdi doğrulama tamamlanmış
|
||||
- [ ] Gizli bilgiler kaynak kodda değil
|
||||
- [ ] Güvenlik başlıkları yapılandırılmış
|
||||
|
||||
### Deployment
|
||||
- [ ] Native compilation successful
|
||||
- [ ] Container image builds
|
||||
- [ ] Health checks respond correctly
|
||||
- [ ] Configuration valid for target environment
|
||||
- [ ] Native derleme başarılı
|
||||
- [ ] Container image oluşturuluyor
|
||||
- [ ] Sağlık kontrolleri doğru yanıt veriyor
|
||||
- [ ] Hedef ortam için yapılandırma geçerli
|
||||
|
||||
### Native Image
|
||||
- [ ] Native executable builds
|
||||
- [ ] Native tests pass
|
||||
- [ ] Startup time < 100ms
|
||||
- [ ] Memory footprint acceptable
|
||||
- [ ] Native executable oluşturuluyor
|
||||
- [ ] Native testler geçiyor
|
||||
- [ ] Başlangıç süresi < 100ms
|
||||
- [ ] Bellek ayak izi kabul edilebilir
|
||||
|
||||
## Automated Verification Script
|
||||
## Otomatik Doğrulama Script'i
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Phase 1: Build ==="
|
||||
echo "=== Faz 1: Build ==="
|
||||
mvn clean verify -DskipTests
|
||||
|
||||
echo "=== Phase 2: Static Analysis ==="
|
||||
echo "=== Faz 2: Static Analiz ==="
|
||||
mvn checkstyle:check pmd:check spotbugs:check
|
||||
|
||||
echo "=== Phase 3: Tests + Coverage ==="
|
||||
echo "=== Faz 3: Testler + Kapsam ==="
|
||||
mvn test jacoco:report jacoco:check
|
||||
|
||||
echo "=== Phase 4: Security Scan ==="
|
||||
echo "=== Faz 4: Güvenlik Taraması ==="
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
|
||||
echo "=== Phase 5: Native Compilation ==="
|
||||
echo "=== Faz 5: Native Derleme ==="
|
||||
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 "=== Tüm Fazlar Tamamlandı ==="
|
||||
echo "Raporları inceleyin:"
|
||||
echo " - Kapsam: target/site/jacoco/index.html"
|
||||
echo " - Güvenlik: target/dependency-check-report.html"
|
||||
echo " - Native: target/*-runner"
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
## CI/CD Entegrasyonu
|
||||
|
||||
### GitHub Actions Example
|
||||
### GitHub Actions Örneği
|
||||
|
||||
```yaml
|
||||
name: Verification
|
||||
@@ -469,15 +467,15 @@ jobs:
|
||||
files: target/site/jacoco/jacoco.xml
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
## En İyi Uygulamalar
|
||||
|
||||
- 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
|
||||
- Her PR öncesi doğrulama döngüsünü çalıştırın
|
||||
- CI/CD pipeline'ında otomatize edin
|
||||
- Sorunları hemen düzeltin; borç biriktirmeyin
|
||||
- Kapsamı %80'in üzerinde tutun
|
||||
- Bağımlılıkları düzenli olarak güncelleyin
|
||||
- Native derlemeyi periyodik olarak test edin
|
||||
- Performans trendlerini izleyin
|
||||
- Breaking change'leri belgeleyin
|
||||
- Güvenlik tarama sonuçlarını inceleyin
|
||||
- Her ortam için yapılandırmayı doğrulayın
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
---
|
||||
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.
|
||||
description: Quarkus 3.x LTS架构模式,Camel消息传递、RESTful API设计、CDI服务、Panache数据访问和异步处理。用于具有事件驱动架构的Java Quarkus后端工作。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Note / 注意**: 本文件尚未翻译为中文,目前为英文原版。欢迎提交翻译 PR。
|
||||
# Quarkus 开发模式
|
||||
|
||||
# Quarkus Development Patterns
|
||||
使用Apache Camel的云原生事件驱动服务的Quarkus 3.x架构和API模式。
|
||||
|
||||
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
|
||||
## 何时激活
|
||||
|
||||
## When to Activate
|
||||
- 使用JAX-RS或RESTEasy Reactive构建REST API
|
||||
- 构建资源 → 服务 → 仓库层结构
|
||||
- 使用Apache Camel和RabbitMQ实现事件驱动模式
|
||||
- 配置Hibernate Panache、缓存或响应式流
|
||||
- 添加验证、异常映射或分页
|
||||
- 为开发/预发布/生产环境设置配置文件(YAML配置)
|
||||
- 使用LogContext和Logback/Logstash编码器进行自定义日志记录
|
||||
- 使用CompletableFuture进行异步操作
|
||||
- 实现条件流处理
|
||||
- 使用GraalVM原生编译
|
||||
|
||||
- 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)
|
||||
## 多依赖服务层(Lombok)
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
@@ -43,7 +41,7 @@ public class As2ProcessingService {
|
||||
|
||||
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);
|
||||
|
||||
@@ -62,7 +60,7 @@ public class As2ProcessingService {
|
||||
|
||||
log.info("Invoice validation completed. Message is valid");
|
||||
|
||||
// CompletableFuture async operation
|
||||
// CompletableFuture异步操作
|
||||
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
||||
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
||||
fileStorageService.uploadOriginalFile(inputStream,
|
||||
@@ -85,7 +83,7 @@ public class As2ProcessingService {
|
||||
documentInfo, originalFileName, structureIdPartner,
|
||||
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||
|
||||
// Async Camel publishing
|
||||
// 异步Camel发布
|
||||
businessRulesPublisher.publishAsync(payload);
|
||||
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||
}
|
||||
@@ -94,16 +92,16 @@ public class As2ProcessingService {
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
**关键模式:**
|
||||
- 通过Lombok的`@RequiredArgsConstructor`进行构造函数注入
|
||||
- 通过`@Slf4j`进行Logback日志记录
|
||||
- 使用try-with-resources的作用域LogContext
|
||||
- 基于运行时参数的条件流逻辑
|
||||
- 使用`.join()`的CompletableFuture异步操作
|
||||
- 成功/错误场景的事件跟踪
|
||||
- 异步Camel消息发布
|
||||
|
||||
## Custom Logging Context Pattern (Logback)
|
||||
## 自定义日志上下文模式(Logback)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -112,14 +110,14 @@ 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");
|
||||
@@ -131,7 +129,7 @@ public class ProcessingService {
|
||||
}
|
||||
```
|
||||
|
||||
**Logback Configuration (logback.xml):**
|
||||
**Logback配置(logback.xml):**
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
@@ -149,7 +147,7 @@ public class ProcessingService {
|
||||
</configuration>
|
||||
```
|
||||
|
||||
## Event Service Pattern
|
||||
## 事件服务模式
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -181,13 +179,13 @@ public class EventService {
|
||||
}
|
||||
|
||||
private String serializePayload(Object payload) {
|
||||
// JSON serialization
|
||||
// JSON序列化
|
||||
return objectMapper.writeValueAsString(payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Message Publishing (RabbitMQ)
|
||||
## Camel消息发布(RabbitMQ)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -215,7 +213,7 @@ public class BusinessRulesPublisher {
|
||||
}
|
||||
```
|
||||
|
||||
**Camel Route Configuration:**
|
||||
**Camel路由配置:**
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -242,7 +240,7 @@ public class BusinessRulesRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Direct Routes (In-Memory)
|
||||
## Camel Direct路由(内存中)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -250,13 +248,13 @@ 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}")
|
||||
@@ -278,7 +276,7 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## Camel File Processing
|
||||
## Camel文件处理
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -308,27 +306,7 @@ public class FileMonitoringRoute extends RouteBuilder {
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
## REST API结构
|
||||
|
||||
```java
|
||||
@Path("/api/documents")
|
||||
@@ -367,7 +345,7 @@ public class DocumentResource {
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern (Panache Repository)
|
||||
## 仓库模式(Panache Repository)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -389,7 +367,7 @@ public class DocumentRepository implements PanacheRepository<Document> {
|
||||
}
|
||||
```
|
||||
|
||||
## Service Layer with Transactions
|
||||
## 带事务的服务层
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -416,16 +394,10 @@ public class DocumentService {
|
||||
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
|
||||
## DTO和验证
|
||||
|
||||
```java
|
||||
public record CreateDocumentRequest(
|
||||
@@ -442,7 +414,7 @@ public record DocumentResponse(Long id, String referenceNumber, DocumentStatus s
|
||||
}
|
||||
```
|
||||
|
||||
## Exception Mapping
|
||||
## 异常映射
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -473,7 +445,7 @@ public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
||||
}
|
||||
```
|
||||
|
||||
## CompletableFuture Async Operations
|
||||
## CompletableFuture异步操作
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -512,7 +484,7 @@ public class FileStorageService {
|
||||
}
|
||||
```
|
||||
|
||||
## Caching
|
||||
## 缓存
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -533,7 +505,7 @@ public class DocumentCacheService {
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration as YAML
|
||||
## YAML配置
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
@@ -580,7 +552,7 @@ public class DocumentCacheService {
|
||||
username: ${RABBITMQ_USER}
|
||||
password: ${RABBITMQ_PASSWORD}
|
||||
|
||||
# Camel configuration
|
||||
# Camel配置
|
||||
camel:
|
||||
rabbitmq:
|
||||
queue:
|
||||
@@ -588,7 +560,7 @@ camel:
|
||||
invoice-processing: invoice-processing-queue
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
## 健康检查
|
||||
|
||||
```java
|
||||
@Readiness
|
||||
@@ -626,7 +598,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies (Maven)
|
||||
## 依赖(Maven)
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
@@ -657,7 +629,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Quarkus Core -->
|
||||
<!-- Quarkus核心 -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
@@ -667,7 +639,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
<artifactId>quarkus-config-yaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Camel Extensions -->
|
||||
<!-- Camel扩展 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.camel.quarkus</groupId>
|
||||
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
||||
@@ -689,7 +661,7 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<!-- 日志 -->
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||
<artifactId>quarkus-logging-logback</artifactId>
|
||||
@@ -701,56 +673,56 @@ public class CamelHealthCheck implements HealthCheck {
|
||||
</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
|
||||
### 架构
|
||||
- 使用Lombok的`@RequiredArgsConstructor`进行构造函数注入
|
||||
- 保持服务层精简,将复杂逻辑委托给专门的类
|
||||
- 使用Camel路由进行消息路由和集成模式
|
||||
- 数据访问优先使用Panache Repository模式
|
||||
|
||||
### 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()`
|
||||
### 事件驱动
|
||||
- 始终使用EventService跟踪操作(成功/错误事件)
|
||||
- 使用Camel的`direct:`端点进行内存路由
|
||||
- 使用`spring-rabbitmq`组件进行RabbitMQ集成
|
||||
- 使用`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
|
||||
### 日志
|
||||
- 使用Logstash编码器的Logback进行结构化日志
|
||||
- 使用`SafeAutoCloseable`在服务调用间传播LogContext
|
||||
- 向LogContext添加上下文信息以进行请求追踪
|
||||
- 使用`@Slf4j`代替手动日志实例化
|
||||
|
||||
### 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
|
||||
### 异步操作
|
||||
- 使用CompletableFuture进行非阻塞I/O操作
|
||||
- 需要等待完成时调用`.join()`
|
||||
- 正确处理CompletableFuture的异常
|
||||
- 为追踪目的向异步操作传递LogContext
|
||||
|
||||
### 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
|
||||
### 配置
|
||||
- 使用YAML配置(`quarkus-config-yaml`)
|
||||
- dev/test/prod环境的配置文件感知配置
|
||||
- 将敏感配置外部化到环境变量
|
||||
- 使用`@ConfigProperty`进行类型安全的配置注入
|
||||
|
||||
### Validation
|
||||
- Validate at resource layer with `@Valid`
|
||||
- Use Bean Validation annotations on DTOs
|
||||
- Map exceptions to proper HTTP responses with `@Provider`
|
||||
### 验证
|
||||
- 在资源层使用`@Valid`进行验证
|
||||
- 在DTO上使用Bean Validation注解
|
||||
- 使用`@Provider`将异常映射到适当的HTTP响应
|
||||
|
||||
### Transactions
|
||||
- Use `@Transactional` on service methods that modify data
|
||||
- Keep transactions short and focused
|
||||
- Avoid calling async operations within transactions
|
||||
### 事务
|
||||
- 在修改数据的服务方法上使用`@Transactional`
|
||||
- 保持事务短小且聚焦
|
||||
- 避免在事务内调用异步操作
|
||||
|
||||
### Testing
|
||||
- Use `camel-quarkus-junit5` for route testing
|
||||
- Use AssertJ for assertions
|
||||
- Mock all external dependencies
|
||||
- Test conditional flow logic thoroughly
|
||||
### 测试
|
||||
- 使用`camel-quarkus-junit5`进行路由测试
|
||||
- 使用AssertJ进行断言
|
||||
- 模拟所有外部依赖
|
||||
- 彻底测试条件流逻辑
|
||||
|
||||
### 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
|
||||
### Quarkus特定
|
||||
- 保持最新的LTS版本(3.x)
|
||||
- 使用Quarkus开发模式进行热重载
|
||||
- 添加健康检查以确保生产就绪
|
||||
- 定期测试原生编译兼容性
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
---
|
||||
name: quarkus-security
|
||||
description: Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
|
||||
description: Quarkus安全最佳实践:认证、授权、JWT/OIDC、RBAC、输入验证、CSRF、密钥管理和依赖安全。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Note / 注意**: 本文件尚未翻译为中文,目前为英文原版。欢迎提交翻译 PR。
|
||||
# Quarkus 安全审查
|
||||
|
||||
# Quarkus Security Review
|
||||
使用认证、授权和输入验证保护Quarkus应用程序的最佳实践。
|
||||
|
||||
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
|
||||
## 何时激活
|
||||
|
||||
## When to Activate
|
||||
- 添加认证(JWT、OIDC、Basic Auth)
|
||||
- 使用@RolesAllowed或SecurityIdentity实现授权
|
||||
- 验证用户输入(Bean Validation、自定义验证器)
|
||||
- 配置CORS或安全头
|
||||
- 管理密钥(Vault、环境变量、配置源)
|
||||
- 添加速率限制或暴力破解保护
|
||||
- 扫描依赖CVE
|
||||
- 使用MicroProfile JWT或SmallRye JWT
|
||||
|
||||
- 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
|
||||
### JWT认证
|
||||
|
||||
```java
|
||||
// Resource protected with JWT
|
||||
@Path("/api/protected")
|
||||
@Authenticated
|
||||
public class ProtectedResource {
|
||||
@@ -50,7 +47,7 @@ public class ProtectedResource {
|
||||
}
|
||||
```
|
||||
|
||||
Configuration (application.properties):
|
||||
配置(application.properties):
|
||||
```properties
|
||||
mp.jwt.verify.publickey.location=publicKey.pem
|
||||
mp.jwt.verify.issuer=https://auth.example.com
|
||||
@@ -61,7 +58,7 @@ quarkus.oidc.client-id=backend-service
|
||||
quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
||||
```
|
||||
|
||||
### Custom Authentication Filter
|
||||
### 自定义认证过滤器
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -77,7 +74,6 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -85,15 +81,15 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
||||
}
|
||||
|
||||
private boolean validateToken(String token) {
|
||||
// Token validation logic
|
||||
// 令牌验证逻辑
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authorization
|
||||
## 授权
|
||||
|
||||
### Role-Based Access Control
|
||||
### 基于角色的访问控制
|
||||
|
||||
```java
|
||||
@Path("/api/admin")
|
||||
@@ -125,21 +121,17 @@ public class UserResource {
|
||||
@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
|
||||
@@ -163,18 +155,18 @@ public class SecurityService {
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
## 输入验证
|
||||
|
||||
### Bean Validation
|
||||
|
||||
```java
|
||||
// BAD: No validation
|
||||
// BAD: 无验证
|
||||
@POST
|
||||
public Response createUser(UserDto dto) {
|
||||
return Response.ok(userService.create(dto)).build();
|
||||
}
|
||||
|
||||
// GOOD: Validated DTO
|
||||
// GOOD: 验证DTO
|
||||
public record CreateUserDto(
|
||||
@NotBlank @Size(max = 100) String name,
|
||||
@NotBlank @Email String email,
|
||||
@@ -190,55 +182,28 @@ public Response createUser(@Valid CreateUserDto dto) {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Validators
|
||||
## SQL注入防护
|
||||
|
||||
### Panache Active Record(默认安全)
|
||||
|
||||
```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
|
||||
// GOOD: Panache参数化查询
|
||||
List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
||||
|
||||
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||
|
||||
// GOOD: Named parameters
|
||||
// GOOD: 命名参数
|
||||
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
|
||||
// BAD: 字符串拼接
|
||||
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||
|
||||
// GOOD: Parameterized native query
|
||||
// GOOD: 参数化原生查询
|
||||
@Entity
|
||||
public class User extends PanacheEntity {
|
||||
public static List<User> findByEmailNative(String email) {
|
||||
@@ -250,7 +215,7 @@ public class User extends PanacheEntity {
|
||||
}
|
||||
```
|
||||
|
||||
## Password Hashing
|
||||
## 密码哈希
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -264,33 +229,9 @@ public class PasswordService {
|
||||
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
|
||||
## CORS配置
|
||||
|
||||
```properties
|
||||
# application.properties
|
||||
@@ -303,37 +244,22 @@ quarkus.http.cors.access-control-max-age=24H
|
||||
quarkus.http.cors.access-control-allow-credentials=true
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
## 密钥管理
|
||||
|
||||
```properties
|
||||
# application.properties - NO SECRETS HERE
|
||||
# application.properties — 此处不放密钥
|
||||
|
||||
# Use environment variables
|
||||
# 使用环境变量
|
||||
quarkus.datasource.username=${DB_USER}
|
||||
quarkus.datasource.password=${DB_PASSWORD}
|
||||
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
||||
|
||||
# Or use Vault
|
||||
# 或使用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
|
||||
@@ -344,7 +270,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
String clientId = getClientIdentifier(requestContext);
|
||||
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||
k -> RateLimiter.create(100.0)); // 100 requests per second
|
||||
k -> RateLimiter.create(100.0)); // 每秒100个请求
|
||||
|
||||
if (!limiter.tryAcquire()) {
|
||||
requestContext.abortWith(
|
||||
@@ -354,15 +280,10 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String getClientIdentifier(ContainerRequestContext ctx) {
|
||||
// Use IP, API key, or user ID
|
||||
return ctx.getHeaderString("X-Forwarded-For");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Headers
|
||||
## 安全头
|
||||
|
||||
```java
|
||||
@Provider
|
||||
@@ -372,10 +293,10 @@ public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||
|
||||
// Prevent clickjacking
|
||||
// 防止点击劫持
|
||||
headers.putSingle("X-Frame-Options", "DENY");
|
||||
|
||||
// XSS protection
|
||||
// XSS保护
|
||||
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||
|
||||
@@ -389,7 +310,7 @@ public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||
}
|
||||
```
|
||||
|
||||
## Audit Logging
|
||||
## 审计日志
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@@ -408,23 +329,9 @@ public class AuditService {
|
||||
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
|
||||
@@ -433,23 +340,23 @@ mvn org.owasp:dependency-check-maven:check
|
||||
# Gradle
|
||||
./gradlew dependencyCheckAnalyze
|
||||
|
||||
# Check Quarkus extensions
|
||||
# 检查Quarkus扩展
|
||||
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
|
||||
- 生产环境始终使用HTTPS
|
||||
- 启用JWT或OIDC进行无状态认证
|
||||
- 使用`@RolesAllowed`进行声明式授权
|
||||
- 使用Bean Validation验证所有输入
|
||||
- 使用BCrypt哈希密码(禁止明文)
|
||||
- 将密钥存储在Vault或环境变量中
|
||||
- 使用参数化查询防止SQL注入
|
||||
- 为所有响应添加安全头
|
||||
- 为公共端点实现速率限制
|
||||
- 审计敏感操作
|
||||
- 保持依赖更新并扫描CVE
|
||||
- 使用SecurityIdentity进行编程式检查
|
||||
- 设置适当的CORS策略
|
||||
- 测试认证和授权路径
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
---
|
||||
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.
|
||||
description: 使用JUnit 5、Mockito、REST Assured、Camel测试和JaCoCo的Quarkus 3.x LTS测试驱动开发。用于添加功能、修复错误或重构事件驱动服务。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Note / 注意**: 本文件尚未翻译为中文,目前为英文原版。欢迎提交翻译 PR。
|
||||
# Quarkus TDD工作流
|
||||
|
||||
# Quarkus TDD Workflow
|
||||
面向80%以上覆盖率(单元+集成)的Quarkus 3.x服务TDD指南。针对Apache Camel的事件驱动架构优化。
|
||||
|
||||
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
|
||||
## 何时使用
|
||||
|
||||
## When to Use
|
||||
- 新功能或REST端点
|
||||
- Bug修复或重构
|
||||
- 添加数据访问逻辑、安全规则或响应式流
|
||||
- 测试Apache Camel路由和事件处理器
|
||||
- 测试RabbitMQ事件驱动服务
|
||||
- 测试条件流逻辑
|
||||
- 验证CompletableFuture异步操作
|
||||
- 测试LogContext传播
|
||||
|
||||
- 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. 先写测试(应该失败)
|
||||
2. 实现通过测试的最少代码
|
||||
3. 测试通过后重构
|
||||
4. 使用JaCoCo强制覆盖率(80%以上目标)
|
||||
|
||||
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)
|
||||
## 使用@Nested组织的单元测试
|
||||
|
||||
## Unit Tests with @Nested Organization
|
||||
|
||||
Follow this structured approach for comprehensive, readable tests:
|
||||
全面、可读测试的结构化方法:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -62,7 +60,7 @@ class As2ProcessingServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// ARRANGE - Common test data
|
||||
// ARRANGE - 公共测试数据
|
||||
testFilePath = Path.of("/tmp/test-invoice.xml");
|
||||
|
||||
testLogContext = new LogContext();
|
||||
@@ -119,48 +117,9 @@ class As2ProcessingServiceTest {
|
||||
|
||||
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 {
|
||||
@@ -174,7 +133,7 @@ class As2ProcessingServiceTest {
|
||||
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||
.thenReturn(FlowProfile.BASIC);
|
||||
|
||||
documentInfo.setPath(""); // Blank path triggers error
|
||||
documentInfo.setPath(""); // 空路径触发错误
|
||||
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||
|
||||
@@ -187,71 +146,26 @@ class As2ProcessingServiceTest {
|
||||
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
|
||||
1. **@Nested类**: 按被测方法分组测试
|
||||
2. **@DisplayName**: 为测试报告提供可读描述
|
||||
3. **命名约定**: 使用`givenX_whenY_thenZ`确保清晰
|
||||
4. **AAA模式**: 明确的`// ARRANGE`、`// ACT`、`// ASSERT`注释
|
||||
5. **@BeforeEach**: 通用测试数据设置以减少重复
|
||||
6. **assertDoesNotThrow**: 不捕获异常的成功场景测试
|
||||
7. **assertThrows**: 带消息验证的异常场景测试
|
||||
8. **全面覆盖**: 测试正常路径、null输入、边界情况、异常
|
||||
9. **验证交互**: 使用Mockito的`verify()`确保方法被正确调用
|
||||
10. **Never验证**: 使用`never()`确保错误场景中方法未被调用
|
||||
|
||||
## Testing Camel Routes
|
||||
## 测试Camel路由
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -267,338 +181,30 @@ class BusinessRulesRouteTest {
|
||||
@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() {
|
||||
@Test
|
||||
@DisplayName("Should successfully publish message to RabbitMQ")
|
||||
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
|
||||
// ARRANGE
|
||||
testPayload = new BusinessRulesPayload();
|
||||
testPayload.setDocumentId(1L);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tests for createSuccessEvent")
|
||||
class CreateSuccessEvent {
|
||||
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||
mockRabbitMQ.expectedMessageCount(1);
|
||||
|
||||
@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 {
|
||||
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");
|
||||
|
||||
@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");
|
||||
}
|
||||
// ACT
|
||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||
|
||||
// ASSERT
|
||||
mockRabbitMQ.assertIsSatisfied(5000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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)
|
||||
## 资源层测试(REST Assured)
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -608,27 +214,6 @@ 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 {
|
||||
@@ -654,14 +239,12 @@ class DocumentResourceTest {
|
||||
.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("""
|
||||
@@ -675,58 +258,12 @@ class DocumentResourceTest {
|
||||
.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
|
||||
## JaCoCo覆盖率
|
||||
|
||||
```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)
|
||||
### Maven配置
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
@@ -734,29 +271,18 @@ class DocumentIntegrationTest {
|
||||
<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>
|
||||
<goals><goal>prepare-agent</goal></goals>
|
||||
</execution>
|
||||
|
||||
<!-- Generate coverage report -->
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<goals><goal>report</goal></goals>
|
||||
</execution>
|
||||
|
||||
<!-- Enforce coverage thresholds -->
|
||||
<execution>
|
||||
<id>check</id>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<goals><goal>check</goal></goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
@@ -767,11 +293,6 @@ class DocumentIntegrationTest {
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.80</minimum>
|
||||
</limit>
|
||||
<limit>
|
||||
<counter>BRANCH</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.70</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
@@ -781,20 +302,19 @@ class DocumentIntegrationTest {
|
||||
</plugin>
|
||||
```
|
||||
|
||||
Run tests with coverage:
|
||||
运行带覆盖率的测试:
|
||||
```bash
|
||||
mvn clean test
|
||||
mvn jacoco:report
|
||||
mvn jacoco:check
|
||||
|
||||
# Report at: target/site/jacoco/index.html
|
||||
# 报告位于: target/site/jacoco/index.html
|
||||
```
|
||||
|
||||
## Test Dependencies
|
||||
## 测试依赖
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Quarkus Testing -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
@@ -805,30 +325,17 @@ mvn jacoco:check
|
||||
<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>
|
||||
@@ -837,74 +344,32 @@ mvn jacoco:check
|
||||
</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
|
||||
### 测试组织
|
||||
- 使用`@Nested`类按被测方法分组
|
||||
- 使用`@DisplayName`提供可读的测试描述
|
||||
- 遵循`givenX_whenY_thenZ`命名约定
|
||||
|
||||
### 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()`
|
||||
### 测试结构
|
||||
- 遵循带明确注释的AAA模式(`// ARRANGE`、`// ACT`、`// ASSERT`)
|
||||
- 成功场景使用`assertDoesNotThrow`
|
||||
- 异常场景使用`assertThrows`并验证消息
|
||||
|
||||
### 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
|
||||
### 断言
|
||||
- **始终使用AssertJ**(`assertThat`)代替JUnit断言
|
||||
- 使用流式AssertJ API提高可读性
|
||||
- 异常断言: `assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessageContaining(...)`
|
||||
|
||||
### 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()`
|
||||
### 事件驱动测试
|
||||
- 使用`AdviceWith`和`MockEndpoint`测试Camel路由
|
||||
- 验证消息内容、头部和路由逻辑
|
||||
- 单独测试错误处理路由
|
||||
- 单元测试中模拟外部系统(RabbitMQ、S3、数据库)
|
||||
|
||||
### 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
|
||||
### Quarkus特定
|
||||
- 保持最新的LTS版本(Quarkus 3.x)
|
||||
- 使用Quarkus测试配置文件处理不同场景
|
||||
- 使用`@InjectMock`代替`@MockBean`(Quarkus特定)
|
||||
|
||||
### 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
|
||||
**请记住**: 保持测试快速、隔离和确定性。测试行为而非实现细节。
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
---
|
||||
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."
|
||||
description: "Quarkus项目验证循环:构建、静态分析、带覆盖率的测试、安全扫描、原生编译以及发布或PR前的diff审查。"
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
> **Note / 注意**: 本文件尚未翻译为中文,目前为英文原版。欢迎提交翻译 PR。
|
||||
# Quarkus 验证循环
|
||||
|
||||
# Quarkus Verification Loop
|
||||
在PR前、重大变更后和部署前运行。
|
||||
|
||||
Run before PRs, after major changes, and pre-deploy.
|
||||
## 何时激活
|
||||
|
||||
## When to Activate
|
||||
- 为Quarkus服务打开PR前
|
||||
- 大规模重构或依赖升级后
|
||||
- 预发布或生产的部署前验证
|
||||
- 运行完整的构建 → lint → 测试 → 安全扫描 → 原生编译流水线
|
||||
- 验证测试覆盖率达到阈值(80%+)
|
||||
- 测试原生镜像兼容性
|
||||
|
||||
- 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
|
||||
## 阶段1: 构建
|
||||
|
||||
```bash
|
||||
# Maven
|
||||
@@ -29,17 +27,17 @@ mvn clean verify -DskipTests
|
||||
./gradlew clean assemble -x test
|
||||
```
|
||||
|
||||
If build fails, stop and fix compilation errors.
|
||||
构建失败时,停止并修复编译错误。
|
||||
|
||||
## Phase 2: Static Analysis
|
||||
## 阶段2: 静态分析
|
||||
|
||||
### Checkstyle, PMD, SpotBugs (Maven)
|
||||
### Checkstyle、PMD、SpotBugs(Maven)
|
||||
|
||||
```bash
|
||||
mvn checkstyle:check pmd:check spotbugs:check
|
||||
```
|
||||
|
||||
### SonarQube (if configured)
|
||||
### SonarQube(如已配置)
|
||||
|
||||
```bash
|
||||
mvn sonar:sonar \
|
||||
@@ -48,33 +46,33 @@ mvn sonar:sonar \
|
||||
-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
|
||||
- 未使用的导入或变量
|
||||
- 复杂方法(高圈复杂度)
|
||||
- 潜在的空指针解引用
|
||||
- SpotBugs标记的安全问题
|
||||
|
||||
## Phase 3: Tests + Coverage
|
||||
## 阶段3: 测试 + 覆盖率
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
# 运行所有测试
|
||||
mvn clean test
|
||||
|
||||
# Generate coverage report
|
||||
# 生成覆盖率报告
|
||||
mvn jacoco:report
|
||||
|
||||
# Enforce coverage threshold (80%)
|
||||
# 强制覆盖率阈值(80%)
|
||||
mvn jacoco:check
|
||||
|
||||
# Or with Gradle
|
||||
# 或使用Gradle
|
||||
./gradlew test jacocoTestReport jacocoTestCoverageVerification
|
||||
```
|
||||
|
||||
### Test Categories
|
||||
### 测试类别
|
||||
|
||||
#### Unit Tests
|
||||
Test service logic with mocked dependencies:
|
||||
#### 单元测试
|
||||
使用模拟依赖测试服务逻辑:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -99,8 +97,8 @@ class UserServiceTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### Integration Tests
|
||||
Test with real database (Testcontainers):
|
||||
#### 集成测试
|
||||
使用真实数据库(Testcontainers)测试:
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -126,8 +124,8 @@ class UserRepositoryIntegrationTest {
|
||||
}
|
||||
```
|
||||
|
||||
#### API Tests
|
||||
Test REST endpoints with REST Assured:
|
||||
#### API测试
|
||||
使用REST Assured测试REST端点:
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@@ -160,324 +158,157 @@ class UserResourceTest {
|
||||
}
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
### 覆盖率报告
|
||||
|
||||
Check `target/site/jacoco/index.html` for detailed coverage:
|
||||
- Overall line coverage (target: 80%+)
|
||||
- Branch coverage (target: 70%+)
|
||||
- Identify uncovered critical paths
|
||||
检查`target/site/jacoco/index.html`获取详细覆盖率:
|
||||
- 总体行覆盖率(目标: 80%+)
|
||||
- 分支覆盖率(目标: 70%+)
|
||||
- 识别未覆盖的关键路径
|
||||
|
||||
## Phase 4: Security Scanning
|
||||
## 阶段4: 安全扫描
|
||||
|
||||
### Dependency Vulnerabilities (Maven)
|
||||
### 依赖漏洞(Maven)
|
||||
|
||||
```bash
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
```
|
||||
|
||||
Review `target/dependency-check-report.html` for CVEs.
|
||||
查看`target/dependency-check-report.html`中的CVE。
|
||||
|
||||
### Quarkus Security Audit
|
||||
### Quarkus安全审计
|
||||
|
||||
```bash
|
||||
# Check vulnerable extensions
|
||||
# 检查有漏洞的扩展
|
||||
mvn quarkus:audit
|
||||
|
||||
# List all extensions
|
||||
# 列出所有扩展
|
||||
mvn quarkus:list-extensions
|
||||
```
|
||||
|
||||
### OWASP ZAP (API Security Testing)
|
||||
### 常见安全检查
|
||||
|
||||
- [ ] 所有密钥在环境变量中(不在代码中)
|
||||
- [ ] 所有端点有输入验证
|
||||
- [ ] 认证/授权已配置
|
||||
- [ ] CORS正确配置
|
||||
- [ ] 安全头已设置
|
||||
- [ ] 密码使用BCrypt哈希
|
||||
- [ ] SQL注入保护(参数化查询)
|
||||
- [ ] 公共端点有速率限制
|
||||
|
||||
## 阶段5: 原生编译
|
||||
|
||||
测试GraalVM原生镜像兼容性:
|
||||
|
||||
```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
|
||||
常见问题:
|
||||
- **Reflection**: 为动态类添加反射配置
|
||||
- **Resources**: 使用`quarkus.native.resources.includes`包含资源
|
||||
- **JNI**: 使用原生库时注册JNI类
|
||||
|
||||
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
|
||||
## 阶段6: 健康检查
|
||||
|
||||
```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
|
||||
### 代码质量
|
||||
- [ ] 构建无警告通过
|
||||
- [ ] 静态分析干净(无高/中问题)
|
||||
- [ ] 代码遵循团队规范
|
||||
- [ ] PR中无注释代码或TODO
|
||||
|
||||
```bash
|
||||
# Build container image
|
||||
mvn package -Dquarkus.container-image.build=true
|
||||
### 测试
|
||||
- [ ] 所有测试通过
|
||||
- [ ] 代码覆盖率 ≥ 80%
|
||||
- [ ] 使用真实数据库的集成测试
|
||||
- [ ] 安全测试通过
|
||||
- [ ] 性能在可接受范围内
|
||||
|
||||
# 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 ==="
|
||||
echo "=== 阶段1: 构建 ==="
|
||||
mvn clean verify -DskipTests
|
||||
|
||||
echo "=== Phase 2: Static Analysis ==="
|
||||
echo "=== 阶段2: 静态分析 ==="
|
||||
mvn checkstyle:check pmd:check spotbugs:check
|
||||
|
||||
echo "=== Phase 3: Tests + Coverage ==="
|
||||
echo "=== 阶段3: 测试 + 覆盖率 ==="
|
||||
mvn test jacoco:report jacoco:check
|
||||
|
||||
echo "=== Phase 4: Security Scan ==="
|
||||
echo "=== 阶段4: 安全扫描 ==="
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
|
||||
echo "=== Phase 5: Native Compilation ==="
|
||||
echo "=== 阶段5: 原生编译 ==="
|
||||
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"
|
||||
echo "=== 所有阶段完成 ==="
|
||||
echo "查看报告:"
|
||||
echo " - 覆盖率: target/site/jacoco/index.html"
|
||||
echo " - 安全: target/dependency-check-report.html"
|
||||
echo " - 原生: 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
|
||||
- 每次PR前运行验证循环
|
||||
- 在CI/CD流水线中自动化
|
||||
- 立即修复问题,不积累技术债务
|
||||
- 保持覆盖率在80%以上
|
||||
- 定期更新依赖
|
||||
- 定期测试原生编译
|
||||
- 监控性能趋势
|
||||
- 记录破坏性变更
|
||||
- 审查安全扫描结果
|
||||
- 验证每个环境的配置
|
||||
|
||||
Reference in New Issue
Block a user