mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-13 23:03:34 +08:00
fix: make plugin hooks run on Node 21+ and green the suite under modern Node (#2184)
ROOT CAUSE: hooks load plugin-hook-bootstrap.js via `node -e "...; process.argv.splice(1,0,s); require(s)"`. On Node 21+, require.main is `undefined` under --eval, so the `if (require.main === module)` guard was false and main() never ran — every plugin hook silently no-op'd (e.g. the MCP-health PreToolUse hook stopped blocking). CI (Node 18/20) hid this; it only surfaces on Node 21+. Fix: also run main() when require.main is undefined (the eval-bootstrap case), while staying dormant on real imports. Also clears pre-existing main debt the full local suite enforces: - catalog:sync — README/docs agent+skill counts drifted after recent merges - tests/ci/supply-chain-watch-workflow: update checkout SHA to the merged v6.0.3 (#2183) - markdownlint + check-unicode-safety --write across docs/skills Suite: 2683/2683 green under Node v25; lint + unicode clean. Co-authored-by: ECC Test <ecc@example.test>
This commit is contained in:
parent
eef31ad39c
commit
e755c5f72b
@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ecc",
|
"name": "ecc",
|
||||||
"source": "./",
|
"source": "./",
|
||||||
"description": "Harness-native ECC operator layer - 63 agents, 251 skills, 79 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
|
"description": "Harness-native ECC operator layer - 64 agents, 255 skills, 79 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
|
||||||
"version": "2.0.0-rc.1",
|
"version": "2.0.0-rc.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Affaan Mustafa",
|
"name": "Affaan Mustafa",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ecc",
|
"name": "ecc",
|
||||||
"version": "2.0.0-rc.1",
|
"version": "2.0.0-rc.1",
|
||||||
"description": "Harness-native ECC plugin for engineering teams - 63 agents, 251 skills, 79 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
|
"description": "Harness-native ECC plugin for engineering teams - 64 agents, 255 skills, 79 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Affaan Mustafa",
|
"name": "Affaan Mustafa",
|
||||||
"url": "https://x.com/affaanmustafa"
|
"url": "https://x.com/affaanmustafa"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Everything Claude Code (ECC) — Agent Instructions
|
# Everything Claude Code (ECC) — Agent Instructions
|
||||||
|
|
||||||
This is a **production-ready AI coding plugin** providing 63 specialized agents, 251 skills, 79 commands, and automated hook workflows for software development.
|
This is a **production-ready AI coding plugin** providing 64 specialized agents, 255 skills, 79 commands, and automated hook workflows for software development.
|
||||||
|
|
||||||
**Version:** 2.0.0-rc.1
|
**Version:** 2.0.0-rc.1
|
||||||
|
|
||||||
@ -149,8 +149,8 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
agents/ — 63 specialized subagents
|
agents/ — 64 specialized subagents
|
||||||
skills/ — 251 workflow skills and domain knowledge
|
skills/ — 255 workflow skills and domain knowledge
|
||||||
commands/ — 79 slash commands
|
commands/ — 79 slash commands
|
||||||
hooks/ — Trigger-based automations
|
hooks/ — Trigger-based automations
|
||||||
rules/ — Always-follow guidelines (common + per-language)
|
rules/ — Always-follow guidelines (common + per-language)
|
||||||
|
|||||||
15
README.md
15
README.md
@ -123,7 +123,7 @@ This repo is the raw code only. The guides explain everything.
|
|||||||
### v2.0.0-rc.1 — Surface Refresh, Operator Workflows, and ECC 2.0 Alpha (Apr 2026)
|
### v2.0.0-rc.1 — Surface Refresh, Operator Workflows, and ECC 2.0 Alpha (Apr 2026)
|
||||||
|
|
||||||
- **Dashboard GUI** — New Tkinter-based desktop application (`ecc_dashboard.py` or `npm run dashboard`) with dark/light theme toggle, font customization, and project logo in header and taskbar.
|
- **Dashboard GUI** — New Tkinter-based desktop application (`ecc_dashboard.py` or `npm run dashboard`) with dark/light theme toggle, font customization, and project logo in header and taskbar.
|
||||||
- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 63 agents, 251 skills, and 79 legacy command shims.
|
- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 64 agents, 255 skills, and 79 legacy command shims.
|
||||||
- **Operator and outbound workflow expansion** — `brand-voice`, `social-graph-ranker`, `connections-optimizer`, `customer-billing-ops`, `ecc-tools-cost-audit`, `google-workspace-ops`, `project-flow-ops`, and `workspace-surface-audit` round out the operator lane.
|
- **Operator and outbound workflow expansion** — `brand-voice`, `social-graph-ranker`, `connections-optimizer`, `customer-billing-ops`, `ecc-tools-cost-audit`, `google-workspace-ops`, `project-flow-ops`, and `workspace-surface-audit` round out the operator lane.
|
||||||
- **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system.
|
- **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system.
|
||||||
- **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone.
|
- **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone.
|
||||||
@ -394,7 +394,7 @@ If you stacked methods, clean up in this order:
|
|||||||
/plugin list ecc@ecc
|
/plugin list ecc@ecc
|
||||||
```
|
```
|
||||||
|
|
||||||
**That's it!** You now have access to 63 agents, 251 skills, and 79 legacy command shims.
|
**That's it!** You now have access to 64 agents, 255 skills, and 79 legacy command shims.
|
||||||
|
|
||||||
### Dashboard GUI
|
### Dashboard GUI
|
||||||
|
|
||||||
@ -487,7 +487,6 @@ export ECC_SESSION_RETENTION_DAYS=14
|
|||||||
export ECC_CONTEXT_MONITOR_COST_WARNINGS=off
|
export ECC_CONTEXT_MONITOR_COST_WARNINGS=off
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
Windows PowerShell:
|
Windows PowerShell:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@ -525,7 +524,7 @@ ECC/
|
|||||||
| |-- plugin.json # Plugin metadata and component paths
|
| |-- plugin.json # Plugin metadata and component paths
|
||||||
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add
|
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add
|
||||||
|
|
|
|
||||||
|-- agents/ # 63 specialized subagents for delegation
|
|-- agents/ # 64 specialized subagents for delegation
|
||||||
| |-- planner.md # Feature implementation planning
|
| |-- planner.md # Feature implementation planning
|
||||||
| |-- architect.md # System design decisions
|
| |-- architect.md # System design decisions
|
||||||
| |-- tdd-guide.md # Test-driven development
|
| |-- tdd-guide.md # Test-driven development
|
||||||
@ -1472,9 +1471,9 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
|||||||
|
|
||||||
| Feature | Claude Code | OpenCode | Status |
|
| Feature | Claude Code | OpenCode | Status |
|
||||||
|---------|---------------------|----------|--------|
|
|---------|---------------------|----------|--------|
|
||||||
| Agents | PASS: 63 agents | PASS: 12 agents | **Claude Code leads** |
|
| Agents | PASS: 64 agents | PASS: 12 agents | **Claude Code leads** |
|
||||||
| Commands | PASS: 79 commands | PASS: 35 commands | **Claude Code leads** |
|
| Commands | PASS: 79 commands | PASS: 35 commands | **Claude Code leads** |
|
||||||
| Skills | PASS: 251 skills | PASS: 37 skills | **Claude Code leads** |
|
| Skills | PASS: 255 skills | PASS: 37 skills | **Claude Code leads** |
|
||||||
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
||||||
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
||||||
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
||||||
@ -1634,9 +1633,9 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|
|||||||
|
|
||||||
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | GitHub Copilot |
|
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | GitHub Copilot |
|
||||||
|---------|-----------------------|------------|-----------|----------|----------------|
|
|---------|-----------------------|------------|-----------|----------|----------------|
|
||||||
| **Agents** | 63 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | N/A |
|
| **Agents** | 64 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | N/A |
|
||||||
| **Commands** | 79 | Shared | Instruction-based | 35 | 6 prompts |
|
| **Commands** | 79 | Shared | Instruction-based | 35 | 6 prompts |
|
||||||
| **Skills** | 251 | Shared | 10 (native format) | 37 | Via instructions |
|
| **Skills** | 255 | Shared | 10 (native format) | 37 | Via instructions |
|
||||||
| **Hook Events** | 8 types | 15 types | None yet | 11 types | None |
|
| **Hook Events** | 8 types | 15 types | None yet | 11 types | None |
|
||||||
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | N/A |
|
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | N/A |
|
||||||
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | 1 always-on file |
|
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | 1 always-on file |
|
||||||
|
|||||||
@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
|||||||
/plugin list ecc@ecc
|
/plugin list ecc@ecc
|
||||||
```
|
```
|
||||||
|
|
||||||
**完成!** 你现在可以使用 63 个代理、251 个技能和 79 个命令。
|
**完成!** 你现在可以使用 64 个代理、255 个技能和 79 个命令。
|
||||||
|
|
||||||
### multi-* 命令需要额外配置
|
### multi-* 命令需要额外配置
|
||||||
|
|
||||||
|
|||||||
@ -96,7 +96,7 @@ Fix: What to change
|
|||||||
## Framework Checks
|
## Framework Checks
|
||||||
|
|
||||||
- **Laravel**: N+1 via `with()`/`load()`, `$fillable`/`$casts`, FormRequest validation, route model binding, `Gate`/`Policy` authorization, Sanctum token abilities, queue idempotency
|
- **Laravel**: N+1 via `with()`/`load()`, `$fillable`/`$casts`, FormRequest validation, route model binding, `Gate`/`Policy` authorization, Sanctum token abilities, queue idempotency
|
||||||
- **Livewire**: Proper `#[Rule]` attributes, authorization in ` authorize()`, wire:model security
|
- **Livewire**: Proper `#[Rule]` attributes, authorization in `authorize()`, wire:model security
|
||||||
- **Filament**: Form/table authorization, `canAccess()`, policy registration
|
- **Filament**: Form/table authorization, `canAccess()`, policy registration
|
||||||
- **Plain PHP**: PDO prepared statements, password_hash/password_verify, header-based CSRF
|
- **Plain PHP**: PDO prepared statements, password_hash/password_verify, header-based CSRF
|
||||||
|
|
||||||
|
|||||||
@ -70,18 +70,18 @@ public class OrderProcessingService {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ProcessingService {
|
public class ProcessingService {
|
||||||
|
|
||||||
public void processDocument(Document doc) {
|
public void processDocument(Document doc) {
|
||||||
LogContext logContext = CustomLog.getCurrentContext();
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
logContext.put("documentId", doc.getId().toString());
|
logContext.put("documentId", doc.getId().toString());
|
||||||
logContext.put("documentType", doc.getType());
|
logContext.put("documentType", doc.getType());
|
||||||
logContext.put("userId", SecurityContext.getUserId());
|
logContext.put("userId", SecurityContext.getUserId());
|
||||||
|
|
||||||
log.info("Iniciando procesamiento de documento");
|
log.info("Iniciando procesamiento de documento");
|
||||||
|
|
||||||
processInternal(doc);
|
processInternal(doc);
|
||||||
|
|
||||||
log.info("Procesamiento de documento completado");
|
log.info("Procesamiento de documento completado");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error en el procesamiento de documento", e);
|
log.error("Error en el procesamiento de documento", e);
|
||||||
@ -101,7 +101,7 @@ public class ProcessingService {
|
|||||||
<includeMdc>true</includeMdc>
|
<includeMdc>true</includeMdc>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="com.example" level="INFO"/>
|
<logger name="com.example" level="INFO"/>
|
||||||
<root level="WARN">
|
<root level="WARN">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
@ -118,7 +118,7 @@ public class ProcessingService {
|
|||||||
public class EventService {
|
public class EventService {
|
||||||
private final EventRepository eventRepository;
|
private final EventRepository eventRepository;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public void createSuccessEvent(Object payload, String eventType) {
|
public void createSuccessEvent(Object payload, String eventType) {
|
||||||
Objects.requireNonNull(payload, "El payload no puede ser null");
|
Objects.requireNonNull(payload, "El payload no puede ser null");
|
||||||
Event event = new Event();
|
Event event = new Event();
|
||||||
@ -126,11 +126,11 @@ public class EventService {
|
|||||||
event.setStatus(EventStatus.SUCCESS);
|
event.setStatus(EventStatus.SUCCESS);
|
||||||
event.setPayload(serializePayload(payload));
|
event.setPayload(serializePayload(payload));
|
||||||
event.setTimestamp(Instant.now());
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
eventRepository.persist(event);
|
eventRepository.persist(event);
|
||||||
log.info("Evento de éxito creado: {}", eventType);
|
log.info("Evento de éxito creado: {}", eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
||||||
Objects.requireNonNull(payload, "El payload no puede ser null");
|
Objects.requireNonNull(payload, "El payload no puede ser null");
|
||||||
if (errorMessage == null || errorMessage.isBlank()) {
|
if (errorMessage == null || errorMessage.isBlank()) {
|
||||||
@ -142,11 +142,11 @@ public class EventService {
|
|||||||
event.setErrorMessage(errorMessage);
|
event.setErrorMessage(errorMessage);
|
||||||
event.setPayload(serializePayload(payload));
|
event.setPayload(serializePayload(payload));
|
||||||
event.setTimestamp(Instant.now());
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
eventRepository.persist(event);
|
eventRepository.persist(event);
|
||||||
log.error("Evento de error creado: {} - {}", eventType, errorMessage);
|
log.error("Evento de error creado: {} - {}", eventType, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String serializePayload(Object payload) {
|
private String serializePayload(Object payload) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(payload);
|
return objectMapper.writeValueAsString(payload);
|
||||||
@ -165,10 +165,10 @@ public class EventService {
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class BusinessRulesPublisher {
|
public class BusinessRulesPublisher {
|
||||||
private final ProducerTemplate producerTemplate;
|
private final ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
public void publishSync(BusinessRulesPayload payload) {
|
public void publishSync(BusinessRulesPayload payload) {
|
||||||
producerTemplate.sendBody(
|
producerTemplate.sendBody(
|
||||||
"direct:business-rules-publisher",
|
"direct:business-rules-publisher",
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -180,23 +180,23 @@ public class BusinessRulesPublisher {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class BusinessRulesRoute extends RouteBuilder {
|
public class BusinessRulesRoute extends RouteBuilder {
|
||||||
|
|
||||||
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
String businessRulesQueue;
|
String businessRulesQueue;
|
||||||
|
|
||||||
@ConfigProperty(name = "rabbitmq.host")
|
@ConfigProperty(name = "rabbitmq.host")
|
||||||
String rabbitHost;
|
String rabbitHost;
|
||||||
|
|
||||||
@ConfigProperty(name = "rabbitmq.port")
|
@ConfigProperty(name = "rabbitmq.port")
|
||||||
Integer rabbitPort;
|
Integer rabbitPort;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
from("direct:business-rules-publisher")
|
from("direct:business-rules-publisher")
|
||||||
.routeId("business-rules-publisher")
|
.routeId("business-rules-publisher")
|
||||||
.log("Publicando mensaje en RabbitMQ: ${body}")
|
.log("Publicando mensaje en RabbitMQ: ${body}")
|
||||||
.marshal().json(JsonLibrary.Jackson)
|
.marshal().json(JsonLibrary.Jackson)
|
||||||
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
||||||
businessRulesQueue, rabbitHost, rabbitPort);
|
businessRulesQueue, rabbitHost, rabbitPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,14 +207,14 @@ public class BusinessRulesRoute extends RouteBuilder {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class DocumentProcessingRoute extends RouteBuilder {
|
public class DocumentProcessingRoute extends RouteBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
onException(ValidationException.class)
|
onException(ValidationException.class)
|
||||||
.handled(true)
|
.handled(true)
|
||||||
.to("direct:validation-error-handler")
|
.to("direct:validation-error-handler")
|
||||||
.log("Error de validación: ${exception.message}");
|
.log("Error de validación: ${exception.message}");
|
||||||
|
|
||||||
from("direct:process-document")
|
from("direct:process-document")
|
||||||
.routeId("document-processing")
|
.routeId("document-processing")
|
||||||
.log("Procesando documento: ${header.documentId}")
|
.log("Procesando documento: ${header.documentId}")
|
||||||
@ -237,19 +237,19 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class FileMonitoringRoute extends RouteBuilder {
|
public class FileMonitoringRoute extends RouteBuilder {
|
||||||
|
|
||||||
@ConfigProperty(name = "file.input.directory")
|
@ConfigProperty(name = "file.input.directory")
|
||||||
String inputDirectory;
|
String inputDirectory;
|
||||||
|
|
||||||
@ConfigProperty(name = "file.processed.directory")
|
@ConfigProperty(name = "file.processed.directory")
|
||||||
String processedDirectory;
|
String processedDirectory;
|
||||||
|
|
||||||
@ConfigProperty(name = "file.error.directory")
|
@ConfigProperty(name = "file.error.directory")
|
||||||
String errorDirectory;
|
String errorDirectory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
||||||
"&moveFailed=" + errorDirectory + "&delay=5000")
|
"&moveFailed=" + errorDirectory + "&delay=5000")
|
||||||
.routeId("file-monitor")
|
.routeId("file-monitor")
|
||||||
.log("Procesando archivo: ${header.CamelFileName}")
|
.log("Procesando archivo: ${header.CamelFileName}")
|
||||||
@ -302,7 +302,7 @@ public class DocumentResource {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class DocumentRepository implements PanacheRepository<Document> {
|
public class DocumentRepository implements PanacheRepository<Document> {
|
||||||
|
|
||||||
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
||||||
return find("status = ?1 order by createdAt desc", status)
|
return find("status = ?1 order by createdAt desc", status)
|
||||||
.page(page, size)
|
.page(page, size)
|
||||||
@ -331,11 +331,11 @@ public class DocumentService {
|
|||||||
document.setDescription(request.description());
|
document.setDescription(request.description());
|
||||||
document.setStatus(DocumentStatus.PENDING);
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
document.setCreatedAt(Instant.now());
|
document.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
repo.persist(document);
|
repo.persist(document);
|
||||||
|
|
||||||
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,7 +352,7 @@ public record CreateDocumentRequest(
|
|||||||
|
|
||||||
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
||||||
public static DocumentResponse from(Document document) {
|
public static DocumentResponse from(Document document) {
|
||||||
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
||||||
document.getStatus());
|
document.getStatus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -368,7 +368,7 @@ public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViol
|
|||||||
String message = exception.getConstraintViolations().stream()
|
String message = exception.getConstraintViolations().stream()
|
||||||
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
.collect(Collectors.joining(", "));
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
return Response.status(Response.Status.BAD_REQUEST)
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
.entity(Map.of("error", "validation_error", "message", message))
|
.entity(Map.of("error", "validation_error", "message", message))
|
||||||
.build();
|
.build();
|
||||||
@ -385,25 +385,25 @@ public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViol
|
|||||||
public class FileStorageService {
|
public class FileStorageService {
|
||||||
private final S3Client s3Client;
|
private final S3Client s3Client;
|
||||||
private final ExecutorService executorService;
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
||||||
InputStream inputStream,
|
InputStream inputStream,
|
||||||
long size,
|
long size,
|
||||||
LogContext logContext,
|
LogContext logContext,
|
||||||
InvoiceFormat format) {
|
InvoiceFormat format) {
|
||||||
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
String path = generateStoragePath(format);
|
String path = generateStoragePath(format);
|
||||||
|
|
||||||
PutObjectRequest request = PutObjectRequest.builder()
|
PutObjectRequest request = PutObjectRequest.builder()
|
||||||
.bucket(bucketName)
|
.bucket(bucketName)
|
||||||
.key(path)
|
.key(path)
|
||||||
.contentLength(size)
|
.contentLength(size)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
||||||
|
|
||||||
return new StoredDocumentInfo(path, size, Instant.now());
|
return new StoredDocumentInfo(path, size, Instant.now());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error al subir archivo a S3", e);
|
log.error("Error al subir archivo a S3", e);
|
||||||
|
|||||||
@ -28,7 +28,7 @@ Buenas prácticas para asegurar aplicaciones Quarkus con autenticación, autoriz
|
|||||||
@Path("/api/protected")
|
@Path("/api/protected")
|
||||||
@Authenticated
|
@Authenticated
|
||||||
public class ProtectedResource {
|
public class ProtectedResource {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
JsonWebToken jwt;
|
JsonWebToken jwt;
|
||||||
|
|
||||||
@ -65,19 +65,19 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
|||||||
@Provider
|
@Provider
|
||||||
@Priority(Priorities.AUTHENTICATION)
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
public class CustomAuthFilter implements ContainerRequestFilter {
|
public class CustomAuthFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity identity;
|
SecurityIdentity identity;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) {
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = authHeader.substring(7);
|
String token = authHeader.substring(7);
|
||||||
if (!validateToken(token)) {
|
if (!validateToken(token)) {
|
||||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
@ -98,7 +98,7 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
|||||||
@Path("/api/admin")
|
@Path("/api/admin")
|
||||||
@RolesAllowed("ADMIN")
|
@RolesAllowed("ADMIN")
|
||||||
public class AdminResource {
|
public class AdminResource {
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/users")
|
@Path("/users")
|
||||||
public List<UserDto> listUsers() {
|
public List<UserDto> listUsers() {
|
||||||
@ -116,7 +116,7 @@ public class AdminResource {
|
|||||||
|
|
||||||
@Path("/api/users")
|
@Path("/api/users")
|
||||||
public class UserResource {
|
public class UserResource {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ public class UserResource {
|
|||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
@RolesAllowed("USER")
|
@RolesAllowed("USER")
|
||||||
public Response getUser(@PathParam("id") Long id) {
|
public Response getUser(@PathParam("id") Long id) {
|
||||||
if (!securityIdentity.hasRole("ADMIN") &&
|
if (!securityIdentity.hasRole("ADMIN") &&
|
||||||
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||||
return Response.status(Response.Status.FORBIDDEN).build();
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
@ -138,7 +138,7 @@ public class UserResource {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class SecurityService {
|
public class SecurityService {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ public class SecurityService {
|
|||||||
if (securityIdentity.isAnonymous()) {
|
if (securityIdentity.isAnonymous()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (securityIdentity.hasRole("ADMIN")) {
|
if (securityIdentity.hasRole("ADMIN")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -216,7 +216,7 @@ List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
|||||||
Optional<User> user = User.find("username", username).firstResultOptional();
|
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||||
|
|
||||||
// BIEN: Parámetros nombrados
|
// BIEN: Parámetros nombrados
|
||||||
List<User> users = User.list("email = :email and age > :minAge",
|
List<User> users = User.list("email = :email and age > :minAge",
|
||||||
Parameters.with("email", email).and("minAge", 18));
|
Parameters.with("email", email).and("minAge", 18));
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -243,7 +243,7 @@ public class User extends PanacheEntity {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class PasswordService {
|
public class PasswordService {
|
||||||
|
|
||||||
public String hash(String plainPassword) {
|
public String hash(String plainPassword) {
|
||||||
return BcryptUtil.bcryptHash(plainPassword);
|
return BcryptUtil.bcryptHash(plainPassword);
|
||||||
}
|
}
|
||||||
@ -297,7 +297,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
|||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) {
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
String clientId = getClientIdentifier();
|
String clientId = getClientIdentifier();
|
||||||
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||||
k -> RateLimiter.create(100.0)); // 100 solicitudes por segundo
|
k -> RateLimiter.create(100.0)); // 100 solicitudes por segundo
|
||||||
|
|
||||||
if (!limiter.tryAcquire()) {
|
if (!limiter.tryAcquire()) {
|
||||||
@ -324,17 +324,17 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
|||||||
```java
|
```java
|
||||||
@Provider
|
@Provider
|
||||||
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||||
MultivaluedMap<String, Object> headers = response.getHeaders();
|
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||||
|
|
||||||
headers.putSingle("X-Frame-Options", "DENY");
|
headers.putSingle("X-Frame-Options", "DENY");
|
||||||
headers.putSingle("X-Content-Type-Options", "nosniff");
|
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||||
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||||
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
// CSP: evitar 'unsafe-inline' para script-src; usar nonces o hashes
|
// CSP: evitar 'unsafe-inline' para script-src; usar nonces o hashes
|
||||||
headers.putSingle("Content-Security-Policy",
|
headers.putSingle("Content-Security-Policy",
|
||||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,11 +351,11 @@ public class AuditService {
|
|||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
public void logAccess(String resource, String action) {
|
public void logAccess(String resource, String action) {
|
||||||
String user = securityIdentity.isAnonymous()
|
String user = securityIdentity.isAnonymous()
|
||||||
? "anonymous"
|
? "anonymous"
|
||||||
: securityIdentity.getPrincipal().getName();
|
: securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||||
user, action, resource, Instant.now());
|
user, action, resource, Instant.now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,19 +32,19 @@ Orientación TDD para servicios Quarkus 3.x con 80%+ de cobertura (unit + integr
|
|||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@DisplayName("Pruebas Unitarias de OrderService")
|
@DisplayName("Pruebas Unitarias de OrderService")
|
||||||
class OrderServiceTest {
|
class OrderServiceTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private OrderRepository orderRepository;
|
private OrderRepository orderRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EventService eventService;
|
private EventService eventService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private FulfillmentPublisher fulfillmentPublisher;
|
private FulfillmentPublisher fulfillmentPublisher;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private OrderService orderService;
|
private OrderService orderService;
|
||||||
|
|
||||||
private CreateOrderCommand validCommand;
|
private CreateOrderCommand validCommand;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -58,16 +58,16 @@ class OrderServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Pruebas para createOrder")
|
@DisplayName("Pruebas para createOrder")
|
||||||
class CreateOrder {
|
class CreateOrder {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Debe persistir orden y publicar evento de fulfillment")
|
@DisplayName("Debe persistir orden y publicar evento de fulfillment")
|
||||||
void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
|
void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
doNothing().when(orderRepository).persist(any(Order.class));
|
doNothing().when(orderRepository).persist(any(Order.class));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
OrderReceipt receipt = orderService.createOrder(validCommand);
|
OrderReceipt receipt = orderService.createOrder(validCommand);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(receipt).isNotNull();
|
assertThat(receipt).isNotNull();
|
||||||
assertThat(receipt.customerId()).isEqualTo("customer-123");
|
assertThat(receipt.customerId()).isEqualTo("customer-123");
|
||||||
@ -81,7 +81,7 @@ class OrderServiceTest {
|
|||||||
void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
|
void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
|
CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
WebApplicationException exception = assertThrows(
|
WebApplicationException exception = assertThrows(
|
||||||
WebApplicationException.class,
|
WebApplicationException.class,
|
||||||
@ -99,13 +99,13 @@ class OrderServiceTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
doThrow(new PersistenceException("base de datos no disponible"))
|
doThrow(new PersistenceException("base de datos no disponible"))
|
||||||
.when(orderRepository).persist(any(Order.class));
|
.when(orderRepository).persist(any(Order.class));
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
PersistenceException exception = assertThrows(
|
PersistenceException exception = assertThrows(
|
||||||
PersistenceException.class,
|
PersistenceException.class,
|
||||||
() -> orderService.createOrder(validCommand)
|
() -> orderService.createOrder(validCommand)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).contains("base de datos no disponible");
|
assertThat(exception.getMessage()).contains("base de datos no disponible");
|
||||||
verify(eventService).createErrorEvent(
|
verify(eventService).createErrorEvent(
|
||||||
eq(validCommand),
|
eq(validCommand),
|
||||||
@ -168,20 +168,20 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||||
mockRabbitMQ.expectedMessageCount(1);
|
mockRabbitMQ.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
advice.replaceFromWith("direct:business-rules-publisher");
|
advice.replaceFromWith("direct:business-rules-publisher");
|
||||||
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("business-rules-publisher");
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockRabbitMQ.assertIsSatisfied(5000);
|
mockRabbitMQ.assertIsSatisfied(5000);
|
||||||
|
|
||||||
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
||||||
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
|
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
assertThat(body).contains("\"documentId\":1");
|
assertThat(body).contains("\"documentId\":1");
|
||||||
@ -199,17 +199,17 @@ class EventServiceTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EventRepository eventRepository;
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private EventService eventService;
|
private EventService eventService;
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Pruebas para createSuccessEvent")
|
@DisplayName("Pruebas para createSuccessEvent")
|
||||||
class CreateSuccessEvent {
|
class CreateSuccessEvent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Debe crear evento de éxito con atributos correctos")
|
@DisplayName("Debe crear evento de éxito con atributos correctos")
|
||||||
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||||
@ -217,13 +217,13 @@ class EventServiceTest {
|
|||||||
BusinessRulesPayload testPayload = new BusinessRulesPayload();
|
BusinessRulesPayload testPayload = new BusinessRulesPayload();
|
||||||
testPayload.setDocumentId(1L);
|
testPayload.setDocumentId(1L);
|
||||||
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() ->
|
assertDoesNotThrow(() ->
|
||||||
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(eventRepository).persist(argThat(event ->
|
verify(eventRepository).persist(argThat(event ->
|
||||||
event.getType().equals("DOCUMENT_PROCESSED") &&
|
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||||
event.getStatus() == EventStatus.SUCCESS &&
|
event.getStatus() == EventStatus.SUCCESS &&
|
||||||
event.getTimestamp() != null
|
event.getTimestamp() != null
|
||||||
@ -235,13 +235,13 @@ class EventServiceTest {
|
|||||||
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
Object nullPayload = null;
|
Object nullPayload = null;
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
NullPointerException exception = assertThrows(
|
NullPointerException exception = assertThrows(
|
||||||
NullPointerException.class,
|
NullPointerException.class,
|
||||||
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
||||||
verify(eventRepository, never()).persist(any());
|
verify(eventRepository, never()).persist(any());
|
||||||
}
|
}
|
||||||
@ -250,7 +250,7 @@ class EventServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Pruebas para createErrorEvent")
|
@DisplayName("Pruebas para createErrorEvent")
|
||||||
class CreateErrorEvent {
|
class CreateErrorEvent {
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@DisplayName("Debe rechazar mensajes de error inválidos")
|
@DisplayName("Debe rechazar mensajes de error inválidos")
|
||||||
@ValueSource(strings = {"", " "})
|
@ValueSource(strings = {"", " "})
|
||||||
@ -263,7 +263,7 @@ class EventServiceTest {
|
|||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,10 +278,10 @@ class FileStorageServiceTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private S3Client s3Client;
|
private S3Client s3Client;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExecutorService executorService;
|
private ExecutorService executorService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private FileStorageService fileStorageService;
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
@ -293,15 +293,15 @@ class FileStorageServiceTest {
|
|||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
.thenThrow(new StorageException("S3 no disponible"));
|
.thenThrow(new StorageException("S3 no disponible"));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
CompletableFuture<StoredDocumentInfo> future =
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL);
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThatThrownBy(() -> future.join())
|
assertThatThrownBy(() -> future.join())
|
||||||
.isInstanceOf(CompletionException.class)
|
.isInstanceOf(CompletionException.class)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ Node.js の `child_process.spawn` で `.sh` ファイルを直接実行すると
|
|||||||
**EFTYPE** で失敗する:
|
**EFTYPE** で失敗する:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
spawn('C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh',
|
spawn('C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh',
|
||||||
['post'], {stdio:['pipe','pipe','pipe']});
|
['post'], {stdio:['pipe','pipe','pipe']});
|
||||||
// → Error: spawn EFTYPE (errno -4028)
|
// → Error: spawn EFTYPE (errno -4028)
|
||||||
```
|
```
|
||||||
|
|||||||
@ -119,7 +119,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stdio トランスポートを介して agentpay MCP サーバーに接続する。
|
// stdio トランスポートを介して agentpay MCP サーバーに接続する。
|
||||||
// サーバーが必要とする env 変数のみをホワイトリストに登録する —
|
// サーバーが必要とする env 変数のみをホワイトリストに登録する —
|
||||||
// 秘密鍵を管理するサードパーティのサブプロセスに process.env のすべてを渡さない。
|
// 秘密鍵を管理するサードパーティのサブプロセスに process.env のすべてを渡さない。
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
command: "npx",
|
command: "npx",
|
||||||
|
|||||||
@ -96,11 +96,11 @@ less pi-hole-install.sh # review before proceeding
|
|||||||
bash pi-hole-install.sh
|
bash pi-hole-install.sh
|
||||||
|
|
||||||
# Follow the interactive installer:
|
# Follow the interactive installer:
|
||||||
# 1. Select network interface (eth0 for wired — recommended)
|
# 1. Select network interface (eth0 for wired — recommended)
|
||||||
# 2. Select upstream DNS (Cloudflare or leave default — can change later)
|
# 2. Select upstream DNS (Cloudflare or leave default — can change later)
|
||||||
# 3. Confirm static IP
|
# 3. Confirm static IP
|
||||||
# 4. Install the web admin interface (recommended)
|
# 4. Install the web admin interface (recommended)
|
||||||
# 5. Note the admin password shown at the end
|
# 5. Note the admin password shown at the end
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pointing Your Network at Pi-hole
|
## Pointing Your Network at Pi-hole
|
||||||
@ -183,9 +183,9 @@ sudo systemctl start cloudflared
|
|||||||
sudo systemctl enable cloudflared
|
sudo systemctl enable cloudflared
|
||||||
|
|
||||||
# Now point Pi-hole at the local DoH proxy:
|
# Now point Pi-hole at the local DoH proxy:
|
||||||
# Pi-hole admin → Settings → DNS → Custom upstream DNS
|
# Pi-hole admin → Settings → DNS → Custom upstream DNS
|
||||||
# Set to: 127.0.0.1#5053
|
# Set to: 127.0.0.1#5053
|
||||||
# Uncheck all other upstream resolvers
|
# Uncheck all other upstream resolvers
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local DNS Records
|
## Local DNS Records
|
||||||
|
|||||||
@ -218,8 +218,8 @@ stays reachable after an IP change.
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# ddns.env (chmod 600, not committed to git):
|
# ddns.env (chmod 600, not committed to git):
|
||||||
# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id
|
# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id
|
||||||
# SETTINGS_CLOUDFLARE_TOKEN=your_api_token
|
# SETTINGS_CLOUDFLARE_TOKEN=your_api_token
|
||||||
|
|
||||||
# Option 2: DuckDNS (free, simple)
|
# Option 2: DuckDNS (free, simple)
|
||||||
Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org)
|
Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org)
|
||||||
|
|||||||
@ -28,7 +28,7 @@ origin: ECC
|
|||||||
@Path("/api/protected")
|
@Path("/api/protected")
|
||||||
@Authenticated
|
@Authenticated
|
||||||
public class ProtectedResource {
|
public class ProtectedResource {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
JsonWebToken jwt;
|
JsonWebToken jwt;
|
||||||
|
|
||||||
@ -65,20 +65,20 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
|||||||
@Provider
|
@Provider
|
||||||
@Priority(Priorities.AUTHENTICATION)
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
public class CustomAuthFilter implements ContainerRequestFilter {
|
public class CustomAuthFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity identity;
|
SecurityIdentity identity;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) {
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
// ヘッダーが無いまたは不正形式の場合は即座に拒否
|
// ヘッダーが無いまたは不正形式の場合は即座に拒否
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = authHeader.substring(7);
|
String token = authHeader.substring(7);
|
||||||
if (!validateToken(token)) {
|
if (!validateToken(token)) {
|
||||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
@ -100,7 +100,7 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
|||||||
@Path("/api/admin")
|
@Path("/api/admin")
|
||||||
@RolesAllowed("ADMIN")
|
@RolesAllowed("ADMIN")
|
||||||
public class AdminResource {
|
public class AdminResource {
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/users")
|
@Path("/users")
|
||||||
public List<UserDto> listUsers() {
|
public List<UserDto> listUsers() {
|
||||||
@ -118,7 +118,7 @@ public class AdminResource {
|
|||||||
|
|
||||||
@Path("/api/users")
|
@Path("/api/users")
|
||||||
public class UserResource {
|
public class UserResource {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ public class UserResource {
|
|||||||
@RolesAllowed("USER")
|
@RolesAllowed("USER")
|
||||||
public Response getUser(@PathParam("id") Long id) {
|
public Response getUser(@PathParam("id") Long id) {
|
||||||
// 所有権確認
|
// 所有権確認
|
||||||
if (!securityIdentity.hasRole("ADMIN") &&
|
if (!securityIdentity.hasRole("ADMIN") &&
|
||||||
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||||
return Response.status(Response.Status.FORBIDDEN).build();
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
@ -145,7 +145,7 @@ public class UserResource {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class SecurityService {
|
public class SecurityService {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ public class SecurityService {
|
|||||||
if (securityIdentity.isAnonymous()) {
|
if (securityIdentity.isAnonymous()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (securityIdentity.hasRole("ADMIN")) {
|
if (securityIdentity.hasRole("ADMIN")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -229,7 +229,7 @@ List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
|||||||
Optional<User> user = User.find("username", username).firstResultOptional();
|
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||||
|
|
||||||
// 良い例:名前付きパラメータ
|
// 良い例:名前付きパラメータ
|
||||||
List<User> users = User.list("email = :email and age > :minAge",
|
List<User> users = User.list("email = :email and age > :minAge",
|
||||||
Parameters.with("email", email).and("minAge", 18));
|
Parameters.with("email", email).and("minAge", 18));
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -256,7 +256,7 @@ public class User extends PanacheEntity {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class PasswordService {
|
public class PasswordService {
|
||||||
|
|
||||||
public String hash(String plainPassword) {
|
public String hash(String plainPassword) {
|
||||||
return BcryptUtil.bcryptHash(plainPassword);
|
return BcryptUtil.bcryptHash(plainPassword);
|
||||||
}
|
}
|
||||||
@ -324,7 +324,7 @@ quarkus.vault.authentication.kubernetes.role=my-role
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class SecretService {
|
public class SecretService {
|
||||||
|
|
||||||
@ConfigProperty(name = "api-key")
|
@ConfigProperty(name = "api-key")
|
||||||
String apiKey; // Vault から取得
|
String apiKey; // Vault から取得
|
||||||
|
|
||||||
@ -350,7 +350,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
|||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) {
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
String clientId = getClientIdentifier();
|
String clientId = getClientIdentifier();
|
||||||
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||||
k -> RateLimiter.create(100.0)); // 1秒あたり100リクエスト
|
k -> RateLimiter.create(100.0)); // 1秒あたり100リクエスト
|
||||||
|
|
||||||
if (!limiter.tryAcquire()) {
|
if (!limiter.tryAcquire()) {
|
||||||
@ -376,25 +376,25 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
|||||||
```java
|
```java
|
||||||
@Provider
|
@Provider
|
||||||
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||||
MultivaluedMap<String, Object> headers = response.getHeaders();
|
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||||
|
|
||||||
// クリックジャッキング防止
|
// クリックジャッキング防止
|
||||||
headers.putSingle("X-Frame-Options", "DENY");
|
headers.putSingle("X-Frame-Options", "DENY");
|
||||||
|
|
||||||
// XSS保護
|
// XSS保護
|
||||||
headers.putSingle("X-Content-Type-Options", "nosniff");
|
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||||
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||||
|
|
||||||
// HSTS
|
// HSTS
|
||||||
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
|
||||||
// CSP — script-src用の'unsafe-inline'は避けてください。XSS保護を無効化します。
|
// CSP — script-src用の'unsafe-inline'は避けてください。XSS保護を無効化します。
|
||||||
// 代わりにnoncesまたはhashesを使用します。CSSフレームワークが必要な場合、
|
// 代わりにnoncesまたはhashesを使用します。CSSフレームワークが必要な場合、
|
||||||
// style-srcの'unsafe-inline'は許容ですが、可能な場合はnoncesを優先してください。
|
// style-srcの'unsafe-inline'は許容ですが、可能な場合はnoncesを優先してください。
|
||||||
headers.putSingle("Content-Security-Policy",
|
headers.putSingle("Content-Security-Policy",
|
||||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -411,11 +411,11 @@ public class AuditService {
|
|||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
public void logAccess(String resource, String action) {
|
public void logAccess(String resource, String action) {
|
||||||
String user = securityIdentity.isAnonymous()
|
String user = securityIdentity.isAnonymous()
|
||||||
? "anonymous"
|
? "anonymous"
|
||||||
: securityIdentity.getPrincipal().getName();
|
: securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||||
user, action, resource, Instant.now());
|
user, action, resource, Instant.now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,19 +34,19 @@ origin: ECC
|
|||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@DisplayName("OrderService Unit Tests")
|
@DisplayName("OrderService Unit Tests")
|
||||||
class OrderServiceTest {
|
class OrderServiceTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private OrderRepository orderRepository;
|
private OrderRepository orderRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EventService eventService;
|
private EventService eventService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private FulfillmentPublisher fulfillmentPublisher;
|
private FulfillmentPublisher fulfillmentPublisher;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private OrderService orderService;
|
private OrderService orderService;
|
||||||
|
|
||||||
private CreateOrderCommand validCommand;
|
private CreateOrderCommand validCommand;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -60,16 +60,16 @@ class OrderServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("createOrder のテスト")
|
@DisplayName("createOrder のテスト")
|
||||||
class CreateOrder {
|
class CreateOrder {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("有効なコマンドが与えられた場合、注文を永続化してフルフィルメントイベントを発行する")
|
@DisplayName("有効なコマンドが与えられた場合、注文を永続化してフルフィルメントイベントを発行する")
|
||||||
void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
|
void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
doNothing().when(orderRepository).persist(any(Order.class));
|
doNothing().when(orderRepository).persist(any(Order.class));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
OrderReceipt receipt = orderService.createOrder(validCommand);
|
OrderReceipt receipt = orderService.createOrder(validCommand);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(receipt).isNotNull();
|
assertThat(receipt).isNotNull();
|
||||||
assertThat(receipt.customerId()).isEqualTo("customer-123");
|
assertThat(receipt.customerId()).isEqualTo("customer-123");
|
||||||
@ -83,7 +83,7 @@ class OrderServiceTest {
|
|||||||
void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
|
void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
|
CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
WebApplicationException exception = assertThrows(
|
WebApplicationException exception = assertThrows(
|
||||||
WebApplicationException.class,
|
WebApplicationException.class,
|
||||||
@ -101,13 +101,13 @@ class OrderServiceTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
doThrow(new PersistenceException("database unavailable"))
|
doThrow(new PersistenceException("database unavailable"))
|
||||||
.when(orderRepository).persist(any(Order.class));
|
.when(orderRepository).persist(any(Order.class));
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
PersistenceException exception = assertThrows(
|
PersistenceException exception = assertThrows(
|
||||||
PersistenceException.class,
|
PersistenceException.class,
|
||||||
() -> orderService.createOrder(validCommand)
|
() -> orderService.createOrder(validCommand)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).contains("database unavailable");
|
assertThat(exception.getMessage()).contains("database unavailable");
|
||||||
verify(eventService).createErrorEvent(
|
verify(eventService).createErrorEvent(
|
||||||
eq(validCommand),
|
eq(validCommand),
|
||||||
@ -125,7 +125,7 @@ class OrderServiceTest {
|
|||||||
NullPointerException.class,
|
NullPointerException.class,
|
||||||
() -> orderService.createOrder(null)
|
() -> orderService.createOrder(null)
|
||||||
);
|
);
|
||||||
|
|
||||||
verify(orderRepository, never()).persist(any(Order.class));
|
verify(orderRepository, never()).persist(any(Order.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||||
mockRabbitMQ.expectedMessageCount(1);
|
mockRabbitMQ.expectedMessageCount(1);
|
||||||
|
|
||||||
// テスト用の実エンドポイントをモックに置き換え
|
// テスト用の実エンドポイントをモックに置き換え
|
||||||
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
@ -192,13 +192,13 @@ class BusinessRulesRouteTest {
|
|||||||
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("business-rules-publisher");
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
// ASSERT — .marshal().json(JsonLibrary.Jackson)の後、bodyはJSON文字列
|
// ASSERT — .marshal().json(JsonLibrary.Jackson)の後、bodyはJSON文字列
|
||||||
mockRabbitMQ.assertIsSatisfied(5000);
|
mockRabbitMQ.assertIsSatisfied(5000);
|
||||||
|
|
||||||
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
||||||
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
|
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
assertThat(body).contains("\"documentId\":1");
|
assertThat(body).contains("\"documentId\":1");
|
||||||
@ -211,19 +211,19 @@ class BusinessRulesRouteTest {
|
|||||||
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
||||||
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
||||||
mockMarshal.expectedMessageCount(1);
|
mockMarshal.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
advice.weaveAddLast().to("mock:marshal");
|
advice.weaveAddLast().to("mock:marshal");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("business-rules-publisher");
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockMarshal.assertIsSatisfied(5000);
|
mockMarshal.assertIsSatisfied(5000);
|
||||||
|
|
||||||
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
assertThat(body).contains("\"documentId\":1");
|
assertThat(body).contains("\"documentId\":1");
|
||||||
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
||||||
@ -240,17 +240,17 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
||||||
mockInvoice.expectedMessageCount(1);
|
mockInvoice.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("document-processing");
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("document-processing");
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBodyAndHeader("direct:process-document",
|
producerTemplate.sendBodyAndHeader("direct:process-document",
|
||||||
testPayload, "documentType", "INVOICE");
|
testPayload, "documentType", "INVOICE");
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockInvoice.assertIsSatisfied(5000);
|
mockInvoice.assertIsSatisfied(5000);
|
||||||
}
|
}
|
||||||
@ -261,23 +261,23 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
||||||
mockError.expectedMessageCount(1);
|
mockError.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("document-processing");
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
advice.weaveByToString(".*direct:validation-error-handler.*")
|
advice.weaveByToString(".*direct:validation-error-handler.*")
|
||||||
.replace().to("mock:error");
|
.replace().to("mock:error");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("document-processing");
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
// バリデータビーンをモック化して例外をスロー
|
// バリデータビーンをモック化して例外をスロー
|
||||||
when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:process-document", testPayload);
|
producerTemplate.sendBody("direct:process-document", testPayload);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockError.assertIsSatisfied(5000);
|
mockError.assertIsSatisfied(5000);
|
||||||
|
|
||||||
Exception exception = mockError.getExchanges().get(0).getException();
|
Exception exception = mockError.getExchanges().get(0).getException();
|
||||||
assertThat(exception).isInstanceOf(ValidationException.class);
|
assertThat(exception).isInstanceOf(ValidationException.class);
|
||||||
assertThat(exception.getMessage()).contains("Invalid document");
|
assertThat(exception.getMessage()).contains("Invalid document");
|
||||||
@ -295,13 +295,13 @@ class EventServiceTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EventRepository eventRepository;
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private EventService eventService;
|
private EventService eventService;
|
||||||
|
|
||||||
private BusinessRulesPayload testPayload;
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -314,19 +314,19 @@ class EventServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("createSuccessEvent のテスト")
|
@DisplayName("createSuccessEvent のテスト")
|
||||||
class CreateSuccessEvent {
|
class CreateSuccessEvent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("有効なペイロードが与えられた場合、正しい属性でサクセスイベント作成")
|
@DisplayName("有効なペイロードが与えられた場合、正しい属性でサクセスイベント作成")
|
||||||
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() ->
|
assertDoesNotThrow(() ->
|
||||||
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(eventRepository).persist(argThat(event ->
|
verify(eventRepository).persist(argThat(event ->
|
||||||
event.getType().equals("DOCUMENT_PROCESSED") &&
|
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||||
event.getStatus() == EventStatus.SUCCESS &&
|
event.getStatus() == EventStatus.SUCCESS &&
|
||||||
event.getPayload().equals("{\"documentId\":1}") &&
|
event.getPayload().equals("{\"documentId\":1}") &&
|
||||||
@ -339,13 +339,13 @@ class EventServiceTest {
|
|||||||
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
Object nullPayload = null;
|
Object nullPayload = null;
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
NullPointerException exception = assertThrows(
|
NullPointerException exception = assertThrows(
|
||||||
NullPointerException.class,
|
NullPointerException.class,
|
||||||
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
||||||
verify(eventRepository, never()).persist(any());
|
verify(eventRepository, never()).persist(any());
|
||||||
}
|
}
|
||||||
@ -354,20 +354,20 @@ class EventServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("createErrorEvent のテスト")
|
@DisplayName("createErrorEvent のテスト")
|
||||||
class CreateErrorEvent {
|
class CreateErrorEvent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("エラーが与えられた場合、エラーメッセージ付きエラーイベント作成")
|
@DisplayName("エラーが与えられた場合、エラーメッセージ付きエラーイベント作成")
|
||||||
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
String errorMessage = "Processing failed";
|
String errorMessage = "Processing failed";
|
||||||
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() ->
|
assertDoesNotThrow(() ->
|
||||||
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(eventRepository).persist(argThat(event ->
|
verify(eventRepository).persist(argThat(event ->
|
||||||
event.getType().equals("PROCESSING_ERROR") &&
|
event.getType().equals("PROCESSING_ERROR") &&
|
||||||
event.getStatus() == EventStatus.ERROR &&
|
event.getStatus() == EventStatus.ERROR &&
|
||||||
event.getErrorMessage().equals(errorMessage) &&
|
event.getErrorMessage().equals(errorMessage) &&
|
||||||
@ -384,7 +384,7 @@ class EventServiceTest {
|
|||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,13 +400,13 @@ class FileStorageServiceTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private S3Client s3Client;
|
private S3Client s3Client;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExecutorService executorService;
|
private ExecutorService executorService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private FileStorageService fileStorageService;
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
private InputStream testInputStream;
|
private InputStream testInputStream;
|
||||||
private LogContext testLogContext;
|
private LogContext testLogContext;
|
||||||
|
|
||||||
@ -421,7 +421,7 @@ class FileStorageServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("uploadOriginalFile のテスト")
|
@DisplayName("uploadOriginalFile のテスト")
|
||||||
class UploadOriginalFile {
|
class UploadOriginalFile {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("有効なファイルが与えられた場合、ファイルアップロード成功とドキュメント情報を返す")
|
@DisplayName("有効なファイルが与えられた場合、ファイルアップロード成功とドキュメント情報を返す")
|
||||||
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
||||||
@ -430,23 +430,23 @@ class FileStorageServiceTest {
|
|||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
.thenReturn(PutObjectResponse.builder().build());
|
.thenReturn(PutObjectResponse.builder().build());
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
CompletableFuture<StoredDocumentInfo> future =
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL);
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
StoredDocumentInfo result = future.join();
|
StoredDocumentInfo result = future.join();
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getPath()).isNotBlank();
|
assertThat(result.getPath()).isNotBlank();
|
||||||
assertThat(result.getSize()).isEqualTo(1024L);
|
assertThat(result.getSize()).isEqualTo(1024L);
|
||||||
assertThat(result.getUploadedAt()).isNotNull();
|
assertThat(result.getUploadedAt()).isNotNull();
|
||||||
|
|
||||||
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,15 +458,15 @@ class FileStorageServiceTest {
|
|||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
.thenThrow(new StorageException("S3 unavailable"));
|
.thenThrow(new StorageException("S3 unavailable"));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
CompletableFuture<StoredDocumentInfo> future =
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL);
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThatThrownBy(() -> future.join())
|
assertThatThrownBy(() -> future.join())
|
||||||
.isInstanceOf(CompletionException.class)
|
.isInstanceOf(CompletionException.class)
|
||||||
@ -479,17 +479,17 @@ class FileStorageServiceTest {
|
|||||||
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
||||||
|
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
capturedContext.set(CustomLog.getCurrentContext());
|
capturedContext.set(CustomLog.getCurrentContext());
|
||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL).join();
|
testLogContext, InvoiceFormat.UBL).join();
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(capturedContext.get()).isNotNull();
|
assertThat(capturedContext.get()).isNotNull();
|
||||||
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
||||||
@ -641,7 +641,7 @@ class DocumentIntegrationTest {
|
|||||||
<goal>prepare-agent</goal>
|
<goal>prepare-agent</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
|
|
||||||
<!-- カバレッジレポート生成 -->
|
<!-- カバレッジレポート生成 -->
|
||||||
<execution>
|
<execution>
|
||||||
<id>report</id>
|
<id>report</id>
|
||||||
@ -650,7 +650,7 @@ class DocumentIntegrationTest {
|
|||||||
<goal>report</goal>
|
<goal>report</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
|
|
||||||
<!-- カバレッジ閾値を強制 -->
|
<!-- カバレッジ閾値を強制 -->
|
||||||
<execution>
|
<execution>
|
||||||
<id>check</id>
|
<id>check</id>
|
||||||
@ -705,14 +705,14 @@ mvn jacoco:check
|
|||||||
<artifactId>quarkus-junit5-mockito</artifactId>
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Mockito -->
|
<!-- Mockito -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- AssertJ(JUnitアサーション推奨) -->
|
<!-- AssertJ(JUnitアサーション推奨) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.assertj</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
@ -720,14 +720,14 @@ mvn jacoco:check
|
|||||||
<version>3.24.2</version>
|
<version>3.24.2</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- REST Assured -->
|
<!-- REST Assured -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.rest-assured</groupId>
|
<groupId>io.rest-assured</groupId>
|
||||||
<artifactId>rest-assured</artifactId>
|
<artifactId>rest-assured</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Camel Testing -->
|
<!-- Camel Testing -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.camel.quarkus</groupId>
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
|||||||
@ -437,28 +437,28 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Cache Maven packages
|
- name: Cache Maven packages
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.m2
|
path: ~/.m2
|
||||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: mvn clean verify -DskipTests
|
run: mvn clean verify -DskipTests
|
||||||
|
|
||||||
- name: Test with Coverage
|
- name: Test with Coverage
|
||||||
run: mvn test jacoco:report jacoco:check
|
run: mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
- name: Security Scan
|
- name: Security Scan
|
||||||
run: mvn org.owasp:dependency-check-maven:check
|
run: mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
- name: Upload Coverage
|
- name: Upload Coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@ -48,52 +48,52 @@ public class As2ProcessingService {
|
|||||||
public void processFile(Path filePath) throws Exception {
|
public void processFile(Path filePath) throws Exception {
|
||||||
LogContext logContext = CustomLog.getCurrentContext();
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
|
|
||||||
String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID);
|
String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID);
|
||||||
|
|
||||||
// Koşullu akış mantığı
|
// Koşullu akış mantığı
|
||||||
boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW));
|
boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW));
|
||||||
log.info("Is CHORUS_FLOW message: {}", isChorusFlow);
|
log.info("Is CHORUS_FLOW message: {}", isChorusFlow);
|
||||||
|
|
||||||
ValidationFlowConfig validationFlowConfig = isChorusFlow
|
ValidationFlowConfig validationFlowConfig = isChorusFlow
|
||||||
? ValidationFlowConfig.xsdOnly()
|
? ValidationFlowConfig.xsdOnly()
|
||||||
: ValidationFlowConfig.allValidations();
|
: ValidationFlowConfig.allValidations();
|
||||||
|
|
||||||
InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator
|
InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator
|
||||||
.validateFlowWithConfig(filePath, validationFlowConfig,
|
.validateFlowWithConfig(filePath, validationFlowConfig,
|
||||||
EInvoiceSyntaxFormat.UBL, logContext);
|
EInvoiceSyntaxFormat.UBL, logContext);
|
||||||
|
|
||||||
FlowProfile flowProfile = isChorusFlow ?
|
FlowProfile flowProfile = isChorusFlow ?
|
||||||
FlowProfile.EXTENDED_CTC_FR :
|
FlowProfile.EXTENDED_CTC_FR :
|
||||||
this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult,
|
this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult,
|
||||||
invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile());
|
invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile());
|
||||||
|
|
||||||
log.info("Invoice validation completed. Message is valid");
|
log.info("Invoice validation completed. Message is valid");
|
||||||
|
|
||||||
// CompletableFuture async işlemi
|
// CompletableFuture async işlemi
|
||||||
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
try(InputStream inputStream = Files.newInputStream(filePath)) {
|
||||||
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
CompletableFuture<StoredDocumentInfo> documentInfoCompletableFuture =
|
||||||
fileStorageService.uploadOriginalFile(inputStream,
|
fileStorageService.uploadOriginalFile(inputStream,
|
||||||
invoiceValidationResult.getSize(), logContext,
|
invoiceValidationResult.getSize(), logContext,
|
||||||
invoiceValidationResult.getInvoiceFormat());
|
invoiceValidationResult.getInvoiceFormat());
|
||||||
|
|
||||||
StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join();
|
StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join();
|
||||||
log.info("File uploaded successfully: {}", documentInfo.getPath());
|
log.info("File uploaded successfully: {}", documentInfo.getPath());
|
||||||
|
|
||||||
if (StringUtils.isBlank(documentInfo.getPath())) {
|
if (StringUtils.isBlank(documentInfo.getPath())) {
|
||||||
String errorMsg = "File path is empty after upload";
|
String errorMsg = "File path is empty after upload";
|
||||||
log.error(errorMsg);
|
log.error(errorMsg);
|
||||||
this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg);
|
this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg);
|
||||||
throw new As2ServerProcessingException(errorMsg);
|
throw new As2ServerProcessingException(errorMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE");
|
this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE");
|
||||||
|
|
||||||
String originalFileName = documentInfo.getOriginalFileName();
|
String originalFileName = documentInfo.getOriginalFileName();
|
||||||
BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities(
|
BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities(
|
||||||
documentInfo, originalFileName, structureIdPartner,
|
documentInfo, originalFileName, structureIdPartner,
|
||||||
flowProfile, invoiceValidationResult.getDocumentHash());
|
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||||
|
|
||||||
// Async Camel yayınlama
|
// Async Camel yayınlama
|
||||||
businessRulesPublisher.publishAsync(payload);
|
businessRulesPublisher.publishAsync(payload);
|
||||||
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||||
@ -117,7 +117,7 @@ public class As2ProcessingService {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ProcessingService {
|
public class ProcessingService {
|
||||||
|
|
||||||
public void processDocument(Document doc) {
|
public void processDocument(Document doc) {
|
||||||
LogContext logContext = CustomLog.getCurrentContext();
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
@ -125,12 +125,12 @@ public class ProcessingService {
|
|||||||
logContext.put("documentId", doc.getId().toString());
|
logContext.put("documentId", doc.getId().toString());
|
||||||
logContext.put("documentType", doc.getType());
|
logContext.put("documentType", doc.getType());
|
||||||
logContext.put("userId", SecurityContext.getUserId());
|
logContext.put("userId", SecurityContext.getUserId());
|
||||||
|
|
||||||
log.info("Starting document processing");
|
log.info("Starting document processing");
|
||||||
|
|
||||||
// Bu kapsam içindeki tüm loglar bağlamı devralır
|
// Bu kapsam içindeki tüm loglar bağlamı devralır
|
||||||
processInternal(doc);
|
processInternal(doc);
|
||||||
|
|
||||||
log.info("Document processing completed");
|
log.info("Document processing completed");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Document processing failed", e);
|
log.error("Document processing failed", e);
|
||||||
@ -150,7 +150,7 @@ public class ProcessingService {
|
|||||||
<includeMdc>true</includeMdc>
|
<includeMdc>true</includeMdc>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="com.example" level="INFO"/>
|
<logger name="com.example" level="INFO"/>
|
||||||
<root level="WARN">
|
<root level="WARN">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
@ -167,7 +167,7 @@ public class ProcessingService {
|
|||||||
public class EventService {
|
public class EventService {
|
||||||
private final EventRepository eventRepository;
|
private final EventRepository eventRepository;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public void createSuccessEvent(Object payload, String eventType) {
|
public void createSuccessEvent(Object payload, String eventType) {
|
||||||
Objects.requireNonNull(payload, "Payload cannot be null");
|
Objects.requireNonNull(payload, "Payload cannot be null");
|
||||||
Event event = new Event();
|
Event event = new Event();
|
||||||
@ -175,11 +175,11 @@ public class EventService {
|
|||||||
event.setStatus(EventStatus.SUCCESS);
|
event.setStatus(EventStatus.SUCCESS);
|
||||||
event.setPayload(serializePayload(payload));
|
event.setPayload(serializePayload(payload));
|
||||||
event.setTimestamp(Instant.now());
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
eventRepository.persist(event);
|
eventRepository.persist(event);
|
||||||
log.info("Success event created: {}", eventType);
|
log.info("Success event created: {}", eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
||||||
Objects.requireNonNull(payload, "Payload cannot be null");
|
Objects.requireNonNull(payload, "Payload cannot be null");
|
||||||
if (errorMessage == null || errorMessage.isBlank()) {
|
if (errorMessage == null || errorMessage.isBlank()) {
|
||||||
@ -191,11 +191,11 @@ public class EventService {
|
|||||||
event.setErrorMessage(errorMessage);
|
event.setErrorMessage(errorMessage);
|
||||||
event.setPayload(serializePayload(payload));
|
event.setPayload(serializePayload(payload));
|
||||||
event.setTimestamp(Instant.now());
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
eventRepository.persist(event);
|
eventRepository.persist(event);
|
||||||
log.error("Error event created: {} - {}", eventType, errorMessage);
|
log.error("Error event created: {} - {}", eventType, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String serializePayload(Object payload) {
|
private String serializePayload(Object payload) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(payload);
|
return objectMapper.writeValueAsString(payload);
|
||||||
@ -213,21 +213,21 @@ public class EventService {
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class BusinessRulesPublisher {
|
public class BusinessRulesPublisher {
|
||||||
private final ProducerTemplate producerTemplate;
|
private final ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
String businessRulesQueue;
|
String businessRulesQueue;
|
||||||
|
|
||||||
public void publishAsync(BusinessRulesPayload payload) {
|
public void publishAsync(BusinessRulesPayload payload) {
|
||||||
producerTemplate.asyncSendBody(
|
producerTemplate.asyncSendBody(
|
||||||
"direct:business-rules-publisher",
|
"direct:business-rules-publisher",
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
log.info("Message published to business rules queue: {}", payload.getDocumentId());
|
log.info("Message published to business rules queue: {}", payload.getDocumentId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void publishSync(BusinessRulesPayload payload) {
|
public void publishSync(BusinessRulesPayload payload) {
|
||||||
producerTemplate.sendBody(
|
producerTemplate.sendBody(
|
||||||
"direct:business-rules-publisher",
|
"direct:business-rules-publisher",
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -239,23 +239,23 @@ public class BusinessRulesPublisher {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class BusinessRulesRoute extends RouteBuilder {
|
public class BusinessRulesRoute extends RouteBuilder {
|
||||||
|
|
||||||
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
String businessRulesQueue;
|
String businessRulesQueue;
|
||||||
|
|
||||||
@ConfigProperty(name = "rabbitmq.host")
|
@ConfigProperty(name = "rabbitmq.host")
|
||||||
String rabbitHost;
|
String rabbitHost;
|
||||||
|
|
||||||
@ConfigProperty(name = "rabbitmq.port")
|
@ConfigProperty(name = "rabbitmq.port")
|
||||||
Integer rabbitPort;
|
Integer rabbitPort;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
from("direct:business-rules-publisher")
|
from("direct:business-rules-publisher")
|
||||||
.routeId("business-rules-publisher")
|
.routeId("business-rules-publisher")
|
||||||
.log("Publishing message to RabbitMQ: ${body}")
|
.log("Publishing message to RabbitMQ: ${body}")
|
||||||
.marshal().json(JsonLibrary.Jackson)
|
.marshal().json(JsonLibrary.Jackson)
|
||||||
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
||||||
businessRulesQueue, rabbitHost, rabbitPort);
|
businessRulesQueue, rabbitHost, rabbitPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,7 +266,7 @@ public class BusinessRulesRoute extends RouteBuilder {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class DocumentProcessingRoute extends RouteBuilder {
|
public class DocumentProcessingRoute extends RouteBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
// Hata yönetimi
|
// Hata yönetimi
|
||||||
@ -274,7 +274,7 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
|||||||
.handled(true)
|
.handled(true)
|
||||||
.to("direct:validation-error-handler")
|
.to("direct:validation-error-handler")
|
||||||
.log("Validation error: ${exception.message}");
|
.log("Validation error: ${exception.message}");
|
||||||
|
|
||||||
// Ana işleme route'u
|
// Ana işleme route'u
|
||||||
from("direct:process-document")
|
from("direct:process-document")
|
||||||
.routeId("document-processing")
|
.routeId("document-processing")
|
||||||
@ -289,7 +289,7 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
|||||||
.otherwise()
|
.otherwise()
|
||||||
.to("direct:process-generic")
|
.to("direct:process-generic")
|
||||||
.end();
|
.end();
|
||||||
|
|
||||||
from("direct:validation-error-handler")
|
from("direct:validation-error-handler")
|
||||||
.bean(EventService.class, "createErrorEvent")
|
.bean(EventService.class, "createErrorEvent")
|
||||||
.log("Validation error handled");
|
.log("Validation error handled");
|
||||||
@ -302,24 +302,24 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class FileMonitoringRoute extends RouteBuilder {
|
public class FileMonitoringRoute extends RouteBuilder {
|
||||||
|
|
||||||
@ConfigProperty(name = "file.input.directory")
|
@ConfigProperty(name = "file.input.directory")
|
||||||
String inputDirectory;
|
String inputDirectory;
|
||||||
|
|
||||||
@ConfigProperty(name = "file.processed.directory")
|
@ConfigProperty(name = "file.processed.directory")
|
||||||
String processedDirectory;
|
String processedDirectory;
|
||||||
|
|
||||||
@ConfigProperty(name = "file.error.directory")
|
@ConfigProperty(name = "file.error.directory")
|
||||||
String errorDirectory;
|
String errorDirectory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
||||||
"&moveFailed=" + errorDirectory + "&delay=5000")
|
"&moveFailed=" + errorDirectory + "&delay=5000")
|
||||||
.routeId("file-monitor")
|
.routeId("file-monitor")
|
||||||
.log("Processing file: ${header.CamelFileName}")
|
.log("Processing file: ${header.CamelFileName}")
|
||||||
.to("direct:process-file");
|
.to("direct:process-file");
|
||||||
|
|
||||||
from("direct:process-file")
|
from("direct:process-file")
|
||||||
.bean(As2ProcessingService.class, "processFile")
|
.bean(As2ProcessingService.class, "processFile")
|
||||||
.log("File processing completed");
|
.log("File processing completed");
|
||||||
@ -332,13 +332,13 @@ public class FileMonitoringRoute extends RouteBuilder {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class InvoiceRoute extends RouteBuilder {
|
public class InvoiceRoute extends RouteBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
from("direct:invoice-validation")
|
from("direct:invoice-validation")
|
||||||
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
||||||
.log("Validation result: ${body}");
|
.log("Validation result: ${body}");
|
||||||
|
|
||||||
from("direct:persist-and-publish")
|
from("direct:persist-and-publish")
|
||||||
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
||||||
.bean(BusinessRulesPublisher.class, "publishAsync")
|
.bean(BusinessRulesPublisher.class, "publishAsync")
|
||||||
@ -391,7 +391,7 @@ public class DocumentResource {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class DocumentRepository implements PanacheRepository<Document> {
|
public class DocumentRepository implements PanacheRepository<Document> {
|
||||||
|
|
||||||
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
||||||
return find("status = ?1 order by createdAt desc", status)
|
return find("status = ?1 order by createdAt desc", status)
|
||||||
.page(page, size)
|
.page(page, size)
|
||||||
@ -401,7 +401,7 @@ public class DocumentRepository implements PanacheRepository<Document> {
|
|||||||
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
||||||
return find("referenceNumber", referenceNumber).firstResultOptional();
|
return find("referenceNumber", referenceNumber).firstResultOptional();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
||||||
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
||||||
}
|
}
|
||||||
@ -424,11 +424,11 @@ public class DocumentService {
|
|||||||
document.setDescription(request.description());
|
document.setDescription(request.description());
|
||||||
document.setStatus(DocumentStatus.PENDING);
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
document.setCreatedAt(Instant.now());
|
document.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
repo.persist(document);
|
repo.persist(document);
|
||||||
|
|
||||||
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,7 +455,7 @@ public record CreateDocumentRequest(
|
|||||||
|
|
||||||
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
||||||
public static DocumentResponse from(Document document) {
|
public static DocumentResponse from(Document document) {
|
||||||
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
||||||
document.getStatus());
|
document.getStatus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -471,7 +471,7 @@ public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViol
|
|||||||
String message = exception.getConstraintViolations().stream()
|
String message = exception.getConstraintViolations().stream()
|
||||||
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
.collect(Collectors.joining(", "));
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
return Response.status(Response.Status.BAD_REQUEST)
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
.entity(Map.of("error", "validation_error", "message", message))
|
.entity(Map.of("error", "validation_error", "message", message))
|
||||||
.build();
|
.build();
|
||||||
@ -501,29 +501,29 @@ public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
|||||||
public class FileStorageService {
|
public class FileStorageService {
|
||||||
private final S3Client s3Client;
|
private final S3Client s3Client;
|
||||||
private final ExecutorService executorService;
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
@ConfigProperty(name = "storage.bucket-name") String bucketName;
|
@ConfigProperty(name = "storage.bucket-name") String bucketName;
|
||||||
|
|
||||||
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
||||||
InputStream inputStream,
|
InputStream inputStream,
|
||||||
long size,
|
long size,
|
||||||
LogContext logContext,
|
LogContext logContext,
|
||||||
InvoiceFormat format) {
|
InvoiceFormat format) {
|
||||||
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
String path = generateStoragePath(format);
|
String path = generateStoragePath(format);
|
||||||
|
|
||||||
PutObjectRequest request = PutObjectRequest.builder()
|
PutObjectRequest request = PutObjectRequest.builder()
|
||||||
.bucket(bucketName)
|
.bucket(bucketName)
|
||||||
.key(path)
|
.key(path)
|
||||||
.contentLength(size)
|
.contentLength(size)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
||||||
|
|
||||||
log.info("File uploaded to S3: {}", path);
|
log.info("File uploaded to S3: {}", path);
|
||||||
|
|
||||||
return new StoredDocumentInfo(path, size, Instant.now());
|
return new StoredDocumentInfo(path, size, Instant.now());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to upload file to S3", e);
|
log.error("Failed to upload file to S3", e);
|
||||||
@ -569,7 +569,7 @@ public class DocumentCacheService {
|
|||||||
hibernate-orm:
|
hibernate-orm:
|
||||||
database:
|
database:
|
||||||
generation: drop-and-create
|
generation: drop-and-create
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 5672
|
port: 5672
|
||||||
@ -595,7 +595,7 @@ public class DocumentCacheService {
|
|||||||
hibernate-orm:
|
hibernate-orm:
|
||||||
database:
|
database:
|
||||||
generation: validate
|
generation: validate
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
host: ${RABBITMQ_HOST}
|
host: ${RABBITMQ_HOST}
|
||||||
port: ${RABBITMQ_PORT}
|
port: ${RABBITMQ_PORT}
|
||||||
@ -688,7 +688,7 @@ public class CamelHealthCheck implements HealthCheck {
|
|||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-config-yaml</artifactId>
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Camel Uzantıları -->
|
<!-- Camel Uzantıları -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.camel.quarkus</groupId>
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
@ -702,7 +702,7 @@ public class CamelHealthCheck implements HealthCheck {
|
|||||||
<groupId>org.apache.camel.quarkus</groupId>
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
<artifactId>camel-quarkus-bean</artifactId>
|
<artifactId>camel-quarkus-bean</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Lombok -->
|
<!-- Lombok -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
@ -710,7 +710,7 @@ public class CamelHealthCheck implements HealthCheck {
|
|||||||
<version>${lombok.version}</version>
|
<version>${lombok.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Loglama -->
|
<!-- Loglama -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkiverse.logging.logback</groupId>
|
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||||
|
|||||||
@ -38,7 +38,7 @@ başlıklarını açıkça yapılandırın, gizli bilgileri Vault veya ortam de
|
|||||||
@Path("/api/protected")
|
@Path("/api/protected")
|
||||||
@Authenticated
|
@Authenticated
|
||||||
public class ProtectedResource {
|
public class ProtectedResource {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
JsonWebToken jwt;
|
JsonWebToken jwt;
|
||||||
|
|
||||||
@ -75,20 +75,20 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
|||||||
@Provider
|
@Provider
|
||||||
@Priority(Priorities.AUTHENTICATION)
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
public class CustomAuthFilter implements ContainerRequestFilter {
|
public class CustomAuthFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity identity;
|
SecurityIdentity identity;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) {
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
// Başlık yoksa veya hatalıysa hemen reddet
|
// Başlık yoksa veya hatalıysa hemen reddet
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = authHeader.substring(7);
|
String token = authHeader.substring(7);
|
||||||
if (!validateToken(token)) {
|
if (!validateToken(token)) {
|
||||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
@ -110,7 +110,7 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
|||||||
@Path("/api/admin")
|
@Path("/api/admin")
|
||||||
@RolesAllowed("ADMIN")
|
@RolesAllowed("ADMIN")
|
||||||
public class AdminResource {
|
public class AdminResource {
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/users")
|
@Path("/users")
|
||||||
public List<UserDto> listUsers() {
|
public List<UserDto> listUsers() {
|
||||||
@ -128,7 +128,7 @@ public class AdminResource {
|
|||||||
|
|
||||||
@Path("/api/users")
|
@Path("/api/users")
|
||||||
public class UserResource {
|
public class UserResource {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ public class UserResource {
|
|||||||
@RolesAllowed("USER")
|
@RolesAllowed("USER")
|
||||||
public Response getUser(@PathParam("id") Long id) {
|
public Response getUser(@PathParam("id") Long id) {
|
||||||
// Sahipliği kontrol et
|
// Sahipliği kontrol et
|
||||||
if (!securityIdentity.hasRole("ADMIN") &&
|
if (!securityIdentity.hasRole("ADMIN") &&
|
||||||
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||||
return Response.status(Response.Status.FORBIDDEN).build();
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
@ -155,7 +155,7 @@ public class UserResource {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class SecurityService {
|
public class SecurityService {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ public class SecurityService {
|
|||||||
if (securityIdentity.isAnonymous()) {
|
if (securityIdentity.isAnonymous()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (securityIdentity.hasRole("ADMIN")) {
|
if (securityIdentity.hasRole("ADMIN")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -239,7 +239,7 @@ List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
|||||||
Optional<User> user = User.find("username", username).firstResultOptional();
|
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||||
|
|
||||||
// İYİ: İsimlendirilmiş parametreler
|
// İYİ: İsimlendirilmiş parametreler
|
||||||
List<User> users = User.list("email = :email and age > :minAge",
|
List<User> users = User.list("email = :email and age > :minAge",
|
||||||
Parameters.with("email", email).and("minAge", 18));
|
Parameters.with("email", email).and("minAge", 18));
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -266,7 +266,7 @@ public class User extends PanacheEntity {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class PasswordService {
|
public class PasswordService {
|
||||||
|
|
||||||
public String hash(String plainPassword) {
|
public String hash(String plainPassword) {
|
||||||
return BcryptUtil.bcryptHash(plainPassword);
|
return BcryptUtil.bcryptHash(plainPassword);
|
||||||
}
|
}
|
||||||
@ -334,7 +334,7 @@ quarkus.vault.authentication.kubernetes.role=my-role
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class SecretService {
|
public class SecretService {
|
||||||
|
|
||||||
@ConfigProperty(name = "api-key")
|
@ConfigProperty(name = "api-key")
|
||||||
String apiKey; // Vault'tan alınır
|
String apiKey; // Vault'tan alınır
|
||||||
|
|
||||||
@ -360,7 +360,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
|||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) {
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
String clientId = getClientIdentifier();
|
String clientId = getClientIdentifier();
|
||||||
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||||
k -> RateLimiter.create(100.0)); // Saniyede 100 istek
|
k -> RateLimiter.create(100.0)); // Saniyede 100 istek
|
||||||
|
|
||||||
if (!limiter.tryAcquire()) {
|
if (!limiter.tryAcquire()) {
|
||||||
@ -385,24 +385,24 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
|||||||
```java
|
```java
|
||||||
@Provider
|
@Provider
|
||||||
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||||
MultivaluedMap<String, Object> headers = response.getHeaders();
|
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||||
|
|
||||||
// Clickjacking'i önle
|
// Clickjacking'i önle
|
||||||
headers.putSingle("X-Frame-Options", "DENY");
|
headers.putSingle("X-Frame-Options", "DENY");
|
||||||
|
|
||||||
// XSS koruması
|
// XSS koruması
|
||||||
headers.putSingle("X-Content-Type-Options", "nosniff");
|
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||||
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||||
|
|
||||||
// HSTS
|
// HSTS
|
||||||
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
|
||||||
// CSP — script-src için 'unsafe-inline' kullanmayın, XSS korumasını etkisiz kılar;
|
// CSP — script-src için 'unsafe-inline' kullanmayın, XSS korumasını etkisiz kılar;
|
||||||
// bunun yerine nonce veya hash kullanın
|
// bunun yerine nonce veya hash kullanın
|
||||||
headers.putSingle("Content-Security-Policy",
|
headers.putSingle("Content-Security-Policy",
|
||||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,11 +419,11 @@ public class AuditService {
|
|||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
public void logAccess(String resource, String action) {
|
public void logAccess(String resource, String action) {
|
||||||
String user = securityIdentity.isAnonymous()
|
String user = securityIdentity.isAnonymous()
|
||||||
? "anonymous"
|
? "anonymous"
|
||||||
: securityIdentity.getPrincipal().getName();
|
: securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||||
user, action, resource, Instant.now());
|
user, action, resource, Instant.now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,25 +36,25 @@ Kapsamlı ve okunabilir testler için bu yapılandırılmış yaklaşımı izley
|
|||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@DisplayName("As2ProcessingService Unit Tests")
|
@DisplayName("As2ProcessingService Unit Tests")
|
||||||
class As2ProcessingServiceTest {
|
class As2ProcessingServiceTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private InvoiceFlowValidator invoiceFlowValidator;
|
private InvoiceFlowValidator invoiceFlowValidator;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EventService eventService;
|
private EventService eventService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private DocumentJobService documentJobService;
|
private DocumentJobService documentJobService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private BusinessRulesPublisher businessRulesPublisher;
|
private BusinessRulesPublisher businessRulesPublisher;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private FileStorageService fileStorageService;
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private As2ProcessingService as2ProcessingService;
|
private As2ProcessingService as2ProcessingService;
|
||||||
|
|
||||||
private Path testFilePath;
|
private Path testFilePath;
|
||||||
private LogContext testLogContext;
|
private LogContext testLogContext;
|
||||||
private InvoiceValidationResult validationResult;
|
private InvoiceValidationResult validationResult;
|
||||||
@ -64,17 +64,17 @@ class As2ProcessingServiceTest {
|
|||||||
void setUp() {
|
void setUp() {
|
||||||
// ARRANGE - Ortak test verisi
|
// ARRANGE - Ortak test verisi
|
||||||
testFilePath = Path.of("/tmp/test-invoice.xml");
|
testFilePath = Path.of("/tmp/test-invoice.xml");
|
||||||
|
|
||||||
testLogContext = new LogContext();
|
testLogContext = new LogContext();
|
||||||
testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001");
|
testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001");
|
||||||
testLogContext.put(As2Constants.FILE_NAME, "invoice.xml");
|
testLogContext.put(As2Constants.FILE_NAME, "invoice.xml");
|
||||||
testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001");
|
testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001");
|
||||||
|
|
||||||
validationResult = new InvoiceValidationResult();
|
validationResult = new InvoiceValidationResult();
|
||||||
validationResult.setValid(true);
|
validationResult.setValid(true);
|
||||||
validationResult.setSize(1024L);
|
validationResult.setSize(1024L);
|
||||||
validationResult.setDocumentHash("abc123");
|
validationResult.setDocumentHash("abc123");
|
||||||
|
|
||||||
documentInfo = new StoredDocumentInfo();
|
documentInfo = new StoredDocumentInfo();
|
||||||
documentInfo.setPath("s3://bucket/path/invoice.xml");
|
documentInfo.setPath("s3://bucket/path/invoice.xml");
|
||||||
documentInfo.setSize(1024L);
|
documentInfo.setSize(1024L);
|
||||||
@ -83,43 +83,43 @@ class As2ProcessingServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("processFile için testler")
|
@DisplayName("processFile için testler")
|
||||||
class ProcessFile {
|
class ProcessFile {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("CHORUS olmayan dosyayı tüm validasyonlarla başarıyla işlemeli")
|
@DisplayName("CHORUS olmayan dosyayı tüm validasyonlarla başarıyla işlemeli")
|
||||||
void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception {
|
void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
CustomLog.setCurrentContext(testLogContext);
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
when(invoiceFlowValidator.validateFlowWithConfig(
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
eq(testFilePath),
|
eq(testFilePath),
|
||||||
eq(ValidationFlowConfig.allValidations()),
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
eq(EInvoiceSyntaxFormat.UBL),
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
any(LogContext.class)))
|
any(LogContext.class)))
|
||||||
.thenReturn(validationResult);
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
.thenReturn(FlowProfile.BASIC);
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any()))
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any()))
|
||||||
.thenReturn(new BusinessRulesPayload());
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(invoiceFlowValidator).validateFlowWithConfig(
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
eq(testFilePath),
|
eq(testFilePath),
|
||||||
eq(ValidationFlowConfig.allValidations()),
|
eq(ValidationFlowConfig.allValidations()),
|
||||||
eq(EInvoiceSyntaxFormat.UBL),
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
any(LogContext.class));
|
any(LogContext.class));
|
||||||
|
|
||||||
verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class),
|
verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class),
|
||||||
eq("PERSISTENCE_BLOB_EVENT_TYPE"));
|
eq("PERSISTENCE_BLOB_EVENT_TYPE"));
|
||||||
verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class),
|
verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class),
|
||||||
eq("BUSINESS_RULES_MESSAGE_SENT"));
|
eq("BUSINESS_RULES_MESSAGE_SENT"));
|
||||||
verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class));
|
verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class));
|
||||||
}
|
}
|
||||||
@ -130,34 +130,34 @@ class As2ProcessingServiceTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
|
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
|
||||||
CustomLog.setCurrentContext(testLogContext);
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
when(invoiceFlowValidator.validateFlowWithConfig(
|
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||||
eq(testFilePath),
|
eq(testFilePath),
|
||||||
eq(ValidationFlowConfig.xsdOnly()),
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
eq(EInvoiceSyntaxFormat.UBL),
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
any(LogContext.class)))
|
any(LogContext.class)))
|
||||||
.thenReturn(validationResult);
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(),
|
when(documentJobService.createDocumentAndJobEntities(any(), any(), any(),
|
||||||
eq(FlowProfile.EXTENDED_CTC_FR), any()))
|
eq(FlowProfile.EXTENDED_CTC_FR), any()))
|
||||||
.thenReturn(new BusinessRulesPayload());
|
.thenReturn(new BusinessRulesPayload());
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(invoiceFlowValidator).validateFlowWithConfig(
|
verify(invoiceFlowValidator).validateFlowWithConfig(
|
||||||
eq(testFilePath),
|
eq(testFilePath),
|
||||||
eq(ValidationFlowConfig.xsdOnly()),
|
eq(ValidationFlowConfig.xsdOnly()),
|
||||||
eq(EInvoiceSyntaxFormat.UBL),
|
eq(EInvoiceSyntaxFormat.UBL),
|
||||||
any(LogContext.class));
|
any(LogContext.class));
|
||||||
|
|
||||||
verify(documentJobService).createDocumentAndJobEntities(
|
verify(documentJobService).createDocumentAndJobEntities(
|
||||||
any(), any(), any(),
|
any(), any(), any(),
|
||||||
eq(FlowProfile.EXTENDED_CTC_FR),
|
eq(FlowProfile.EXTENDED_CTC_FR),
|
||||||
any());
|
any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,31 +167,31 @@ class As2ProcessingServiceTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
CustomLog.setCurrentContext(testLogContext);
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
.thenReturn(validationResult);
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
.thenReturn(FlowProfile.BASIC);
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
documentInfo.setPath(""); // Boş path hatayı tetikler
|
documentInfo.setPath(""); // Boş path hatayı tetikler
|
||||||
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
.thenReturn(CompletableFuture.completedFuture(documentInfo));
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
As2ServerProcessingException exception = assertThrows(
|
As2ServerProcessingException exception = assertThrows(
|
||||||
As2ServerProcessingException.class,
|
As2ServerProcessingException.class,
|
||||||
() -> as2ProcessingService.processFile(testFilePath)
|
() -> as2ProcessingService.processFile(testFilePath)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage())
|
assertThat(exception.getMessage())
|
||||||
.contains("File path is empty after upload");
|
.contains("File path is empty after upload");
|
||||||
|
|
||||||
verify(eventService).createErrorEvent(
|
verify(eventService).createErrorEvent(
|
||||||
eq(documentInfo),
|
eq(documentInfo),
|
||||||
eq("FILE_UPLOAD_FAILED"),
|
eq("FILE_UPLOAD_FAILED"),
|
||||||
contains("File path is empty"));
|
contains("File path is empty"));
|
||||||
|
|
||||||
verify(businessRulesPublisher, never()).publishAsync(any());
|
verify(businessRulesPublisher, never()).publishAsync(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,18 +201,18 @@ class As2ProcessingServiceTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
testLogContext.put(As2Constants.CHORUS_FLOW, "false");
|
||||||
CustomLog.setCurrentContext(testLogContext);
|
CustomLog.setCurrentContext(testLogContext);
|
||||||
|
|
||||||
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any()))
|
||||||
.thenReturn(validationResult);
|
.thenReturn(validationResult);
|
||||||
|
|
||||||
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
when(invoiceFlowValidator.computeFlowProfile(any(), any()))
|
||||||
.thenReturn(FlowProfile.BASIC);
|
.thenReturn(FlowProfile.BASIC);
|
||||||
|
|
||||||
CompletableFuture<StoredDocumentInfo> failedFuture =
|
CompletableFuture<StoredDocumentInfo> failedFuture =
|
||||||
CompletableFuture.failedFuture(new StorageException("S3 connection failed"));
|
CompletableFuture.failedFuture(new StorageException("S3 connection failed"));
|
||||||
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||||
.thenReturn(failedFuture);
|
.thenReturn(failedFuture);
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
assertThrows(
|
assertThrows(
|
||||||
CompletionException.class,
|
CompletionException.class,
|
||||||
@ -225,13 +225,13 @@ class As2ProcessingServiceTest {
|
|||||||
void givenNullFilePath_whenProcessFile_thenThrowsException() {
|
void givenNullFilePath_whenProcessFile_thenThrowsException() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
Path nullPath = null;
|
Path nullPath = null;
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
NullPointerException exception = assertThrows(
|
NullPointerException exception = assertThrows(
|
||||||
NullPointerException.class,
|
NullPointerException.class,
|
||||||
() -> as2ProcessingService.processFile(nullPath)
|
() -> as2ProcessingService.processFile(nullPath)
|
||||||
);
|
);
|
||||||
|
|
||||||
verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any());
|
verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -287,7 +287,7 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||||
mockRabbitMQ.expectedMessageCount(1);
|
mockRabbitMQ.expectedMessageCount(1);
|
||||||
|
|
||||||
// Test için gerçek endpoint'i mock ile değiştir
|
// Test için gerçek endpoint'i mock ile değiştir
|
||||||
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
@ -295,13 +295,13 @@ class BusinessRulesRouteTest {
|
|||||||
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("business-rules-publisher");
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
// ASSERT — .marshal().json() sonrası body JSON String'dir
|
// ASSERT — .marshal().json() sonrası body JSON String'dir
|
||||||
mockRabbitMQ.assertIsSatisfied(5000);
|
mockRabbitMQ.assertIsSatisfied(5000);
|
||||||
|
|
||||||
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
||||||
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
|
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
assertThat(body).contains("\"documentId\":1");
|
assertThat(body).contains("\"documentId\":1");
|
||||||
@ -314,19 +314,19 @@ class BusinessRulesRouteTest {
|
|||||||
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
||||||
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
||||||
mockMarshal.expectedMessageCount(1);
|
mockMarshal.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
advice.weaveAddLast().to("mock:marshal");
|
advice.weaveAddLast().to("mock:marshal");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("business-rules-publisher");
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockMarshal.assertIsSatisfied(5000);
|
mockMarshal.assertIsSatisfied(5000);
|
||||||
|
|
||||||
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
assertThat(body).contains("\"documentId\":1");
|
assertThat(body).contains("\"documentId\":1");
|
||||||
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
||||||
@ -343,17 +343,17 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
||||||
mockInvoice.expectedMessageCount(1);
|
mockInvoice.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("document-processing");
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("document-processing");
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBodyAndHeader("direct:process-document",
|
producerTemplate.sendBodyAndHeader("direct:process-document",
|
||||||
testPayload, "documentType", "INVOICE");
|
testPayload, "documentType", "INVOICE");
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockInvoice.assertIsSatisfied(5000);
|
mockInvoice.assertIsSatisfied(5000);
|
||||||
}
|
}
|
||||||
@ -364,25 +364,25 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
||||||
mockError.expectedMessageCount(1);
|
mockError.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("document-processing");
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
advice.weaveByToString(".*direct:validation-error-handler.*")
|
advice.weaveByToString(".*direct:validation-error-handler.*")
|
||||||
.replace().to("mock:error");
|
.replace().to("mock:error");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("document-processing");
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
// Error event oluşturma hatasını gerçek EventService API'si üzerinden simüle et
|
// Error event oluşturma hatasını gerçek EventService API'si üzerinden simüle et
|
||||||
doThrow(new ValidationException("Invalid document"))
|
doThrow(new ValidationException("Invalid document"))
|
||||||
.when(eventService)
|
.when(eventService)
|
||||||
.createErrorEvent(any(), eq("VALIDATION_ERROR"), anyString());
|
.createErrorEvent(any(), eq("VALIDATION_ERROR"), anyString());
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:process-document", testPayload);
|
producerTemplate.sendBody("direct:process-document", testPayload);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockError.assertIsSatisfied(5000);
|
mockError.assertIsSatisfied(5000);
|
||||||
|
|
||||||
Exception exception = mockError.getExchanges().get(0).getException();
|
Exception exception = mockError.getExchanges().get(0).getException();
|
||||||
assertThat(exception).isInstanceOf(ValidationException.class);
|
assertThat(exception).isInstanceOf(ValidationException.class);
|
||||||
assertThat(exception.getMessage()).contains("Invalid document");
|
assertThat(exception.getMessage()).contains("Invalid document");
|
||||||
@ -400,13 +400,13 @@ class EventServiceTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EventRepository eventRepository;
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private EventService eventService;
|
private EventService eventService;
|
||||||
|
|
||||||
private BusinessRulesPayload testPayload;
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -419,19 +419,19 @@ class EventServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("createSuccessEvent için testler")
|
@DisplayName("createSuccessEvent için testler")
|
||||||
class CreateSuccessEvent {
|
class CreateSuccessEvent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Doğru niteliklerle başarı eventi oluşturulmalı")
|
@DisplayName("Doğru niteliklerle başarı eventi oluşturulmalı")
|
||||||
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() ->
|
assertDoesNotThrow(() ->
|
||||||
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(eventRepository).persist(argThat(event ->
|
verify(eventRepository).persist(argThat(event ->
|
||||||
event.getType().equals("DOCUMENT_PROCESSED") &&
|
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||||
event.getStatus() == EventStatus.SUCCESS &&
|
event.getStatus() == EventStatus.SUCCESS &&
|
||||||
event.getPayload().equals("{\"documentId\":1}") &&
|
event.getPayload().equals("{\"documentId\":1}") &&
|
||||||
@ -444,13 +444,13 @@ class EventServiceTest {
|
|||||||
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
Object nullPayload = null;
|
Object nullPayload = null;
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
NullPointerException exception = assertThrows(
|
NullPointerException exception = assertThrows(
|
||||||
NullPointerException.class,
|
NullPointerException.class,
|
||||||
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
||||||
verify(eventRepository, never()).persist(any());
|
verify(eventRepository, never()).persist(any());
|
||||||
}
|
}
|
||||||
@ -459,20 +459,20 @@ class EventServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("createErrorEvent için testler")
|
@DisplayName("createErrorEvent için testler")
|
||||||
class CreateErrorEvent {
|
class CreateErrorEvent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Hata mesajıyla hata eventi oluşturulmalı")
|
@DisplayName("Hata mesajıyla hata eventi oluşturulmalı")
|
||||||
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
String errorMessage = "Processing failed";
|
String errorMessage = "Processing failed";
|
||||||
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() ->
|
assertDoesNotThrow(() ->
|
||||||
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(eventRepository).persist(argThat(event ->
|
verify(eventRepository).persist(argThat(event ->
|
||||||
event.getType().equals("PROCESSING_ERROR") &&
|
event.getType().equals("PROCESSING_ERROR") &&
|
||||||
event.getStatus() == EventStatus.ERROR &&
|
event.getStatus() == EventStatus.ERROR &&
|
||||||
event.getErrorMessage().equals(errorMessage) &&
|
event.getErrorMessage().equals(errorMessage) &&
|
||||||
@ -489,7 +489,7 @@ class EventServiceTest {
|
|||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -505,13 +505,13 @@ class FileStorageServiceTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private S3Client s3Client;
|
private S3Client s3Client;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExecutorService executorService;
|
private ExecutorService executorService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private FileStorageService fileStorageService;
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
private InputStream testInputStream;
|
private InputStream testInputStream;
|
||||||
private LogContext testLogContext;
|
private LogContext testLogContext;
|
||||||
|
|
||||||
@ -526,7 +526,7 @@ class FileStorageServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("uploadOriginalFile için testler")
|
@DisplayName("uploadOriginalFile için testler")
|
||||||
class UploadOriginalFile {
|
class UploadOriginalFile {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Dosyayı başarıyla yüklemeli ve belge bilgisi döndürmeli")
|
@DisplayName("Dosyayı başarıyla yüklemeli ve belge bilgisi döndürmeli")
|
||||||
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
||||||
@ -535,23 +535,23 @@ class FileStorageServiceTest {
|
|||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
.thenReturn(PutObjectResponse.builder().build());
|
.thenReturn(PutObjectResponse.builder().build());
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
CompletableFuture<StoredDocumentInfo> future =
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL);
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
StoredDocumentInfo result = future.join();
|
StoredDocumentInfo result = future.join();
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getPath()).isNotBlank();
|
assertThat(result.getPath()).isNotBlank();
|
||||||
assertThat(result.getSize()).isEqualTo(1024L);
|
assertThat(result.getSize()).isEqualTo(1024L);
|
||||||
assertThat(result.getUploadedAt()).isNotNull();
|
assertThat(result.getUploadedAt()).isNotNull();
|
||||||
|
|
||||||
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,12 +566,12 @@ class FileStorageServiceTest {
|
|||||||
|
|
||||||
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
.thenThrow(new StorageException("S3 unavailable"));
|
.thenThrow(new StorageException("S3 unavailable"));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
CompletableFuture<StoredDocumentInfo> future =
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL);
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThatThrownBy(() -> future.join())
|
assertThatThrownBy(() -> future.join())
|
||||||
.isInstanceOf(CompletionException.class)
|
.isInstanceOf(CompletionException.class)
|
||||||
@ -584,17 +584,17 @@ class FileStorageServiceTest {
|
|||||||
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
||||||
|
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
capturedContext.set(CustomLog.getCurrentContext());
|
capturedContext.set(CustomLog.getCurrentContext());
|
||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL).join();
|
testLogContext, InvoiceFormat.UBL).join();
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(capturedContext.get()).isNotNull();
|
assertThat(capturedContext.get()).isNotNull();
|
||||||
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
||||||
@ -746,7 +746,7 @@ class DocumentIntegrationTest {
|
|||||||
<goal>prepare-agent</goal>
|
<goal>prepare-agent</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
|
|
||||||
<!-- Kapsam raporu oluştur -->
|
<!-- Kapsam raporu oluştur -->
|
||||||
<execution>
|
<execution>
|
||||||
<id>report</id>
|
<id>report</id>
|
||||||
@ -755,7 +755,7 @@ class DocumentIntegrationTest {
|
|||||||
<goal>report</goal>
|
<goal>report</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
|
|
||||||
<!-- Kapsam eşiklerini zorla -->
|
<!-- Kapsam eşiklerini zorla -->
|
||||||
<execution>
|
<execution>
|
||||||
<id>check</id>
|
<id>check</id>
|
||||||
@ -810,14 +810,14 @@ mvn jacoco:check
|
|||||||
<artifactId>quarkus-junit5-mockito</artifactId>
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Mockito -->
|
<!-- Mockito -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- AssertJ (JUnit assertion'larına tercih edilir) -->
|
<!-- AssertJ (JUnit assertion'larına tercih edilir) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.assertj</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
@ -825,14 +825,14 @@ mvn jacoco:check
|
|||||||
<version>3.24.2</version>
|
<version>3.24.2</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- REST Assured -->
|
<!-- REST Assured -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.rest-assured</groupId>
|
<groupId>io.rest-assured</groupId>
|
||||||
<artifactId>rest-assured</artifactId>
|
<artifactId>rest-assured</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Camel Test -->
|
<!-- Camel Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.camel.quarkus</groupId>
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
|||||||
@ -437,28 +437,28 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Cache Maven packages
|
- name: Cache Maven packages
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.m2
|
path: ~/.m2
|
||||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: mvn clean verify -DskipTests
|
run: mvn clean verify -DskipTests
|
||||||
|
|
||||||
- name: Test with Coverage
|
- name: Test with Coverage
|
||||||
run: mvn test jacoco:report jacoco:check
|
run: mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
- name: Security Scan
|
- name: Security Scan
|
||||||
run: mvn org.owasp:dependency-check-maven:check
|
run: mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
- name: Upload Coverage
|
- name: Upload Coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@ -47,26 +47,26 @@ ECC v2.0.0-rc.1 اس قابل استعمال پرت پر عوامی Hermes آپ
|
|||||||
<tr>
|
<tr>
|
||||||
<td width="25%" align="center">
|
<td width="25%" align="center">
|
||||||
<a href="https://ecc.tools/pricing">
|
<a href="https://ecc.tools/pricing">
|
||||||
<strong>💼 ECC Pro</strong><br />
|
<strong> ECC Pro</strong><br />
|
||||||
<sub>نجی ریپوز · GitHub App · $19/نشست/ماہ</sub>
|
<sub>نجی ریپوز · GitHub App · $19/نشست/ماہ</sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td width="25%" align="center">
|
<td width="25%" align="center">
|
||||||
<a href="https://github.com/sponsors/affaan-m">
|
<a href="https://github.com/sponsors/affaan-m">
|
||||||
<strong>❤️ اسپانسر</strong><br />
|
<strong> اسپانسر</strong><br />
|
||||||
<sub>OSS کو فنڈ کریں · $5/ماہ سے</sub>
|
<sub>OSS کو فنڈ کریں · $5/ماہ سے</sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td width="25%" align="center">
|
<td width="25%" align="center">
|
||||||
<a href="https://github.com/affaan-m/ECC/discussions">
|
<a href="https://github.com/affaan-m/ECC/discussions">
|
||||||
<strong>💬 کمیونٹی</strong>
|
<strong> کمیونٹی</strong>
|
||||||
<br />
|
<br />
|
||||||
<sub>Discussions · Q&A · Show & Tell</sub>
|
<sub>Discussions · Q&A · Show & Tell</sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td width="25%" align="center">
|
<td width="25%" align="center">
|
||||||
<a href="https://github.com/apps/ecc-tools">
|
<a href="https://github.com/apps/ecc-tools">
|
||||||
<strong>🤖 GitHub App</strong><br />
|
<strong> GitHub App</strong><br />
|
||||||
<sub>انسٹال · PR آڈٹس · مفت ٹیئر</sub>
|
<sub>انسٹال · PR آڈٹس · مفت ٹیئر</sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Everything Claude Code (ECC) — 智能体指令
|
# Everything Claude Code (ECC) — 智能体指令
|
||||||
|
|
||||||
这是一个**生产就绪的 AI 编码插件**,提供 63 个专业代理、251 项技能、79 条命令以及自动化钩子工作流,用于软件开发。
|
这是一个**生产就绪的 AI 编码插件**,提供 64 个专业代理、255 项技能、79 条命令以及自动化钩子工作流,用于软件开发。
|
||||||
|
|
||||||
**版本:** 2.0.0-rc.1
|
**版本:** 2.0.0-rc.1
|
||||||
|
|
||||||
@ -146,8 +146,8 @@
|
|||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
agents/ — 63 个专业子代理
|
agents/ — 64 个专业子代理
|
||||||
skills/ — 251 个工作流技能和领域知识
|
skills/ — 255 个工作流技能和领域知识
|
||||||
commands/ — 79 个斜杠命令
|
commands/ — 79 个斜杠命令
|
||||||
hooks/ — 基于触发的自动化
|
hooks/ — 基于触发的自动化
|
||||||
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||||
|
|||||||
@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
|||||||
/plugin list ecc@ecc
|
/plugin list ecc@ecc
|
||||||
```
|
```
|
||||||
|
|
||||||
**搞定!** 你现在可以使用 63 个智能体、251 项技能和 79 个命令了。
|
**搞定!** 你现在可以使用 64 个智能体、255 项技能和 79 个命令了。
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
@ -1136,9 +1136,9 @@ opencode
|
|||||||
|
|
||||||
| 功能特性 | Claude Code | OpenCode | 状态 |
|
| 功能特性 | Claude Code | OpenCode | 状态 |
|
||||||
|---------|---------------|----------|--------|
|
|---------|---------------|----------|--------|
|
||||||
| 智能体 | PASS: 63 个 | PASS: 12 个 | **Claude Code 领先** |
|
| 智能体 | PASS: 64 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||||
| 命令 | PASS: 79 个 | PASS: 35 个 | **Claude Code 领先** |
|
| 命令 | PASS: 79 个 | PASS: 35 个 | **Claude Code 领先** |
|
||||||
| 技能 | PASS: 251 项 | PASS: 37 项 | **Claude Code 领先** |
|
| 技能 | PASS: 255 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||||
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
||||||
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
||||||
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
||||||
@ -1244,9 +1244,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
|||||||
|
|
||||||
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
||||||
|---------|-----------------------|------------|-----------|----------|
|
|---------|-----------------------|------------|-----------|----------|
|
||||||
| **智能体** | 63 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
| **智能体** | 64 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||||
| **命令** | 79 | 共享 | 基于指令 | 35 |
|
| **命令** | 79 | 共享 | 基于指令 | 35 |
|
||||||
| **技能** | 251 | 共享 | 10 (原生格式) | 37 |
|
| **技能** | 255 | 共享 | 10 (原生格式) | 37 |
|
||||||
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
||||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
||||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||||
|
|||||||
@ -171,7 +171,14 @@ function main() {
|
|||||||
process.exit(Number.isInteger(result.status) ? result.status : 0);
|
process.exit(Number.isInteger(result.status) ? result.status : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
// Run when invoked as a hook entry. Production hooks load this via
|
||||||
|
// `node -e "...; process.argv.splice(1,0,s); require(s)"`; on Node 21+ that
|
||||||
|
// leaves require.main undefined (not this module), which previously skipped
|
||||||
|
// main() and made every plugin hook a silent no-op. Guard on both the
|
||||||
|
// direct-entry case and that eval-bootstrap case. When imported for its
|
||||||
|
// exports (tests), require.main is a real, different module, so main() stays
|
||||||
|
// dormant.
|
||||||
|
if (require.main === module || require.main === undefined) {
|
||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ origin: community
|
|||||||
|
|
||||||
Structural maintainability feedback for AI-assisted coding. Complements style/lint skills (`coding-standards`, `plankton-code-quality`) with **design-level** health scores and regression gates.
|
Structural maintainability feedback for AI-assisted coding. Complements style/lint skills (`coding-standards`, `plankton-code-quality`) with **design-level** health scores and regression gates.
|
||||||
|
|
||||||
**Upstream:** [codescene-oss/codescene-mcp-server](https://github.com/codescene-oss/codescene-mcp-server)
|
**Upstream:** [codescene-oss/codescene-mcp-server](https://github.com/codescene-oss/codescene-mcp-server)
|
||||||
**Package:** `@codescene/codehealth-mcp` (stdio via npx)
|
**Package:** `@codescene/codehealth-mcp` (stdio via npx)
|
||||||
|
|
||||||
## Security and boundaries
|
## Security and boundaries
|
||||||
|
|||||||
@ -510,4 +510,4 @@ async def list_items(db: AsyncSession = Depends(get_db)):
|
|||||||
- Wrap database mutation boundaries gracefully within transactions inside your service layer, catching structural database errors directly.
|
- Wrap database mutation boundaries gracefully within transactions inside your service layer, catching structural database errors directly.
|
||||||
- Parse JWT parameters defensively, expecting potential string/integer cast mismatches from modern payload variations.
|
- Parse JWT parameters defensively, expecting potential string/integer cast mismatches from modern payload variations.
|
||||||
- Enforce deterministic sorting (e.g., `.order_by(Model.id)`) on all offset/limit paginated endpoints to avoid data skips.
|
- Enforce deterministic sorting (e.g., `.order_by(Model.id)`) on all offset/limit paginated endpoints to avoid data skips.
|
||||||
- Isolate authorization checks from core authentication dependencies to provide precise REST status signals (`401` vs `403`).
|
- Isolate authorization checks from core authentication dependencies to provide precise REST status signals (`401` vs `403`).
|
||||||
|
|||||||
@ -203,8 +203,7 @@ Use ARIA only when native HTML semantics are insufficient. Wrong ARIA is worse t
|
|||||||
<button
|
<button
|
||||||
aria-describedby="delete-warning"
|
aria-describedby="delete-warning"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
>
|
> Delete account
|
||||||
Delete account
|
|
||||||
</button>
|
</button>
|
||||||
<p id="delete-warning">This action cannot be undone.</p>
|
<p id="delete-warning">This action cannot be undone.</p>
|
||||||
```
|
```
|
||||||
|
|||||||
@ -96,11 +96,11 @@ less pi-hole-install.sh # review before proceeding
|
|||||||
bash pi-hole-install.sh
|
bash pi-hole-install.sh
|
||||||
|
|
||||||
# Follow the interactive installer:
|
# Follow the interactive installer:
|
||||||
# 1. Select network interface (eth0 for wired — recommended)
|
# 1. Select network interface (eth0 for wired — recommended)
|
||||||
# 2. Select upstream DNS (Cloudflare or leave default — can change later)
|
# 2. Select upstream DNS (Cloudflare or leave default — can change later)
|
||||||
# 3. Confirm static IP
|
# 3. Confirm static IP
|
||||||
# 4. Install the web admin interface (recommended)
|
# 4. Install the web admin interface (recommended)
|
||||||
# 5. Note the admin password shown at the end
|
# 5. Note the admin password shown at the end
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pointing Your Network at Pi-hole
|
## Pointing Your Network at Pi-hole
|
||||||
@ -183,9 +183,9 @@ sudo systemctl start cloudflared
|
|||||||
sudo systemctl enable cloudflared
|
sudo systemctl enable cloudflared
|
||||||
|
|
||||||
# Now point Pi-hole at the local DoH proxy:
|
# Now point Pi-hole at the local DoH proxy:
|
||||||
# Pi-hole admin → Settings → DNS → Custom upstream DNS
|
# Pi-hole admin → Settings → DNS → Custom upstream DNS
|
||||||
# Set to: 127.0.0.1#5053
|
# Set to: 127.0.0.1#5053
|
||||||
# Uncheck all other upstream resolvers
|
# Uncheck all other upstream resolvers
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local DNS Records
|
## Local DNS Records
|
||||||
|
|||||||
@ -218,8 +218,8 @@ stays reachable after an IP change.
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# ddns.env (chmod 600, not committed to git):
|
# ddns.env (chmod 600, not committed to git):
|
||||||
# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id
|
# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id
|
||||||
# SETTINGS_CLOUDFLARE_TOKEN=your_api_token
|
# SETTINGS_CLOUDFLARE_TOKEN=your_api_token
|
||||||
|
|
||||||
# Option 2: DuckDNS (free, simple)
|
# Option 2: DuckDNS (free, simple)
|
||||||
Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org)
|
Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org)
|
||||||
|
|||||||
@ -71,9 +71,9 @@ Before interrupting the user, evaluate signal strength:
|
|||||||
|
|
||||||
For each strong-signal conflict, present exactly ONE question with 4 options:
|
For each strong-signal conflict, present exactly ONE question with 4 options:
|
||||||
|
|
||||||
> 📄 Evidence: `pathA` uses style X, `pathB` uses style Y
|
> Evidence: `pathA` uses style X, `pathB` uses style Y
|
||||||
> ⚠️ Risk: mixing both fractures the project style
|
> WARNING: Risk: mixing both fractures the project style
|
||||||
> ❓ Choose: `1` follow X `2` follow Y `3` this is evolution, update rules `4` I have a new rule
|
> Choose: `1` follow X `2` follow Y `3` this is evolution, update rules `4` I have a new rule
|
||||||
|
|
||||||
Suspend until the user answers, then proceed to the next conflict. Never stack questions.
|
Suspend until the user answers, then proceed to the next conflict. Never stack questions.
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ Ask the user for enforcement strength (use `AskUserQuestion`):
|
|||||||
| Option | Mechanism |
|
| Option | Mechanism |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **1** Soft hook (recommended) | Write `@.ai-style-rules.md` reference into project `CLAUDE.md` |
|
| **1** Soft hook (recommended) | Write `@.ai-style-rules.md` reference into project `CLAUDE.md` |
|
||||||
| **2** Hard hook | Soft hook + `PreToolUse[Write|Edit|MultiEdit]` Hook in `settings.json` |
|
| **2** Hard hook | Soft hook + `PreToolUse[Write\|Edit\|MultiEdit]` Hook in `settings.json` |
|
||||||
| **3** No hook | Keep the rules file; user references manually |
|
| **3** No hook | Keep the rules file; user references manually |
|
||||||
|
|
||||||
### Branch B — Incremental Sniff
|
### Branch B — Incremental Sniff
|
||||||
@ -120,12 +120,12 @@ This skill auto-detects whether it's a first-time or incremental run via `.ai-st
|
|||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Do NOT skip the scale measurement step — sampling a 30-file project "starves" it; full-scanning a 5,000-file repo blows up
|
- FAIL: Do NOT skip the scale measurement step — sampling a 30-file project "starves" it; full-scanning a 5,000-file repo blows up
|
||||||
- ❌ Do NOT stack multiple conflict questions at once — grilling is strictly one-at-a-time
|
- FAIL: Do NOT stack multiple conflict questions at once — grilling is strictly one-at-a-time
|
||||||
- ❌ Do NOT overwrite old rules in incremental mode — always append evolution logs
|
- FAIL: Do NOT overwrite old rules in incremental mode — always append evolution logs
|
||||||
- ❌ Do NOT default to "hard hook" without asking — enforcement strength is the user's call
|
- FAIL: Do NOT default to "hard hook" without asking — enforcement strength is the user's call
|
||||||
- ❌ Do NOT judge syntax or tech-stack quality — this skill aligns meta-architecture only
|
- FAIL: Do NOT judge syntax or tech-stack quality — this skill aligns meta-architecture only
|
||||||
- ❌ Do NOT copy bugs from exemplar files — reuse structure, flag defects
|
- FAIL: Do NOT copy bugs from exemplar files — reuse structure, flag defects
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
|
|||||||
@ -337,8 +337,7 @@ export function ExpandingCard({ title, body }: { title: string; body: string })
|
|||||||
duration: motionTokens.duration.normal,
|
duration: motionTokens.duration.normal,
|
||||||
ease: motionTokens.easing.smooth,
|
ease: motionTokens.easing.smooth,
|
||||||
}}
|
}}
|
||||||
>
|
> {children}
|
||||||
{children}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@ public class OrderProcessingService {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ProcessingService {
|
public class ProcessingService {
|
||||||
|
|
||||||
public void processDocument(Document doc) {
|
public void processDocument(Document doc) {
|
||||||
LogContext logContext = CustomLog.getCurrentContext();
|
LogContext logContext = CustomLog.getCurrentContext();
|
||||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
@ -78,12 +78,12 @@ public class ProcessingService {
|
|||||||
logContext.put("documentId", doc.getId().toString());
|
logContext.put("documentId", doc.getId().toString());
|
||||||
logContext.put("documentType", doc.getType());
|
logContext.put("documentType", doc.getType());
|
||||||
logContext.put("userId", SecurityContext.getUserId());
|
logContext.put("userId", SecurityContext.getUserId());
|
||||||
|
|
||||||
log.info("Starting document processing");
|
log.info("Starting document processing");
|
||||||
|
|
||||||
// All logs within this scope inherit the context
|
// All logs within this scope inherit the context
|
||||||
processInternal(doc);
|
processInternal(doc);
|
||||||
|
|
||||||
log.info("Document processing completed");
|
log.info("Document processing completed");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Document processing failed", e);
|
log.error("Document processing failed", e);
|
||||||
@ -103,7 +103,7 @@ public class ProcessingService {
|
|||||||
<includeMdc>true</includeMdc>
|
<includeMdc>true</includeMdc>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="com.example" level="INFO"/>
|
<logger name="com.example" level="INFO"/>
|
||||||
<root level="WARN">
|
<root level="WARN">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
@ -120,7 +120,7 @@ public class ProcessingService {
|
|||||||
public class EventService {
|
public class EventService {
|
||||||
private final EventRepository eventRepository;
|
private final EventRepository eventRepository;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public void createSuccessEvent(Object payload, String eventType) {
|
public void createSuccessEvent(Object payload, String eventType) {
|
||||||
Objects.requireNonNull(payload, "Payload cannot be null");
|
Objects.requireNonNull(payload, "Payload cannot be null");
|
||||||
Event event = new Event();
|
Event event = new Event();
|
||||||
@ -128,11 +128,11 @@ public class EventService {
|
|||||||
event.setStatus(EventStatus.SUCCESS);
|
event.setStatus(EventStatus.SUCCESS);
|
||||||
event.setPayload(serializePayload(payload));
|
event.setPayload(serializePayload(payload));
|
||||||
event.setTimestamp(Instant.now());
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
eventRepository.persist(event);
|
eventRepository.persist(event);
|
||||||
log.info("Success event created: {}", eventType);
|
log.info("Success event created: {}", eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
||||||
Objects.requireNonNull(payload, "Payload cannot be null");
|
Objects.requireNonNull(payload, "Payload cannot be null");
|
||||||
if (errorMessage == null || errorMessage.isBlank()) {
|
if (errorMessage == null || errorMessage.isBlank()) {
|
||||||
@ -144,11 +144,11 @@ public class EventService {
|
|||||||
event.setErrorMessage(errorMessage);
|
event.setErrorMessage(errorMessage);
|
||||||
event.setPayload(serializePayload(payload));
|
event.setPayload(serializePayload(payload));
|
||||||
event.setTimestamp(Instant.now());
|
event.setTimestamp(Instant.now());
|
||||||
|
|
||||||
eventRepository.persist(event);
|
eventRepository.persist(event);
|
||||||
log.error("Error event created: {} - {}", eventType, errorMessage);
|
log.error("Error event created: {} - {}", eventType, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String serializePayload(Object payload) {
|
private String serializePayload(Object payload) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(payload);
|
return objectMapper.writeValueAsString(payload);
|
||||||
@ -167,10 +167,10 @@ public class EventService {
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class BusinessRulesPublisher {
|
public class BusinessRulesPublisher {
|
||||||
private final ProducerTemplate producerTemplate;
|
private final ProducerTemplate producerTemplate;
|
||||||
|
|
||||||
public void publishSync(BusinessRulesPayload payload) {
|
public void publishSync(BusinessRulesPayload payload) {
|
||||||
producerTemplate.sendBody(
|
producerTemplate.sendBody(
|
||||||
"direct:business-rules-publisher",
|
"direct:business-rules-publisher",
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -182,23 +182,23 @@ public class BusinessRulesPublisher {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class BusinessRulesRoute extends RouteBuilder {
|
public class BusinessRulesRoute extends RouteBuilder {
|
||||||
|
|
||||||
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
||||||
String businessRulesQueue;
|
String businessRulesQueue;
|
||||||
|
|
||||||
@ConfigProperty(name = "rabbitmq.host")
|
@ConfigProperty(name = "rabbitmq.host")
|
||||||
String rabbitHost;
|
String rabbitHost;
|
||||||
|
|
||||||
@ConfigProperty(name = "rabbitmq.port")
|
@ConfigProperty(name = "rabbitmq.port")
|
||||||
Integer rabbitPort;
|
Integer rabbitPort;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
from("direct:business-rules-publisher")
|
from("direct:business-rules-publisher")
|
||||||
.routeId("business-rules-publisher")
|
.routeId("business-rules-publisher")
|
||||||
.log("Publishing message to RabbitMQ: ${body}")
|
.log("Publishing message to RabbitMQ: ${body}")
|
||||||
.marshal().json(JsonLibrary.Jackson)
|
.marshal().json(JsonLibrary.Jackson)
|
||||||
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
||||||
businessRulesQueue, rabbitHost, rabbitPort);
|
businessRulesQueue, rabbitHost, rabbitPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,7 +209,7 @@ public class BusinessRulesRoute extends RouteBuilder {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class DocumentProcessingRoute extends RouteBuilder {
|
public class DocumentProcessingRoute extends RouteBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
// Error handling
|
// Error handling
|
||||||
@ -217,7 +217,7 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
|||||||
.handled(true)
|
.handled(true)
|
||||||
.to("direct:validation-error-handler")
|
.to("direct:validation-error-handler")
|
||||||
.log("Validation error: ${exception.message}");
|
.log("Validation error: ${exception.message}");
|
||||||
|
|
||||||
// Main processing route
|
// Main processing route
|
||||||
from("direct:process-document")
|
from("direct:process-document")
|
||||||
.routeId("document-processing")
|
.routeId("document-processing")
|
||||||
@ -232,7 +232,7 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
|||||||
.otherwise()
|
.otherwise()
|
||||||
.to("direct:process-generic")
|
.to("direct:process-generic")
|
||||||
.end();
|
.end();
|
||||||
|
|
||||||
from("direct:validation-error-handler")
|
from("direct:validation-error-handler")
|
||||||
.bean(EventService.class, "createErrorEvent")
|
.bean(EventService.class, "createErrorEvent")
|
||||||
.log("Validation error handled");
|
.log("Validation error handled");
|
||||||
@ -245,24 +245,24 @@ public class DocumentProcessingRoute extends RouteBuilder {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class FileMonitoringRoute extends RouteBuilder {
|
public class FileMonitoringRoute extends RouteBuilder {
|
||||||
|
|
||||||
@ConfigProperty(name = "file.input.directory")
|
@ConfigProperty(name = "file.input.directory")
|
||||||
String inputDirectory;
|
String inputDirectory;
|
||||||
|
|
||||||
@ConfigProperty(name = "file.processed.directory")
|
@ConfigProperty(name = "file.processed.directory")
|
||||||
String processedDirectory;
|
String processedDirectory;
|
||||||
|
|
||||||
@ConfigProperty(name = "file.error.directory")
|
@ConfigProperty(name = "file.error.directory")
|
||||||
String errorDirectory;
|
String errorDirectory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
||||||
"&moveFailed=" + errorDirectory + "&delay=5000")
|
"&moveFailed=" + errorDirectory + "&delay=5000")
|
||||||
.routeId("file-monitor")
|
.routeId("file-monitor")
|
||||||
.log("Processing file: ${header.CamelFileName}")
|
.log("Processing file: ${header.CamelFileName}")
|
||||||
.to("direct:process-file");
|
.to("direct:process-file");
|
||||||
|
|
||||||
from("direct:process-file")
|
from("direct:process-file")
|
||||||
.bean(OrderProcessingService.class, "processFile")
|
.bean(OrderProcessingService.class, "processFile")
|
||||||
.log("File processing completed");
|
.log("File processing completed");
|
||||||
@ -275,13 +275,13 @@ public class FileMonitoringRoute extends RouteBuilder {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class InvoiceRoute extends RouteBuilder {
|
public class InvoiceRoute extends RouteBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
from("direct:invoice-validation")
|
from("direct:invoice-validation")
|
||||||
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
||||||
.log("Validation result: ${body}");
|
.log("Validation result: ${body}");
|
||||||
|
|
||||||
from("direct:persist-and-publish")
|
from("direct:persist-and-publish")
|
||||||
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
||||||
.bean(BusinessRulesPublisher.class, "publishAsync")
|
.bean(BusinessRulesPublisher.class, "publishAsync")
|
||||||
@ -334,7 +334,7 @@ public class DocumentResource {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class DocumentRepository implements PanacheRepository<Document> {
|
public class DocumentRepository implements PanacheRepository<Document> {
|
||||||
|
|
||||||
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
||||||
return find("status = ?1 order by createdAt desc", status)
|
return find("status = ?1 order by createdAt desc", status)
|
||||||
.page(page, size)
|
.page(page, size)
|
||||||
@ -344,7 +344,7 @@ public class DocumentRepository implements PanacheRepository<Document> {
|
|||||||
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
||||||
return find("referenceNumber", referenceNumber).firstResultOptional();
|
return find("referenceNumber", referenceNumber).firstResultOptional();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
||||||
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
||||||
}
|
}
|
||||||
@ -367,11 +367,11 @@ public class DocumentService {
|
|||||||
document.setDescription(request.description());
|
document.setDescription(request.description());
|
||||||
document.setStatus(DocumentStatus.PENDING);
|
document.setStatus(DocumentStatus.PENDING);
|
||||||
document.setCreatedAt(Instant.now());
|
document.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
repo.persist(document);
|
repo.persist(document);
|
||||||
|
|
||||||
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,7 +398,7 @@ public record CreateDocumentRequest(
|
|||||||
|
|
||||||
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
||||||
public static DocumentResponse from(Document document) {
|
public static DocumentResponse from(Document document) {
|
||||||
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
||||||
document.getStatus());
|
document.getStatus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,7 +414,7 @@ public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViol
|
|||||||
String message = exception.getConstraintViolations().stream()
|
String message = exception.getConstraintViolations().stream()
|
||||||
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
.collect(Collectors.joining(", "));
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
return Response.status(Response.Status.BAD_REQUEST)
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
.entity(Map.of("error", "validation_error", "message", message))
|
.entity(Map.of("error", "validation_error", "message", message))
|
||||||
.build();
|
.build();
|
||||||
@ -444,30 +444,30 @@ public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
|||||||
public class FileStorageService {
|
public class FileStorageService {
|
||||||
private final S3Client s3Client;
|
private final S3Client s3Client;
|
||||||
private final ExecutorService executorService;
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
@ConfigProperty(name = "storage.bucket-name")
|
@ConfigProperty(name = "storage.bucket-name")
|
||||||
String bucketName;
|
String bucketName;
|
||||||
|
|
||||||
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
||||||
InputStream inputStream,
|
InputStream inputStream,
|
||||||
long size,
|
long size,
|
||||||
LogContext logContext,
|
LogContext logContext,
|
||||||
InvoiceFormat format) {
|
InvoiceFormat format) {
|
||||||
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||||
String path = generateStoragePath(format);
|
String path = generateStoragePath(format);
|
||||||
|
|
||||||
PutObjectRequest request = PutObjectRequest.builder()
|
PutObjectRequest request = PutObjectRequest.builder()
|
||||||
.bucket(bucketName)
|
.bucket(bucketName)
|
||||||
.key(path)
|
.key(path)
|
||||||
.contentLength(size)
|
.contentLength(size)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
||||||
|
|
||||||
log.info("File uploaded to S3: {}", path);
|
log.info("File uploaded to S3: {}", path);
|
||||||
|
|
||||||
return new StoredDocumentInfo(path, size, Instant.now());
|
return new StoredDocumentInfo(path, size, Instant.now());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to upload file to S3", e);
|
log.error("Failed to upload file to S3", e);
|
||||||
@ -513,7 +513,7 @@ public class DocumentCacheService {
|
|||||||
hibernate-orm:
|
hibernate-orm:
|
||||||
database:
|
database:
|
||||||
generation: drop-and-create
|
generation: drop-and-create
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 5672
|
port: 5672
|
||||||
@ -539,7 +539,7 @@ public class DocumentCacheService {
|
|||||||
hibernate-orm:
|
hibernate-orm:
|
||||||
database:
|
database:
|
||||||
generation: validate
|
generation: validate
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
host: ${RABBITMQ_HOST}
|
host: ${RABBITMQ_HOST}
|
||||||
port: ${RABBITMQ_PORT}
|
port: ${RABBITMQ_PORT}
|
||||||
@ -632,7 +632,7 @@ public class CamelHealthCheck implements HealthCheck {
|
|||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-config-yaml</artifactId>
|
<artifactId>quarkus-config-yaml</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Camel Extensions -->
|
<!-- Camel Extensions -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.camel.quarkus</groupId>
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
@ -646,7 +646,7 @@ public class CamelHealthCheck implements HealthCheck {
|
|||||||
<groupId>org.apache.camel.quarkus</groupId>
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
<artifactId>camel-quarkus-bean</artifactId>
|
<artifactId>camel-quarkus-bean</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Lombok -->
|
<!-- Lombok -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
@ -654,7 +654,7 @@ public class CamelHealthCheck implements HealthCheck {
|
|||||||
<version>${lombok.version}</version>
|
<version>${lombok.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Logging -->
|
<!-- Logging -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkiverse.logging.logback</groupId>
|
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ Best practices for securing Quarkus applications with authentication, authorizat
|
|||||||
@Path("/api/protected")
|
@Path("/api/protected")
|
||||||
@Authenticated
|
@Authenticated
|
||||||
public class ProtectedResource {
|
public class ProtectedResource {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
JsonWebToken jwt;
|
JsonWebToken jwt;
|
||||||
|
|
||||||
@ -65,20 +65,20 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
|||||||
@Provider
|
@Provider
|
||||||
@Priority(Priorities.AUTHENTICATION)
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
public class CustomAuthFilter implements ContainerRequestFilter {
|
public class CustomAuthFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity identity;
|
SecurityIdentity identity;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) {
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
// Reject immediately if header is absent or malformed
|
// Reject immediately if header is absent or malformed
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = authHeader.substring(7);
|
String token = authHeader.substring(7);
|
||||||
if (!validateToken(token)) {
|
if (!validateToken(token)) {
|
||||||
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
|
||||||
@ -100,7 +100,7 @@ public class CustomAuthFilter implements ContainerRequestFilter {
|
|||||||
@Path("/api/admin")
|
@Path("/api/admin")
|
||||||
@RolesAllowed("ADMIN")
|
@RolesAllowed("ADMIN")
|
||||||
public class AdminResource {
|
public class AdminResource {
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/users")
|
@Path("/users")
|
||||||
public List<UserDto> listUsers() {
|
public List<UserDto> listUsers() {
|
||||||
@ -118,7 +118,7 @@ public class AdminResource {
|
|||||||
|
|
||||||
@Path("/api/users")
|
@Path("/api/users")
|
||||||
public class UserResource {
|
public class UserResource {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ public class UserResource {
|
|||||||
@RolesAllowed("USER")
|
@RolesAllowed("USER")
|
||||||
public Response getUser(@PathParam("id") Long id) {
|
public Response getUser(@PathParam("id") Long id) {
|
||||||
// Check ownership
|
// Check ownership
|
||||||
if (!securityIdentity.hasRole("ADMIN") &&
|
if (!securityIdentity.hasRole("ADMIN") &&
|
||||||
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||||
return Response.status(Response.Status.FORBIDDEN).build();
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
@ -145,7 +145,7 @@ public class UserResource {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class SecurityService {
|
public class SecurityService {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ public class SecurityService {
|
|||||||
if (securityIdentity.isAnonymous()) {
|
if (securityIdentity.isAnonymous()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (securityIdentity.hasRole("ADMIN")) {
|
if (securityIdentity.hasRole("ADMIN")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -229,7 +229,7 @@ List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
|||||||
Optional<User> user = User.find("username", username).firstResultOptional();
|
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||||
|
|
||||||
// GOOD: Named parameters
|
// GOOD: Named parameters
|
||||||
List<User> users = User.list("email = :email and age > :minAge",
|
List<User> users = User.list("email = :email and age > :minAge",
|
||||||
Parameters.with("email", email).and("minAge", 18));
|
Parameters.with("email", email).and("minAge", 18));
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -256,7 +256,7 @@ public class User extends PanacheEntity {
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class PasswordService {
|
public class PasswordService {
|
||||||
|
|
||||||
public String hash(String plainPassword) {
|
public String hash(String plainPassword) {
|
||||||
return BcryptUtil.bcryptHash(plainPassword);
|
return BcryptUtil.bcryptHash(plainPassword);
|
||||||
}
|
}
|
||||||
@ -324,7 +324,7 @@ quarkus.vault.authentication.kubernetes.role=my-role
|
|||||||
```java
|
```java
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class SecretService {
|
public class SecretService {
|
||||||
|
|
||||||
@ConfigProperty(name = "api-key")
|
@ConfigProperty(name = "api-key")
|
||||||
String apiKey; // Fetched from Vault
|
String apiKey; // Fetched from Vault
|
||||||
|
|
||||||
@ -351,7 +351,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
|||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) {
|
public void filter(ContainerRequestContext requestContext) {
|
||||||
String clientId = getClientIdentifier();
|
String clientId = getClientIdentifier();
|
||||||
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||||
k -> RateLimiter.create(100.0)); // 100 requests per second
|
k -> RateLimiter.create(100.0)); // 100 requests per second
|
||||||
|
|
||||||
if (!limiter.tryAcquire()) {
|
if (!limiter.tryAcquire()) {
|
||||||
@ -377,25 +377,25 @@ public class RateLimitFilter implements ContainerRequestFilter {
|
|||||||
```java
|
```java
|
||||||
@Provider
|
@Provider
|
||||||
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||||
MultivaluedMap<String, Object> headers = response.getHeaders();
|
MultivaluedMap<String, Object> headers = response.getHeaders();
|
||||||
|
|
||||||
// Prevent clickjacking
|
// Prevent clickjacking
|
||||||
headers.putSingle("X-Frame-Options", "DENY");
|
headers.putSingle("X-Frame-Options", "DENY");
|
||||||
|
|
||||||
// XSS protection
|
// XSS protection
|
||||||
headers.putSingle("X-Content-Type-Options", "nosniff");
|
headers.putSingle("X-Content-Type-Options", "nosniff");
|
||||||
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
headers.putSingle("X-XSS-Protection", "1; mode=block");
|
||||||
|
|
||||||
// HSTS
|
// HSTS
|
||||||
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
|
||||||
// CSP — avoid 'unsafe-inline' for script-src as it negates XSS protection;
|
// CSP — avoid 'unsafe-inline' for script-src as it negates XSS protection;
|
||||||
// use nonces or hashes instead. 'unsafe-inline' for style-src is acceptable
|
// use nonces or hashes instead. 'unsafe-inline' for style-src is acceptable
|
||||||
// when CSS frameworks require it, but prefer nonces where possible.
|
// when CSS frameworks require it, but prefer nonces where possible.
|
||||||
headers.putSingle("Content-Security-Policy",
|
headers.putSingle("Content-Security-Policy",
|
||||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -412,11 +412,11 @@ public class AuditService {
|
|||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
public void logAccess(String resource, String action) {
|
public void logAccess(String resource, String action) {
|
||||||
String user = securityIdentity.isAnonymous()
|
String user = securityIdentity.isAnonymous()
|
||||||
? "anonymous"
|
? "anonymous"
|
||||||
: securityIdentity.getPrincipal().getName();
|
: securityIdentity.getPrincipal().getName();
|
||||||
|
|
||||||
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||||
user, action, resource, Instant.now());
|
user, action, resource, Instant.now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,19 +34,19 @@ Follow this structured approach for comprehensive, readable tests:
|
|||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@DisplayName("OrderService Unit Tests")
|
@DisplayName("OrderService Unit Tests")
|
||||||
class OrderServiceTest {
|
class OrderServiceTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private OrderRepository orderRepository;
|
private OrderRepository orderRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EventService eventService;
|
private EventService eventService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private FulfillmentPublisher fulfillmentPublisher;
|
private FulfillmentPublisher fulfillmentPublisher;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private OrderService orderService;
|
private OrderService orderService;
|
||||||
|
|
||||||
private CreateOrderCommand validCommand;
|
private CreateOrderCommand validCommand;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -60,16 +60,16 @@ class OrderServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Tests for createOrder")
|
@DisplayName("Tests for createOrder")
|
||||||
class CreateOrder {
|
class CreateOrder {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Should persist order and publish fulfillment event")
|
@DisplayName("Should persist order and publish fulfillment event")
|
||||||
void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
|
void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
doNothing().when(orderRepository).persist(any(Order.class));
|
doNothing().when(orderRepository).persist(any(Order.class));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
OrderReceipt receipt = orderService.createOrder(validCommand);
|
OrderReceipt receipt = orderService.createOrder(validCommand);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(receipt).isNotNull();
|
assertThat(receipt).isNotNull();
|
||||||
assertThat(receipt.customerId()).isEqualTo("customer-123");
|
assertThat(receipt.customerId()).isEqualTo("customer-123");
|
||||||
@ -83,7 +83,7 @@ class OrderServiceTest {
|
|||||||
void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
|
void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
|
CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
WebApplicationException exception = assertThrows(
|
WebApplicationException exception = assertThrows(
|
||||||
WebApplicationException.class,
|
WebApplicationException.class,
|
||||||
@ -101,13 +101,13 @@ class OrderServiceTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
doThrow(new PersistenceException("database unavailable"))
|
doThrow(new PersistenceException("database unavailable"))
|
||||||
.when(orderRepository).persist(any(Order.class));
|
.when(orderRepository).persist(any(Order.class));
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
PersistenceException exception = assertThrows(
|
PersistenceException exception = assertThrows(
|
||||||
PersistenceException.class,
|
PersistenceException.class,
|
||||||
() -> orderService.createOrder(validCommand)
|
() -> orderService.createOrder(validCommand)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).contains("database unavailable");
|
assertThat(exception.getMessage()).contains("database unavailable");
|
||||||
verify(eventService).createErrorEvent(
|
verify(eventService).createErrorEvent(
|
||||||
eq(validCommand),
|
eq(validCommand),
|
||||||
@ -125,7 +125,7 @@ class OrderServiceTest {
|
|||||||
NullPointerException.class,
|
NullPointerException.class,
|
||||||
() -> orderService.createOrder(null)
|
() -> orderService.createOrder(null)
|
||||||
);
|
);
|
||||||
|
|
||||||
verify(orderRepository, never()).persist(any(Order.class));
|
verify(orderRepository, never()).persist(any(Order.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
|
||||||
mockRabbitMQ.expectedMessageCount(1);
|
mockRabbitMQ.expectedMessageCount(1);
|
||||||
|
|
||||||
// Replace real endpoint with mock for testing
|
// Replace real endpoint with mock for testing
|
||||||
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
@ -192,13 +192,13 @@ class BusinessRulesRouteTest {
|
|||||||
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("business-rules-publisher");
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
// ASSERT — body is a JSON String after .marshal().json(JsonLibrary.Jackson)
|
// ASSERT — body is a JSON String after .marshal().json(JsonLibrary.Jackson)
|
||||||
mockRabbitMQ.assertIsSatisfied(5000);
|
mockRabbitMQ.assertIsSatisfied(5000);
|
||||||
|
|
||||||
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
|
||||||
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
|
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
assertThat(body).contains("\"documentId\":1");
|
assertThat(body).contains("\"documentId\":1");
|
||||||
@ -211,19 +211,19 @@ class BusinessRulesRouteTest {
|
|||||||
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");
|
||||||
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
camelContext.addEndpoint("mock:marshal", mockMarshal);
|
||||||
mockMarshal.expectedMessageCount(1);
|
mockMarshal.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
camelContext.getRouteController().stopRoute("business-rules-publisher");
|
||||||
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
|
||||||
advice.weaveAddLast().to("mock:marshal");
|
advice.weaveAddLast().to("mock:marshal");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("business-rules-publisher");
|
camelContext.getRouteController().startRoute("business-rules-publisher");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockMarshal.assertIsSatisfied(5000);
|
mockMarshal.assertIsSatisfied(5000);
|
||||||
|
|
||||||
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);
|
||||||
assertThat(body).contains("\"documentId\":1");
|
assertThat(body).contains("\"documentId\":1");
|
||||||
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
assertThat(body).contains("\"flowProfile\":\"BASIC\"");
|
||||||
@ -240,17 +240,17 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);
|
||||||
mockInvoice.expectedMessageCount(1);
|
mockInvoice.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("document-processing");
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("document-processing");
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBodyAndHeader("direct:process-document",
|
producerTemplate.sendBodyAndHeader("direct:process-document",
|
||||||
testPayload, "documentType", "INVOICE");
|
testPayload, "documentType", "INVOICE");
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockInvoice.assertIsSatisfied(5000);
|
mockInvoice.assertIsSatisfied(5000);
|
||||||
}
|
}
|
||||||
@ -261,23 +261,23 @@ class BusinessRulesRouteTest {
|
|||||||
// ARRANGE
|
// ARRANGE
|
||||||
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);
|
||||||
mockError.expectedMessageCount(1);
|
mockError.expectedMessageCount(1);
|
||||||
|
|
||||||
camelContext.getRouteController().stopRoute("document-processing");
|
camelContext.getRouteController().stopRoute("document-processing");
|
||||||
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
AdviceWith.adviceWith(camelContext, "document-processing", advice -> {
|
||||||
advice.weaveByToString(".*direct:validation-error-handler.*")
|
advice.weaveByToString(".*direct:validation-error-handler.*")
|
||||||
.replace().to("mock:error");
|
.replace().to("mock:error");
|
||||||
});
|
});
|
||||||
camelContext.getRouteController().startRoute("document-processing");
|
camelContext.getRouteController().startRoute("document-processing");
|
||||||
|
|
||||||
// Mock validator bean to throw exception
|
// Mock validator bean to throw exception
|
||||||
when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
producerTemplate.sendBody("direct:process-document", testPayload);
|
producerTemplate.sendBody("direct:process-document", testPayload);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
mockError.assertIsSatisfied(5000);
|
mockError.assertIsSatisfied(5000);
|
||||||
|
|
||||||
Exception exception = mockError.getExchanges().get(0).getException();
|
Exception exception = mockError.getExchanges().get(0).getException();
|
||||||
assertThat(exception).isInstanceOf(ValidationException.class);
|
assertThat(exception).isInstanceOf(ValidationException.class);
|
||||||
assertThat(exception.getMessage()).contains("Invalid document");
|
assertThat(exception.getMessage()).contains("Invalid document");
|
||||||
@ -295,13 +295,13 @@ class EventServiceTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EventRepository eventRepository;
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private EventService eventService;
|
private EventService eventService;
|
||||||
|
|
||||||
private BusinessRulesPayload testPayload;
|
private BusinessRulesPayload testPayload;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -314,19 +314,19 @@ class EventServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Tests for createSuccessEvent")
|
@DisplayName("Tests for createSuccessEvent")
|
||||||
class CreateSuccessEvent {
|
class CreateSuccessEvent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Should create success event with correct attributes")
|
@DisplayName("Should create success event with correct attributes")
|
||||||
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() ->
|
assertDoesNotThrow(() ->
|
||||||
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(eventRepository).persist(argThat(event ->
|
verify(eventRepository).persist(argThat(event ->
|
||||||
event.getType().equals("DOCUMENT_PROCESSED") &&
|
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||||
event.getStatus() == EventStatus.SUCCESS &&
|
event.getStatus() == EventStatus.SUCCESS &&
|
||||||
event.getPayload().equals("{\"documentId\":1}") &&
|
event.getPayload().equals("{\"documentId\":1}") &&
|
||||||
@ -339,13 +339,13 @@ class EventServiceTest {
|
|||||||
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
Object nullPayload = null;
|
Object nullPayload = null;
|
||||||
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
NullPointerException exception = assertThrows(
|
NullPointerException exception = assertThrows(
|
||||||
NullPointerException.class,
|
NullPointerException.class,
|
||||||
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
|
||||||
verify(eventRepository, never()).persist(any());
|
verify(eventRepository, never()).persist(any());
|
||||||
}
|
}
|
||||||
@ -354,20 +354,20 @@ class EventServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Tests for createErrorEvent")
|
@DisplayName("Tests for createErrorEvent")
|
||||||
class CreateErrorEvent {
|
class CreateErrorEvent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Should create error event with error message")
|
@DisplayName("Should create error event with error message")
|
||||||
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
String errorMessage = "Processing failed";
|
String errorMessage = "Processing failed";
|
||||||
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
assertDoesNotThrow(() ->
|
assertDoesNotThrow(() ->
|
||||||
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
verify(eventRepository).persist(argThat(event ->
|
verify(eventRepository).persist(argThat(event ->
|
||||||
event.getType().equals("PROCESSING_ERROR") &&
|
event.getType().equals("PROCESSING_ERROR") &&
|
||||||
event.getStatus() == EventStatus.ERROR &&
|
event.getStatus() == EventStatus.ERROR &&
|
||||||
event.getErrorMessage().equals(errorMessage) &&
|
event.getErrorMessage().equals(errorMessage) &&
|
||||||
@ -384,7 +384,7 @@ class EventServiceTest {
|
|||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,13 +400,13 @@ class FileStorageServiceTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private S3Client s3Client;
|
private S3Client s3Client;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExecutorService executorService;
|
private ExecutorService executorService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private FileStorageService fileStorageService;
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
private InputStream testInputStream;
|
private InputStream testInputStream;
|
||||||
private LogContext testLogContext;
|
private LogContext testLogContext;
|
||||||
|
|
||||||
@ -421,7 +421,7 @@ class FileStorageServiceTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Tests for uploadOriginalFile")
|
@DisplayName("Tests for uploadOriginalFile")
|
||||||
class UploadOriginalFile {
|
class UploadOriginalFile {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Should successfully upload file and return document info")
|
@DisplayName("Should successfully upload file and return document info")
|
||||||
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
|
||||||
@ -430,23 +430,23 @@ class FileStorageServiceTest {
|
|||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
.thenReturn(PutObjectResponse.builder().build());
|
.thenReturn(PutObjectResponse.builder().build());
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
CompletableFuture<StoredDocumentInfo> future =
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL);
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
StoredDocumentInfo result = future.join();
|
StoredDocumentInfo result = future.join();
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.getPath()).isNotBlank();
|
assertThat(result.getPath()).isNotBlank();
|
||||||
assertThat(result.getSize()).isEqualTo(1024L);
|
assertThat(result.getSize()).isEqualTo(1024L);
|
||||||
assertThat(result.getUploadedAt()).isNotNull();
|
assertThat(result.getUploadedAt()).isNotNull();
|
||||||
|
|
||||||
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,15 +458,15 @@ class FileStorageServiceTest {
|
|||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
|
||||||
.thenThrow(new StorageException("S3 unavailable"));
|
.thenThrow(new StorageException("S3 unavailable"));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
CompletableFuture<StoredDocumentInfo> future =
|
CompletableFuture<StoredDocumentInfo> future =
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL);
|
testLogContext, InvoiceFormat.UBL);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThatThrownBy(() -> future.join())
|
assertThatThrownBy(() -> future.join())
|
||||||
.isInstanceOf(CompletionException.class)
|
.isInstanceOf(CompletionException.class)
|
||||||
@ -479,17 +479,17 @@ class FileStorageServiceTest {
|
|||||||
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
AtomicReference<LogContext> capturedContext = new AtomicReference<>();
|
||||||
|
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
capturedContext.set(CustomLog.getCurrentContext());
|
capturedContext.set(CustomLog.getCurrentContext());
|
||||||
((Runnable) invocation.getArgument(0)).run();
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
return null;
|
return null;
|
||||||
}).when(executorService).execute(any(Runnable.class));
|
}).when(executorService).execute(any(Runnable.class));
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||||
testLogContext, InvoiceFormat.UBL).join();
|
testLogContext, InvoiceFormat.UBL).join();
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
assertThat(capturedContext.get()).isNotNull();
|
assertThat(capturedContext.get()).isNotNull();
|
||||||
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
||||||
@ -641,7 +641,7 @@ class DocumentIntegrationTest {
|
|||||||
<goal>prepare-agent</goal>
|
<goal>prepare-agent</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
|
|
||||||
<!-- Generate coverage report -->
|
<!-- Generate coverage report -->
|
||||||
<execution>
|
<execution>
|
||||||
<id>report</id>
|
<id>report</id>
|
||||||
@ -650,7 +650,7 @@ class DocumentIntegrationTest {
|
|||||||
<goal>report</goal>
|
<goal>report</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
|
|
||||||
<!-- Enforce coverage thresholds -->
|
<!-- Enforce coverage thresholds -->
|
||||||
<execution>
|
<execution>
|
||||||
<id>check</id>
|
<id>check</id>
|
||||||
@ -705,14 +705,14 @@ mvn jacoco:check
|
|||||||
<artifactId>quarkus-junit5-mockito</artifactId>
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Mockito -->
|
<!-- Mockito -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- AssertJ (preferred over JUnit assertions) -->
|
<!-- AssertJ (preferred over JUnit assertions) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.assertj</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
@ -720,14 +720,14 @@ mvn jacoco:check
|
|||||||
<version>3.24.2</version>
|
<version>3.24.2</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- REST Assured -->
|
<!-- REST Assured -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.rest-assured</groupId>
|
<groupId>io.rest-assured</groupId>
|
||||||
<artifactId>rest-assured</artifactId>
|
<artifactId>rest-assured</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Camel Testing -->
|
<!-- Camel Testing -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.camel.quarkus</groupId>
|
<groupId>org.apache.camel.quarkus</groupId>
|
||||||
|
|||||||
@ -437,28 +437,28 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Cache Maven packages
|
- name: Cache Maven packages
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.m2
|
path: ~/.m2
|
||||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: mvn clean verify -DskipTests
|
run: mvn clean verify -DskipTests
|
||||||
|
|
||||||
- name: Test with Coverage
|
- name: Test with Coverage
|
||||||
run: mvn test jacoco:report jacoco:check
|
run: mvn test jacoco:report jacoco:check
|
||||||
|
|
||||||
- name: Security Scan
|
- name: Security Scan
|
||||||
run: mvn org.owasp:dependency-check-maven:check
|
run: mvn org.owasp:dependency-check-maven:check
|
||||||
|
|
||||||
- name: Upload Coverage
|
- name: Upload Coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@ -43,7 +43,7 @@ function run() {
|
|||||||
if (test('uses read-only permissions and non-persisting checkout credentials', () => {
|
if (test('uses read-only permissions and non-persisting checkout credentials', () => {
|
||||||
assert.match(source, /permissions:\r?\n\s+contents: read/);
|
assert.match(source, /permissions:\r?\n\s+contents: read/);
|
||||||
assert.doesNotMatch(source, /^\s+[A-Za-z-]+:\s*write\b/m);
|
assert.doesNotMatch(source, /^\s+[A-Za-z-]+:\s*write\b/m);
|
||||||
assert.match(source, /uses: actions\/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd/);
|
assert.match(source, /uses: actions\/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10/);
|
||||||
assert.match(source, /persist-credentials: false/);
|
assert.match(source, /persist-credentials: false/);
|
||||||
assert.doesNotMatch(source, /id-token:\s*write/);
|
assert.doesNotMatch(source, /id-token:\s*write/);
|
||||||
assert.doesNotMatch(source, /actions\/cache@/);
|
assert.doesNotMatch(source, /actions\/cache@/);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user