Conversation
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
📝 WalkthroughWalkthroughAdds first-class Quarkus support across agents, skills, docs, and manifests: four new Quarkus skill modules; framework-detection and Quarkus-aware logic in Java resolver/reviewer agents; many localized Quarkus SKILL pages; README/manifests updates; and Java coding-standards expanded to include Quarkus conventions. Changes
Sequence Diagram(s)sequenceDiagram
participant Agent as Agent
participant Repo as Repository (pom.xml / build.gradle)
participant Detector as Framework Detector
participant Resolver as Build Resolver
participant Skills as Pattern Skills (Spring/Quarkus)
Agent->>Repo: Inspect build files
Repo-->>Detector: Provide markers (spring-boot / quarkus)
Detector-->>Agent: Framework = [SPRING|QUARKUS|BOTH|UNKNOWN]
Agent->>Resolver: Run resolver with Framework
Resolver->>Skills: Select `springboot-patterns` or `quarkus-patterns`
Skills-->>Resolver: Return framework-specific fixes & commands
Resolver-->>Agent: Output (Framework, Errors, Fixes, Commands)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds four Quarkus skills (
Confidence Score: 4/5Not safe to merge until the One P1 regression: the PR accidentally deleted all 80+ command registrations from
|
| Filename | Overview |
|---|---|
| agent.yaml | Quarkus skills correctly added to skills: list, but the entire commands: section (80+ harness commands) was accidentally deleted — a clear regression. |
| skills/quarkus-patterns/SKILL.md | Comprehensive Quarkus pattern catalog; BOM fix applied, @Slf4j/bucketName fixes applied, but EventService and BusinessRulesPublisher sections are missing opening code fences, causing malformed markdown rendering. |
| skills/quarkus-security/SKILL.md | Auth filter now correctly rejects absent/malformed Authorization header; rate limiter uses getRemoteAddr(). No remaining issues. |
| skills/quarkus-tdd/SKILL.md | Exception type corrected to NullPointerException; Camel route assertions fixed; executorService.execute() stubbed correctly with doAnswer. |
| skills/quarkus-verification/SKILL.md | doNothing() correctly replaces the invalid thenReturn stub for void Panache persist(). Verification loop phases are complete. |
| manifests/install-modules.json | All four Quarkus skills correctly added to framework-language and security install modules. |
| agents/java-build-resolver.md | Spring Boot vs Quarkus auto-detection added; Quarkus-specific error table and Maven/Gradle commands are thorough and accurate. |
| agents/java-reviewer.md | Dual Spring Boot / Quarkus detection added; framework-specific review checklists are clear and actionable. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Java Project] --> B{Framework Detection}
B -->|pom.xml contains 'quarkus'| C[QUARKUS path]
B -->|pom.xml contains 'spring-boot'| D[SPRING path]
B -->|neither detected| E[General Java rules]
C --> C1[quarkus-patterns]
C --> C2[quarkus-security]
C --> C3[quarkus-tdd]
C --> C4[quarkus-verification]
D --> D1[springboot-patterns]
D --> D2[springboot-security]
D --> D3[springboot-tdd]
D --> D4[springboot-verification]
C1 & C2 & C3 & C4 --> F[java-reviewer agent]
D1 & D2 & D3 & D4 --> F
C1 & C2 & C3 & C4 --> G[java-build-resolver agent]
D1 & D2 & D3 & D4 --> G
Comments Outside Diff (1)
-
agent.yaml, line 149-150 (link)Entire
commands:registry accidentally deletedThe PR removed all 80+ slash-command registrations from
agent.yamlwhile adding the four Quarkus skills toskills:. The base branch carried acommands:block covering every harness command (tdd,plan,e2e,code-review,build-fix,learn, etc.); the head branch has none. Any harness or tooling that reads this manifest to discover available commands will find an empty set, making all those commands invisible.The Quarkus additions should only touch the
skills:list — thecommands:section should be restored in full.
Reviews (17): Last reviewed commit: "fix: use doNothing for void Panache pers..." | Re-trigger Greptile
There was a problem hiding this comment.
Pull request overview
This PR adds first-class Quarkus support to the Java portion of the ECC catalog by introducing four quarkus-* skills and wiring them into the installer, rule references, agents, and documentation surfaces—mirroring existing springboot-* integration.
Changes:
- Added new Quarkus skills (
quarkus-patterns,quarkus-security,quarkus-tdd,quarkus-verification) and registered them inagent.yamlandmanifests/install-modules.json. - Updated Java rules (
rules/java/*) and prompt/installer skills (skills/configure-ecc,skills/prompt-optimizer) to reference Quarkus skills. - Updated READMEs, agent prompts, and localized docs trees to include Quarkus entries.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/quarkus-verification/SKILL.md | Adds Quarkus verification-loop skill content and frontmatter (origin: ECC). |
| skills/quarkus-tdd/SKILL.md | Adds Quarkus TDD skill content and frontmatter (origin: ECC). |
| skills/quarkus-security/SKILL.md | Adds Quarkus security review skill content and frontmatter (origin: ECC). |
| skills/quarkus-patterns/SKILL.md | Adds Quarkus architecture/patterns skill content and frontmatter (origin: ECC). |
| skills/prompt-optimizer/SKILL.md | Adds Quarkus/Java tech-stack mapping to skills + agent selection. |
| skills/java-coding-standards/SKILL.md | Expands Java standards to cover both Spring Boot and Quarkus with framework detection guidance. |
| skills/configure-ecc/SKILL.md | Updates “Framework & Language” category text/count and adds Quarkus skills to the selection table. |
| rules/java/testing.md | Adds references to quarkus-tdd for Quarkus testing guidance. |
| rules/java/security.md | Adds reference to quarkus-security for Quarkus security guidance. |
| rules/java/patterns.md | Adds reference to quarkus-patterns for Quarkus architecture guidance. |
| README.zh-CN.md | Updates repo tree and install example to include Quarkus skills. |
| README.md | Updates repo tree and install example to include Quarkus skills. |
| manifests/install-modules.json | Registers Quarkus skills in installer modules (framework-language + security). |
| docs/zh-CN/skills/quarkus-verification/SKILL.md | Adds zh-CN docs entry for Quarkus verification skill. |
| docs/zh-CN/skills/quarkus-security/SKILL.md | Adds zh-CN docs entry for Quarkus security skill. |
| docs/zh-CN/skills/quarkus-patterns/SKILL.md | Adds zh-CN docs entry for Quarkus patterns skill. |
| docs/zh-CN/skills/prompt-optimizer/SKILL.md | Adds Quarkus/Java tech-stack row in the zh-CN prompt-optimizer doc. |
| docs/zh-CN/skills/configure-ecc/SKILL.md | Updates zh-CN configure-ecc table to include Quarkus skills and dependency note. |
| docs/zh-CN/rules/java/testing.md | Adds zh-CN rule references to quarkus-tdd. |
| docs/zh-CN/rules/java/security.md | Adds zh-CN rule reference to quarkus-security. |
| docs/zh-CN/rules/java/patterns.md | Adds zh-CN rule reference to quarkus-patterns. |
| docs/zh-CN/README.md | Updates zh-CN README tree and install example to include Quarkus skills. |
| docs/zh-CN/agents/java-reviewer.md | Updates zh-CN java-reviewer agent doc to reference both Spring and Quarkus patterns. |
| docs/zh-CN/agents/java-build-resolver.md | Updates zh-CN java-build-resolver agent doc to reference both Spring and Quarkus patterns. |
| docs/tr/skills/quarkus-verification/SKILL.md | Adds Turkish docs entry for Quarkus verification skill. |
| docs/tr/skills/quarkus-security/SKILL.md | Adds Turkish docs entry for Quarkus security skill. |
| docs/tr/skills/quarkus-patterns/SKILL.md | Adds Turkish docs entry for Quarkus patterns skill. |
| docs/tr/agents/java-reviewer.md | Updates Turkish java-reviewer agent doc to reference both Spring and Quarkus patterns. |
| docs/tr/agents/java-build-resolver.md | Updates Turkish java-build-resolver agent doc to reference both Spring and Quarkus patterns. |
| docs/ja-JP/skills/README.md | Lists Quarkus skills in the Japanese skills index. |
| docs/ja-JP/skills/quarkus-verification/SKILL.md | Adds Japanese docs entry for Quarkus verification skill. |
| docs/ja-JP/skills/quarkus-security/SKILL.md | Adds Japanese docs entry for Quarkus security skill. |
| docs/ja-JP/skills/quarkus-patterns/SKILL.md | Adds Japanese docs entry for Quarkus patterns skill. |
| docs/ja-JP/skills/configure-ecc/SKILL.md | Updates Japanese configure-ecc table/count to include Quarkus skills. |
| docs/ja-JP/README.md | Updates Japanese README tree to include Quarkus skills. |
| agents/java-reviewer.md | Expands java-reviewer agent to support Spring Boot + Quarkus via framework detection. |
| agents/java-build-resolver.md | Expands java-build-resolver agent to support Spring Boot + Quarkus via framework detection. |
| agent.yaml | Registers the four Quarkus skills in the top-level skill list. |
| .opencode/prompts/agents/java-reviewer.txt | Updates opencode prompt to reference Quarkus patterns alongside Spring Boot. |
| .opencode/prompts/agents/java-build-resolver.txt | Updates opencode prompt to reference Quarkus patterns alongside Spring Boot. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
docs/zh-CN/skills/configure-ecc/SKILL.md (1)
108-132:⚠️ Potential issue | 🟡 MinorFix skill count in Framework & Language header.
After adding the 4 Quarkus entries, this category now lists 25 skills, but the header still says 21. Please update the count to keep the installer guidance accurate.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/zh-CN/skills/configure-ecc/SKILL.md` around lines 108 - 132, The category header "**类别:框架与语言(21项技能)**" is out of date after adding four Quarkus entries; update that header to read "**类别:框架与语言(25项技能)**" so the count matches the listed skills (references: the header text string and the Quarkus entries `quarkus-patterns`, `quarkus-security`, `quarkus-tdd`, `quarkus-verification`).skills/java-coding-standards/SKILL.md (1)
19-57:⚠️ Potential issue | 🟡 MinorAlign this skill to the canonical section template.
Please add explicit
When to Use,How It Works, andExamplesheadings (currentlyWhen to Activate+ distributed examples).As per coding guidelines:
skills/**/*.md: Skill format must be Markdown with clear sections for When to Use, How It Works, and Examples.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/java-coding-standards/SKILL.md` around lines 19 - 57, Rename the "When to Activate" heading to "When to Use", add a new "How It Works" section that succinctly explains the core principles (clarity over cleverness, immutable-by-default, fail-fast, naming/package consistency, Quarkus note about build-time processing), and consolidate all code snippets and usage bullets into a new "Examples" section; update heading order to "When to Use", "How It Works", "Examples" and ensure existing naming and immutability bullets move into "How It Works" while the Java code blocks and framework-specific resource/controller notes go under "Examples" so the file matches the canonical skill template.
🟡 Minor comments (9)
docs/ja-JP/skills/configure-ecc/SKILL.md-99-102 (1)
99-102:⚠️ Potential issue | 🟡 MinorAdd Quarkus dependency check in Step 4c.
Since
quarkus-tddis now listed, Step 4c should also include a cross-reference check forquarkus-tdd→quarkus-patterns, matching the existing pattern used for other frameworks.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ja-JP/skills/configure-ecc/SKILL.md` around lines 99 - 102, Add a cross-reference check in Step 4c to ensure that selecting `quarkus-tdd` also requires `quarkus-patterns` like the other framework checks; update the Step 4c logic that currently validates framework selections to include a rule that when `quarkus-tdd` is present it enforces/auto-adds `quarkus-patterns` (or emits the same error message used for similar pairs), referencing the `quarkus-tdd` and `quarkus-patterns` identifiers so the check is consistent with existing pattern checks.docs/ja-JP/skills/configure-ecc/SKILL.md-68-68 (1)
68-68:⚠️ Potential issue | 🟡 MinorCorrect the total skill count.
Line 68 says 31 skills, but the listed categories total 32 (20 + 3 + 8 + 1). Please update the number to avoid operator confusion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ja-JP/skills/configure-ecc/SKILL.md` at line 68, Update the incorrect skill count in the sentence "31個のスキルが4つのカテゴリに分類されています。`multiSelect: true` で `AskUserQuestion` を使用します:" to "32個のスキルが4つのカテゴリに分類されています。" so the total (20 + 3 + 8 + 1) matches the stated count; edit the phrase in SKILL.md replacing "31" with "32".skills/quarkus-tdd/SKILL.md-876-876 (1)
876-876:⚠️ Potential issue | 🟡 MinorHyphenate compound modifier for readability.
error handling routesshould beerror-handling routeson Line 876.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/quarkus-tdd/SKILL.md` at line 876, Replace the unhyphenated phrase "error handling routes" with the hyphenated compound "error-handling routes" in the SKILL.md content so the modifier is correctly formed ( locate the occurrence of the string "error handling routes" and change it to "error-handling routes" ).docs/tr/skills/quarkus-tdd/SKILL.md-22-29 (1)
22-29:⚠️ Potential issue | 🟡 MinorUse the required section title
How It Works.The file has
## Workflow, but TR skill docs require explicit sections:When to Use,How It Works, andExamples. Please rename/add headings accordingly.As per coding guidelines:
docs/tr/skills/**/*.md: Skills should be formatted as Markdown files with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/skills/quarkus-tdd/SKILL.md` around lines 22 - 29, Replace the incorrect "## Workflow" heading with the required "## How It Works" heading and ensure the document includes the other mandatory top-level sections titled exactly "When to Use" and "Examples"; update the content under the current workflow list to live under "How It Works", add a short "When to Use" section that explains the intended audience or scenario, and add an "Examples" section with any sample usages or links so the file conforms to the docs/tr/skills/**/*.md guideline requiring 'When to Use', 'How It Works', and 'Examples'.docs/tr/skills/quarkus-verification/SKILL.md-11-20 (1)
11-20:⚠️ Potential issue | 🟡 MinorAdd canonical
How It WorksandExamplessections.Current sectioning (
When to Activate, phase blocks) doesn’t satisfy the required TR skill section format.As per coding guidelines:
docs/tr/skills/**/*.md: Skills should be formatted as Markdown files with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/skills/quarkus-verification/SKILL.md` around lines 11 - 20, Rename the top section "When to Activate" to the canonical header "When to Use", then add a new "How It Works" section that describes the Phase structure (e.g., "Phase 1: Build" etc.) and details the step-by-step verification flow (build → lint → test → security scan → native compilation and thresholds like 80% coverage), and finally add an "Examples" section with at least one concrete example use case (e.g., PR after dependency upgrade, pre-production staging release) showing commands/expected outcomes; update headings "Phase 1: Build" to live under "How It Works" and ensure all three required headers ("When to Use", "How It Works", "Examples") appear in SKILL.md.docs/zh-CN/skills/quarkus-tdd/SKILL.md-11-29 (1)
11-29:⚠️ Potential issue | 🟡 MinorUse the required zh-CN skill section headings.
Please add/rename to the canonical sections:
When to use,How it works, andExamples.As per coding guidelines:
docs/zh-CN/skills/**/*.md: Skills must be formatted as Markdown files with clear sections: 'When to use', 'How it works', and 'Examples'.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/zh-CN/skills/quarkus-tdd/SKILL.md` around lines 11 - 29, The document uses non-canonical headings; rename and/or add the required zh-CN skill sections exactly as "When to use", "How it works", and "Examples" (case-sensitive) in docs/zh-CN/skills/quarkus-tdd/SKILL.md: change "When to Use" to "When to use", convert the "Workflow" block into "How it works" (preserving steps 1–4), and add an "Examples" section (even if stubbed) after "How it works"; ensure each section is a Markdown heading (##) so the file conforms to docs/zh-CN/skills/**/*.md guidelines.docs/zh-CN/skills/quarkus-verification/SKILL.md-11-20 (1)
11-20:⚠️ Potential issue | 🟡 MinorConform to required zh-CN skill section structure.
Add explicit
When to use,How it works, andExamplessections to match repository skill format.As per coding guidelines:
docs/zh-CN/skills/**/*.md: Skills must be formatted as Markdown files with clear sections: 'When to use', 'How it works', and 'Examples'.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/zh-CN/skills/quarkus-verification/SKILL.md` around lines 11 - 20, Replace the current "When to Activate" section with a properly named "When to use" heading and split the content into three required sections: add a "How it works" section that briefly explains the verification steps (build → lint → test → security scan → native compilation and when each is run), and an "Examples" section that shows 1–2 concrete usage scenarios (e.g., pre-PR after refactor, pre-deploy staging verification), keeping the existing "Phase 1: Build" content under the workflow or "How it works" as appropriate so the file contains clearly labeled "When to use", "How it works", and "Examples" sections per the docs/zh-CN/skills/*.md format.docs/tr/skills/quarkus-patterns/SKILL.md-11-24 (1)
11-24:⚠️ Potential issue | 🟡 MinorPlease include canonical skill sections (
How It Works,Examples).The page is rich in content but does not follow the required TR skill section headings.
As per coding guidelines:
docs/tr/skills/**/*.md: Skills should be formatted as Markdown files with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/skills/quarkus-patterns/SKILL.md` around lines 11 - 24, The file uses a "When to Activate" heading and lacks the required canonical sections; rename or duplicate the "When to Activate" section to "When to Use", and add two new top-level sections "How It Works" and "Examples" under the existing content (move or expand the "Service Layer with Multiple Dependencies (Lombok)" part into the "Examples" section); ensure the new "How It Works" explains core concepts (resource→service→repository flow, event-driven hooks, Hibernate Panache behavior, GraalVM notes) and that each example has a short title and concrete steps or snippets pointers to illustrate the skills.docs/ja-JP/skills/quarkus-tdd/SKILL.md-876-876 (1)
876-876:⚠️ Potential issue | 🟡 MinorHyphenate compound modifier for clarity.
Use “error-handling routes” instead of “error handling routes”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ja-JP/skills/quarkus-tdd/SKILL.md` at line 876, Update the sentence "Test error handling routes separately" to hyphenate the compound modifier; replace it with "Test error-handling routes separately" wherever that exact string appears (e.g., the line containing that sentence in SKILL.md) to improve clarity.
🧹 Nitpick comments (6)
docs/tr/agents/java-reviewer.md (1)
92-94: Consider updating scope text to include Quarkus explicitly.The new references are good. For consistency, align the top-level agent description with this dual-framework intent instead of emphasizing only Spring Boot.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/agents/java-reviewer.md` around lines 92 - 94, Update the agent's top-level scope text to explicitly mention both Spring Boot and Quarkus (not only Spring Boot) so the description aligns with the added references; modify the overview/intro paragraph that currently emphasizes Spring Boot to include Quarkus as a peer framework and ensure the skill links `skill: springboot-patterns` and `skill: quarkus-patterns` are referenced in that scope text so readers see the dual-framework intent.docs/zh-CN/agents/java-build-resolver.md (1)
154-156: Good Quarkus addition; consider scope-text alignment.Lines 154–156 are a good extension. Consider updating the agent description text to explicitly include Quarkus build failures so scope and references fully match.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/zh-CN/agents/java-build-resolver.md` around lines 154 - 156, Update the agent description to explicitly mention Quarkus build failures so the scope matches the new references; specifically, where the doc references the SPRING and QUARKUS patterns (the bullets with "**[SPRING]**: `skill: springboot-patterns`" and "**[QUARKUS]**: `skill: quarkus-patterns`"), add a clear phrase in the agent description that Quarkus build failures are handled/diagnosed (e.g., "includes Quarkus build failures and troubleshooting"), and ensure any scope or summary lines use parallel wording for both Spring and Quarkus so the references and scope text are aligned.docs/zh-CN/agents/java-reviewer.md (1)
105-107: Align agent scope wording with Quarkus support.Nice addition at Lines 105–107. To avoid ambiguity, consider updating the frontmatter/intro scope text (currently Spring Boot-centric) so it explicitly includes Quarkus as first-class coverage.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/zh-CN/agents/java-reviewer.md` around lines 105 - 107, The intro/frontmatter scope text is Spring Boot–centric and should explicitly name Quarkus as first-class support; update the document's intro language to mention both Spring Boot and Quarkus equally and ensure the example bullet list references both skills using the existing tokens `skill: springboot-patterns` and `skill: quarkus-patterns` (the current SPRING/QUARKUS bullets at Lines 105–107). Modify the scope sentence(s) in the frontmatter/intro so they read like "Supports Spring Boot and Quarkus patterns" (or similar), and verify any downstream references/search labels in the doc match that expanded scope.skills/quarkus-tdd/SKILL.md (1)
22-908: Add explicitHow It WorksandExamplessections.The file already has strong content, but it should include the required skill section headings explicitly to match repository skill format expectations.
As per coding guidelines: "
skills/**/*.md: Skill format must be Markdown with clear sections for When to Use, How It Works, and Examples."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/quarkus-tdd/SKILL.md` around lines 22 - 908, The SKILL.md is missing explicit "How It Works" and "Examples" sections required by the repository skill format; add two new top-level sections "## How It Works" and "## Examples" (alongside the existing "When to Use" / "Workflow" material) and populate them with concise content: under "How It Works" describe the TDD flow and how the provided tests (e.g., As2ProcessingServiceTest, BusinessRulesRouteTest, FileStorageServiceTest) exercise validation, async upload, Camel routes and event creation; under "Examples" include short, runnable examples/links to the unit test snippets shown (referencing As2ProcessingServiceTest, BusinessRulesRouteTest, EventServiceTest) demonstrating typical usages. Ensure headings match exact strings "How It Works" and "Examples" so the skill validator recognizes them.docs/zh-CN/skills/quarkus-patterns/SKILL.md (1)
1-5: Filename convention violation (SKILL.md).The uppercase filename does not follow the lowercase-with-hyphens convention for Markdown files. Please rename consistently (and apply the same fix to the new Quarkus skill files).
As per coding guidelines: “
**/*.{md,js,ts,json}: File naming convention: use lowercase with hyphens.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/zh-CN/skills/quarkus-patterns/SKILL.md` around lines 1 - 5, The file name SKILL.md violates the repository naming convention (use lowercase with hyphens); rename docs/zh-CN/skills/quarkus-patterns/SKILL.md to skill.md or quarkus-patterns.md (lowercase-with-hyphens) and update any references/imports to SKILL.md accordingly; apply the same rename pattern to any new Quarkus skill files so all Markdown files match the "**/*.{md,js,ts,json}" lowercase-with-hyphens rule.docs/ja-JP/skills/quarkus-tdd/SKILL.md (1)
531-534: Async test stubbing targets the wrong executor API path.The test stubs
executorService.submit(...), butCompletableFuture.supplyAsync(..., executorService)schedules work via theExecutorpath (execute), so this stub does not validate the intended behavior.Proposed snippet adjustment
- when(executorService.submit(any(Callable.class))).thenAnswer(invocation -> { - Callable<?> callable = invocation.getArgument(0); - return CompletableFuture.completedFuture(callable.call()); - }); + ExecutorService realExecutor = Executors.newSingleThreadExecutor(); + fileStorageService = new FileStorageService(s3Client, realExecutor); + // assert via future.join() and verify interactionsAlso applies to: 559-561
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ja-JP/skills/quarkus-tdd/SKILL.md` around lines 531 - 534, The test currently stubs executorService.submit(...) but CompletableFuture.supplyAsync(..., executorService) uses the Executor.execute path; update the stub to intercept executorService.execute(Runnable) (or use doAnswer on execute) and run the provided Runnable synchronously (invoking runnable.run()) so the async work is executed during the test; locate the stub near the existing thenAnswer for executorService.submit and replace it with a stub for executorService.execute that triggers the Runnable, ensuring the same behavior is applied at the other occurrence (lines around the second instance).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/ja-JP/skills/quarkus-patterns/SKILL.md`:
- Around line 7-754: The JA-JP skill file
(docs/ja-JP/skills/quarkus-patterns/SKILL.md) is written in English; localize
the entire user-facing content into natural Japanese while preserving technical
identifiers and code blocks (e.g., headings like "Quarkus Development Patterns",
sections "When to Activate", code examples such as As2ProcessingService,
ProcessingService, EventService, BusinessRulesPublisher, BusinessRulesRoute,
FileMonitoringRoute, DocumentResource, DocumentRepository, FileStorageService,
DatabaseHealthCheck, etc.) and keep config/YAML/XML and code samples unchanged;
ensure section headings, key patterns, and best-practices lists are translated,
update any in-text references to symbols/classes unchanged, and run a quick
spell/consistency pass to ensure Japanese grammar and terminology are correct.
In `@docs/ja-JP/skills/quarkus-security/SKILL.md`:
- Around line 11-453: The MD file uses non-compliant headings (starts with "When
to Activate")—rename that section to "When to Use", add a new "How It Works"
section that summarizes the concepts and flows (e.g., JWT/OIDC validation,
SecurityIdentity, role checks, filters) and move the explanatory paragraphs
(like the Authentication and Authorization descriptions) under it, and create a
top-level "Examples" section that contains the code samples (JWT Authentication,
Custom Authentication Filter, Role-Based Access Control, Programmatic Security,
Bean Validation, SQL Injection examples, Password Hashing, CORS, Vault, Rate
Limiting, Security Headers, Audit Logging) so the document contains the required
three headings: "When to Use", "How It Works", and "Examples" while preserving
existing subheadings like "JWT Authentication", "Custom Authentication Filter",
"Role-Based Access Control", "SecurityService", and "PasswordService".
In `@docs/ja-JP/skills/quarkus-tdd/SKILL.md`:
- Around line 375-376: The test snippets use eventService.validate(any()) and
documentService.list(0, 20) that don't match the documented APIs; update the
test stubs to call the documented event methods (use
eventService.createSuccessEvent(...) / eventService.createErrorEvent(...)
instead of eventService.validate(...)) and change the documentService stub to
return the documented paginated type (replace List<Document> with the paginated
return type used in the PR, e.g., Page<Document> or PaginatedResult<Document>,
and ensure the stub signature matches documentService.list(page, size)
accordingly). Ensure the mocked responses and any assertions use the documented
method names and the paginated result shape.
- Around line 7-908: The file under the "Quarkus TDD Workflow" skill is still in
English; translate the entire SKILL.md content into Japanese while preserving
all markdown structure and code blocks (examples, headings like "Workflow",
"Unit Tests with `@Nested` Organization", "Testing Camel Routes", etc.), keep
technical names (class names, annotations, config keys, code snippets, pom xml,
and assertions) unchanged or transliterated where appropriate, localize prose,
section titles, bullets and best-practice notes to natural Japanese, and update
the top-level locale indication (ja-JP) in the document if present so the
document fully matches the Japanese docs folder.
In `@docs/ja-JP/skills/quarkus-verification/SKILL.md`:
- Around line 11-481: The document is missing the required top-level "How It
Works" and "Examples" sections; add a "How It Works" section that summarizes the
phased pipeline (Phase 1–10) and nests the detailed phases (Build, Static
Analysis, Tests+Coverage, Security Scanning, Native Compilation, etc.) under it,
and add an "Examples" section that includes representative usage snippets (e.g.,
the Unit Test, Integration Test, API Test, k6 load test and CI GitHub Actions
examples already present) with brief captions; ensure the new headings "How It
Works" and "Examples" follow the existing "When to Activate" heading order and
conform to the skills/*.md canonical format.
In `@docs/tr/skills/quarkus-patterns/SKILL.md`:
- Around line 7-754: The document SKILL.md under the Turkish docs is still in
English; translate all human-readable text (titles, headings, paragraphs, lists,
Best Practices sections, YAML comments, and descriptive sentences) into Turkish
while preserving all code blocks exactly as-is (class names, annotations like
`@RequiredArgsConstructor`, `@ApplicationScoped`, method names like processFile,
uploadOriginalFile, and configuration keys). Ensure terminology consistency
(e.g., "Service", "EventService", "BusinessRulesPublisher", "CompletableFuture")
and keep inline code identifiers and config keys unchanged so code samples
remain valid.
In `@docs/tr/skills/quarkus-security/SKILL.md`:
- Around line 11-453: The document uses "When to Activate" but must follow the
skill schema; rename the "When to Activate" heading to "When to Use", insert a
new "How It Works" section after it that summarizes the security concepts
(authentication, authorization, validation, CORS, secrets, rate limiting,
headers, audit, scanning) and explains Quarkus mechanisms (SecurityIdentity,
JWT/ODIC, ContainerRequestFilter/ResponseFilter, Bean Validation, Panache
parameterization), and add an "Examples" section that groups the existing code
samples (JWT Authentication, Custom Authentication Filter, Role-Based Access
Control, Programmatic Security, Bean Validation, Custom Validators, SQL
Injection examples, Password Hashing, CORS config, Vault, Rate Limiting,
Security Headers, Audit Logging) under that heading; keep existing code blocks
intact but move them into the "Examples" section so the file matches the
required "When to Use", "How It Works", and "Examples" schema.
In `@docs/tr/skills/quarkus-tdd/SKILL.md`:
- Around line 7-908: The page under "Quarkus TDD Workflow" is written in English
but lives in the Turkish docs, so translate the entire SKILL.md content into
Turkish (including the main heading "Quarkus TDD Workflow", section titles like
"When to Use", "Workflow", "Unit Tests with `@Nested` Organization", all code
block comments and descriptive text), preserving code examples, class names
(e.g., As2ProcessingServiceTest, BusinessRulesRouteTest, EventServiceTest),
annotations (e.g., `@QuarkusTest`, `@ExtendWith`(MockitoExtension.class)), and
technical identifiers (FlowProfile, ValidationFlowConfig, As2Constants)
unchanged; ensure translations keep meaning and tone consistent with Turkish
docs and update any inline display strings (e.g., `@DisplayName` texts) to Turkish
where they are part of documentation (not code behavior) while leaving code
literals and JSON/XML snippets intact.
In `@docs/tr/skills/quarkus-verification/SKILL.md`:
- Around line 7-481: The SKILL.md under docs/tr is still in English (starts with
the "Quarkus Verification Loop" heading); translate the entire document into
Turkish and replace the English content so the Turkish docs tree contains
localized text; ensure headings like "Quarkus Verification Loop", phase names
(Phase 1: Build, Phase 2: Static Analysis, Phase 3: Tests + Coverage, etc.),
checklists, and the automated verification script are fully translated and
culturally adapted while preserving technical accuracy and command examples
(Maven/Gradle commands and code blocks) and keep the file name SKILL.md
unchanged.
In `@docs/zh-CN/skills/prompt-optimizer/SKILL.md`:
- Line 120: The table row mapping for "Quarkus / Java" currently uses the
generic reviewer token "code-reviewer"; update that mapping to use the
language-specific agent "java-reviewer" instead (replace the trailing
"code-reviewer" entry with "java-reviewer") so Quarkus/Java skill lookups route
to the Java/Spring Boot reviewer.
In `@docs/zh-CN/skills/quarkus-patterns/SKILL.md`:
- Around line 7-754: The zh-CN skill file currently contains English content
(starting with the "Quarkus Development Patterns" header and sections like "When
to Activate", "Key Patterns", and code examples such as As2ProcessingService,
EventService, BusinessRulesPublisher), so translate the entire markdown body
into Simplified Chinese while preserving all code blocks, class/method names
(`@RequiredArgsConstructor`, `@Slf4j`, As2ProcessingService, EventService,
BusinessRulesPublisher, FileStorageService, DocumentResource, etc.),
configuration snippets, YAML and XML examples, and technical identifiers exactly
as-is; ensure headings (e.g., "When to Activate", "Key Patterns", "Best
Practices") and inline annotations remain semantically identical but localized,
and update any localized front-matter or summary lines to indicate zh-CN content
is present.
- Around line 11-754: The document uses the wrong section headings for zh-CN
skill docs—replace the top-level "When to Activate" heading with the required
"When to use", add an explicit "How it works" section (move explanatory pattern
descriptions such as "Service Layer with Multiple Dependencies (Lombok)",
"Custom Logging Context Pattern", "Event Service Pattern" into it) and create a
distinct "Examples" section containing runnable snippets like the
As2ProcessingService, BusinessRulesRoute, FileMonitoringRoute, and YAML/config
examples; ensure headings are exactly "When to use", "How it works", and
"Examples" so the file conforms to the docs/zh-CN/skills template.
In `@docs/zh-CN/skills/quarkus-security/SKILL.md`:
- Around line 11-453: The headings don't match the required zh-CN skill
template; replace "When to Activate" with "When to use", add a new "How it
works" section (summarize authentication, authorization, validation, CORS,
secrets, rate-limiting concepts currently scattered across the file), and
consolidate all code samples under a new "Examples" section (group JWT
Authentication, Custom Authentication Filter, Role-Based Access Control, Bean
Validation, SQL Injection examples, CORS, Vault, rate limiting, security
headers, and audit logging) so the document contains explicit "When to use",
"How it works", and "Examples" sections while preserving the existing code
snippets and guidance (update headings for sections like JWT, Authorization,
Input Validation accordingly).
In `@docs/zh-CN/skills/quarkus-verification/SKILL.md`:
- Around line 7-481: The file SKILL.md under the zh-CN docs contains English
content ("Quarkus Verification Loop" and all phases/headings like "Phase 1:
Build", "Phase 3: Tests + Coverage", the verification checklist, automation
scripts and CI examples); translate the entire document into Chinese while
leaving code blocks, commands, filenames and technical identifiers
(mvn/./gradlew commands, Java snippets, YAML blocks, curl/k6 examples, and
checklist items) exactly as-is, preserve all headings and structure, and ensure
the translated title and section headings are appropriate for zh-CN readers.
In `@skills/quarkus-patterns/SKILL.md`:
- Around line 11-754: The document currently uses "When to Activate" and other
informal sections; update SKILL.md to follow the repository skill format by
adding top-level Markdown headings "When to Use", "How It Works", and "Examples"
(replace or map the existing "When to Activate" content into "When to Use"),
create a "How It Works" section that summarizes key patterns (reference symbols
like As2ProcessingService, FileStorageService.uploadOriginalFile,
BusinessRulesPublisher.publishAsync, DocumentResource,
EventService.createSuccessEvent/createErrorEvent, and
DocumentRepository.findByStatus), and move the concrete code samples and
route/config snippets under an "Examples" section; ensure headings are top-level
(e.g., "# When to Use") and keep existing content but reorganize headings and
brief lead sentences to satisfy the skill format requirement.
In `@skills/quarkus-security/SKILL.md`:
- Around line 11-453: Rename the top-level "When to Activate" heading to "When
to Use", add a new top-level "How It Works" section summarizing key concepts
(authentication flows, JWT/OIDC basics, SecurityIdentity/@RolesAllowed behavior,
and Panache query safety) and create a top-level "Examples" section that
contains the concrete code samples (move "JWT Authentication", "Custom
Authentication Filter", "Authorization", "Input Validation", "SQL Injection
Prevention", "Password Hashing", "CORS Configuration", "Secrets Management",
"Rate Limiting", "Security Headers", and "Audit Logging" code blocks under that
Examples heading); ensure existing internal headings like "Authentication", "JWT
Authentication", "Custom Authentication Filter", "Authorization", and
"Role-Based Access Control" remain as subsections under the new top-level
sections and update the README/skill header to follow the required three-section
format.
In `@skills/quarkus-verification/SKILL.md`:
- Around line 11-481: The document uses non-compliant section headings ("When to
Activate" and the Phase-based flow) instead of the required contract sections;
update SKILL.md to include explicit top-level headings "When to Use", "How It
Works", and "Examples" by renaming and reorganizing content: move the current
"When to Activate" content under "When to Use", consolidate Phases 1–10 and the
Verification Checklist and CI snippets into a clear "How It Works" section
(keeping Phase titles like "Phase 1: Build" etc. as subsections), and place
sample commands/tests/REST Assured snippets and the "Automated Verification
Script" under "Examples"; ensure the new headings are exact strings "When to
Use", "How It Works", and "Examples" so the skill validator recognizes them.
---
Outside diff comments:
In `@docs/zh-CN/skills/configure-ecc/SKILL.md`:
- Around line 108-132: The category header "**类别:框架与语言(21项技能)**" is out of date
after adding four Quarkus entries; update that header to read
"**类别:框架与语言(25项技能)**" so the count matches the listed skills (references: the
header text string and the Quarkus entries `quarkus-patterns`,
`quarkus-security`, `quarkus-tdd`, `quarkus-verification`).
In `@skills/java-coding-standards/SKILL.md`:
- Around line 19-57: Rename the "When to Activate" heading to "When to Use", add
a new "How It Works" section that succinctly explains the core principles
(clarity over cleverness, immutable-by-default, fail-fast, naming/package
consistency, Quarkus note about build-time processing), and consolidate all code
snippets and usage bullets into a new "Examples" section; update heading order
to "When to Use", "How It Works", "Examples" and ensure existing naming and
immutability bullets move into "How It Works" while the Java code blocks and
framework-specific resource/controller notes go under "Examples" so the file
matches the canonical skill template.
---
Minor comments:
In `@docs/ja-JP/skills/configure-ecc/SKILL.md`:
- Around line 99-102: Add a cross-reference check in Step 4c to ensure that
selecting `quarkus-tdd` also requires `quarkus-patterns` like the other
framework checks; update the Step 4c logic that currently validates framework
selections to include a rule that when `quarkus-tdd` is present it
enforces/auto-adds `quarkus-patterns` (or emits the same error message used for
similar pairs), referencing the `quarkus-tdd` and `quarkus-patterns` identifiers
so the check is consistent with existing pattern checks.
- Line 68: Update the incorrect skill count in the sentence
"31個のスキルが4つのカテゴリに分類されています。`multiSelect: true` で `AskUserQuestion` を使用します:" to
"32個のスキルが4つのカテゴリに分類されています。" so the total (20 + 3 + 8 + 1) matches the stated
count; edit the phrase in SKILL.md replacing "31" with "32".
In `@docs/ja-JP/skills/quarkus-tdd/SKILL.md`:
- Line 876: Update the sentence "Test error handling routes separately" to
hyphenate the compound modifier; replace it with "Test error-handling routes
separately" wherever that exact string appears (e.g., the line containing that
sentence in SKILL.md) to improve clarity.
In `@docs/tr/skills/quarkus-patterns/SKILL.md`:
- Around line 11-24: The file uses a "When to Activate" heading and lacks the
required canonical sections; rename or duplicate the "When to Activate" section
to "When to Use", and add two new top-level sections "How It Works" and
"Examples" under the existing content (move or expand the "Service Layer with
Multiple Dependencies (Lombok)" part into the "Examples" section); ensure the
new "How It Works" explains core concepts (resource→service→repository flow,
event-driven hooks, Hibernate Panache behavior, GraalVM notes) and that each
example has a short title and concrete steps or snippets pointers to illustrate
the skills.
In `@docs/tr/skills/quarkus-tdd/SKILL.md`:
- Around line 22-29: Replace the incorrect "## Workflow" heading with the
required "## How It Works" heading and ensure the document includes the other
mandatory top-level sections titled exactly "When to Use" and "Examples"; update
the content under the current workflow list to live under "How It Works", add a
short "When to Use" section that explains the intended audience or scenario, and
add an "Examples" section with any sample usages or links so the file conforms
to the docs/tr/skills/**/*.md guideline requiring 'When to Use', 'How It Works',
and 'Examples'.
In `@docs/tr/skills/quarkus-verification/SKILL.md`:
- Around line 11-20: Rename the top section "When to Activate" to the canonical
header "When to Use", then add a new "How It Works" section that describes the
Phase structure (e.g., "Phase 1: Build" etc.) and details the step-by-step
verification flow (build → lint → test → security scan → native compilation and
thresholds like 80% coverage), and finally add an "Examples" section with at
least one concrete example use case (e.g., PR after dependency upgrade,
pre-production staging release) showing commands/expected outcomes; update
headings "Phase 1: Build" to live under "How It Works" and ensure all three
required headers ("When to Use", "How It Works", "Examples") appear in SKILL.md.
In `@docs/zh-CN/skills/quarkus-tdd/SKILL.md`:
- Around line 11-29: The document uses non-canonical headings; rename and/or add
the required zh-CN skill sections exactly as "When to use", "How it works", and
"Examples" (case-sensitive) in docs/zh-CN/skills/quarkus-tdd/SKILL.md: change
"When to Use" to "When to use", convert the "Workflow" block into "How it works"
(preserving steps 1–4), and add an "Examples" section (even if stubbed) after
"How it works"; ensure each section is a Markdown heading (##) so the file
conforms to docs/zh-CN/skills/**/*.md guidelines.
In `@docs/zh-CN/skills/quarkus-verification/SKILL.md`:
- Around line 11-20: Replace the current "When to Activate" section with a
properly named "When to use" heading and split the content into three required
sections: add a "How it works" section that briefly explains the verification
steps (build → lint → test → security scan → native compilation and when each is
run), and an "Examples" section that shows 1–2 concrete usage scenarios (e.g.,
pre-PR after refactor, pre-deploy staging verification), keeping the existing
"Phase 1: Build" content under the workflow or "How it works" as appropriate so
the file contains clearly labeled "When to use", "How it works", and "Examples"
sections per the docs/zh-CN/skills/*.md format.
In `@skills/quarkus-tdd/SKILL.md`:
- Line 876: Replace the unhyphenated phrase "error handling routes" with the
hyphenated compound "error-handling routes" in the SKILL.md content so the
modifier is correctly formed ( locate the occurrence of the string "error
handling routes" and change it to "error-handling routes" ).
---
Nitpick comments:
In `@docs/ja-JP/skills/quarkus-tdd/SKILL.md`:
- Around line 531-534: The test currently stubs executorService.submit(...) but
CompletableFuture.supplyAsync(..., executorService) uses the Executor.execute
path; update the stub to intercept executorService.execute(Runnable) (or use
doAnswer on execute) and run the provided Runnable synchronously (invoking
runnable.run()) so the async work is executed during the test; locate the stub
near the existing thenAnswer for executorService.submit and replace it with a
stub for executorService.execute that triggers the Runnable, ensuring the same
behavior is applied at the other occurrence (lines around the second instance).
In `@docs/tr/agents/java-reviewer.md`:
- Around line 92-94: Update the agent's top-level scope text to explicitly
mention both Spring Boot and Quarkus (not only Spring Boot) so the description
aligns with the added references; modify the overview/intro paragraph that
currently emphasizes Spring Boot to include Quarkus as a peer framework and
ensure the skill links `skill: springboot-patterns` and `skill:
quarkus-patterns` are referenced in that scope text so readers see the
dual-framework intent.
In `@docs/zh-CN/agents/java-build-resolver.md`:
- Around line 154-156: Update the agent description to explicitly mention
Quarkus build failures so the scope matches the new references; specifically,
where the doc references the SPRING and QUARKUS patterns (the bullets with
"**[SPRING]**: `skill: springboot-patterns`" and "**[QUARKUS]**: `skill:
quarkus-patterns`"), add a clear phrase in the agent description that Quarkus
build failures are handled/diagnosed (e.g., "includes Quarkus build failures and
troubleshooting"), and ensure any scope or summary lines use parallel wording
for both Spring and Quarkus so the references and scope text are aligned.
In `@docs/zh-CN/agents/java-reviewer.md`:
- Around line 105-107: The intro/frontmatter scope text is Spring Boot–centric
and should explicitly name Quarkus as first-class support; update the document's
intro language to mention both Spring Boot and Quarkus equally and ensure the
example bullet list references both skills using the existing tokens `skill:
springboot-patterns` and `skill: quarkus-patterns` (the current SPRING/QUARKUS
bullets at Lines 105–107). Modify the scope sentence(s) in the frontmatter/intro
so they read like "Supports Spring Boot and Quarkus patterns" (or similar), and
verify any downstream references/search labels in the doc match that expanded
scope.
In `@docs/zh-CN/skills/quarkus-patterns/SKILL.md`:
- Around line 1-5: The file name SKILL.md violates the repository naming
convention (use lowercase with hyphens); rename
docs/zh-CN/skills/quarkus-patterns/SKILL.md to skill.md or quarkus-patterns.md
(lowercase-with-hyphens) and update any references/imports to SKILL.md
accordingly; apply the same rename pattern to any new Quarkus skill files so all
Markdown files match the "**/*.{md,js,ts,json}" lowercase-with-hyphens rule.
In `@skills/quarkus-tdd/SKILL.md`:
- Around line 22-908: The SKILL.md is missing explicit "How It Works" and
"Examples" sections required by the repository skill format; add two new
top-level sections "## How It Works" and "## Examples" (alongside the existing
"When to Use" / "Workflow" material) and populate them with concise content:
under "How It Works" describe the TDD flow and how the provided tests (e.g.,
As2ProcessingServiceTest, BusinessRulesRouteTest, FileStorageServiceTest)
exercise validation, async upload, Camel routes and event creation; under
"Examples" include short, runnable examples/links to the unit test snippets
shown (referencing As2ProcessingServiceTest, BusinessRulesRouteTest,
EventServiceTest) demonstrating typical usages. Ensure headings match exact
strings "How It Works" and "Examples" so the skill validator recognizes them.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e91b43b6-b2df-4bb2-8a5d-5dcaa9ff2db8
📒 Files selected for processing (43)
.opencode/prompts/agents/java-build-resolver.txt.opencode/prompts/agents/java-reviewer.txtREADME.mdREADME.zh-CN.mdagent.yamlagents/java-build-resolver.mdagents/java-reviewer.mddocs/ja-JP/README.mddocs/ja-JP/skills/README.mddocs/ja-JP/skills/configure-ecc/SKILL.mddocs/ja-JP/skills/quarkus-patterns/SKILL.mddocs/ja-JP/skills/quarkus-security/SKILL.mddocs/ja-JP/skills/quarkus-tdd/SKILL.mddocs/ja-JP/skills/quarkus-verification/SKILL.mddocs/tr/agents/java-build-resolver.mddocs/tr/agents/java-reviewer.mddocs/tr/skills/quarkus-patterns/SKILL.mddocs/tr/skills/quarkus-security/SKILL.mddocs/tr/skills/quarkus-tdd/SKILL.mddocs/tr/skills/quarkus-verification/SKILL.mddocs/zh-CN/README.mddocs/zh-CN/agents/java-build-resolver.mddocs/zh-CN/agents/java-reviewer.mddocs/zh-CN/rules/java/patterns.mddocs/zh-CN/rules/java/security.mddocs/zh-CN/rules/java/testing.mddocs/zh-CN/skills/configure-ecc/SKILL.mddocs/zh-CN/skills/prompt-optimizer/SKILL.mddocs/zh-CN/skills/quarkus-patterns/SKILL.mddocs/zh-CN/skills/quarkus-security/SKILL.mddocs/zh-CN/skills/quarkus-tdd/SKILL.mddocs/zh-CN/skills/quarkus-verification/SKILL.mdmanifests/install-modules.jsonrules/java/patterns.mdrules/java/security.mdrules/java/testing.mdskills/configure-ecc/SKILL.mdskills/java-coding-standards/SKILL.mdskills/prompt-optimizer/SKILL.mdskills/quarkus-patterns/SKILL.mdskills/quarkus-security/SKILL.mdskills/quarkus-tdd/SKILL.mdskills/quarkus-verification/SKILL.md
There was a problem hiding this comment.
23 issues found across 43 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/ja-JP/skills/quarkus-patterns/SKILL.md">
<violation number="1" location="docs/ja-JP/skills/quarkus-patterns/SKILL.md:83">
P2: The documentation snippet uses `originalFileName` without defining it, so the example will not compile when copied.</violation>
<violation number="2" location="docs/ja-JP/skills/quarkus-patterns/SKILL.md:183">
P2: EventService example calls `objectMapper.writeValueAsString(...)` without declaring or injecting an `ObjectMapper`, so the sample code will not compile as written.</violation>
</file>
<file name="docs/ja-JP/skills/quarkus-tdd/SKILL.md">
<violation number="1" location="docs/ja-JP/skills/quarkus-tdd/SKILL.md:375">
P2: The doc example mocks `eventService.validate(...)`, but the documented `EventService` API only exposes `createSuccessEvent`/`createErrorEvent`. This looks like a service/method mismatch in the example that would not compile for readers.</violation>
</file>
<file name="agents/java-build-resolver.md">
<violation number="1" location="agents/java-build-resolver.md:175">
P2: Quarkus-specific commands are Maven-only, so the framework-specific workflow breaks for Gradle-based Quarkus projects despite the agent claiming Maven/Gradle support.</violation>
<violation number="2" location="agents/java-build-resolver.md:226">
P3: Output format can’t represent the “both frameworks present” branch described above; add a BOTH/multi value or update the schema to avoid inconsistent reporting.</violation>
</file>
<file name=".opencode/prompts/agents/java-reviewer.txt">
<violation number="1" location=".opencode/prompts/agents/java-reviewer.txt:99">
P2: The prompt remains explicitly Spring Boot–centric, but this change advertises Quarkus support. This mismatch can route Quarkus reviews through Spring-specific rules, leading to false positives/negatives. Either update the prompt to cover Quarkus behavior or keep it Spring-only.</violation>
</file>
<file name="docs/tr/skills/quarkus-security/SKILL.md">
<violation number="1" location="docs/tr/skills/quarkus-security/SKILL.md:339">
P2: The rate-limiting example stores per-client limiters in an unbounded map keyed by `X-Forwarded-For`. Because this header is client-controlled unless sanitized by a trusted proxy, attackers can bypass limits by rotating the value and can also force unbounded map growth (memory/DoS). Consider using a trusted identifier (proxy-provided remote address/user ID/API key) and a cache with eviction/TTL instead of an unbounded map.</violation>
<violation number="2" location="docs/tr/skills/quarkus-security/SKILL.md:385">
P2: Security best-practices doc shows a CSP example that allows `'unsafe-inline'`, which weakens XSS protections and can normalize insecure defaults when copied into production.</violation>
</file>
<file name="docs/tr/skills/quarkus-verification/SKILL.md">
<violation number="1" location="docs/tr/skills/quarkus-verification/SKILL.md:90">
P2: The unit test example stubs `persist(...)` with `thenReturn(...)`, but `persist` is a void method in PanacheRepository. This snippet won’t compile; use `doNothing()`/`doAnswer()` for void methods instead.</violation>
<violation number="2" location="docs/tr/skills/quarkus-verification/SKILL.md:421">
P2: The automated verification script only runs phases 1–5 but prints “All Phases Complete,” which misrepresents that phases 6–10 were executed. This can mislead readers into skipping later verification steps.</violation>
</file>
<file name="docs/zh-CN/skills/quarkus-tdd/SKILL.md">
<violation number="1" location="docs/zh-CN/skills/quarkus-tdd/SKILL.md:861">
P2: Test guidance is internally inconsistent: it mandates `assertDoesNotThrow`/`assertThrows` (JUnit) but later says to always use AssertJ instead of JUnit assertions.</violation>
</file>
<file name="skills/quarkus-security/SKILL.md">
<violation number="1" location="skills/quarkus-security/SKILL.md:358">
P2: Rate limit example uses `X-Forwarded-For` directly as the limiter key. If the header is absent, `clientId` is null and `computeIfAbsent` on `ConcurrentHashMap` throws. Even when present, the header is attacker-controlled unless validated, enabling spoofed values to bypass throttling and grow the map unbounded. Add a safe fallback and only trust `X-Forwarded-For` when validated (or use an authenticated user/API key).</violation>
<violation number="2" location="skills/quarkus-security/SKILL.md:385">
P2: The CSP example in this security best-practices doc allows `unsafe-inline` for scripts and styles, which weakens XSS protection and reads like a recommended baseline. Consider removing `unsafe-inline` and using nonces/hashes when inline code is unavoidable.</violation>
</file>
<file name="skills/prompt-optimizer/SKILL.md">
<violation number="1" location="skills/prompt-optimizer/SKILL.md:138">
P2: Phase 3 now maps Quarkus-specific skills, but Phase 0 detection still only maps build.gradle/pom.xml to Java/Kotlin/Spring Boot. Without a Quarkus detection heuristic, the new Quarkus mapping is effectively unreachable via auto-detection.</violation>
</file>
<file name="docs/ja-JP/skills/quarkus-security/SKILL.md">
<violation number="1" location="docs/ja-JP/skills/quarkus-security/SKILL.md:358">
P2: Rate limiting example uses the raw X-Forwarded-For header as the map key without validation or fallback. If the header is missing, computeIfAbsent will throw on a null key, and if it’s spoofed the limit can be bypassed and the map can grow unbounded. Add validation and a safe fallback (or use a trusted source for client identity).</violation>
<violation number="2" location="docs/ja-JP/skills/quarkus-security/SKILL.md:385">
P2: The security guidance recommends CSP with `unsafe-inline` for scripts/styles, which weakens XSS protection. For a best-practices doc, use a safer default (e.g., omit `unsafe-inline` or use nonces/hashes).</violation>
</file>
<file name="docs/tr/skills/quarkus-patterns/SKILL.md">
<violation number="1" location="docs/tr/skills/quarkus-patterns/SKILL.md:83">
P2: The documentation example calls `createDocumentAndJobEntities` with `originalFileName`, but `originalFileName` is never defined in the snippet, so the sample code would not compile as written.</violation>
<violation number="2" location="docs/tr/skills/quarkus-patterns/SKILL.md:183">
P2: EventService example uses `objectMapper` without declaring or injecting it, so the snippet is incomplete/non-compilable.</violation>
</file>
<file name="docs/zh-CN/skills/quarkus-security/SKILL.md">
<violation number="1" location="docs/zh-CN/skills/quarkus-security/SKILL.md:358">
P2: Rate limiter key uses raw `X-Forwarded-For` without validation or fallback, which can return null (causing NPE in `computeIfAbsent`) and is client-controlled, enabling bypass and unbounded limiter growth in this security example.</violation>
<violation number="2" location="docs/zh-CN/skills/quarkus-security/SKILL.md:385">
P2: The CSP example in the security best-practices section allows 'unsafe-inline' for scripts/styles, which weakens XSS mitigation and can mislead readers into deploying an insecure policy.</violation>
</file>
<file name="docs/tr/skills/quarkus-tdd/SKILL.md">
<violation number="1" location="docs/tr/skills/quarkus-tdd/SKILL.md:861">
P2: The doc mandates "Always use AssertJ instead of JUnit assertions" but elsewhere in the same file recommends and demonstrates JUnit assertions like assertDoesNotThrow/assertThrows. This creates conflicting guidance and inconsistent test standards.</violation>
</file>
<file name="skills/quarkus-patterns/SKILL.md">
<violation number="1" location="skills/quarkus-patterns/SKILL.md:83">
P2: The primary service-layer example uses `originalFileName` without declaring it, so the documented pattern doesn’t compile as written.</violation>
</file>
<file name="docs/zh-CN/skills/prompt-optimizer/SKILL.md">
<violation number="1" location="docs/zh-CN/skills/prompt-optimizer/SKILL.md:120">
P2: Keep this Quarkus/Java mapping consistent with the canonical prompt-optimizer table. Diverging reviewer/skill mappings by locale can route the same tech stack to different agents depending on language.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
|
Tip: Greploops — Automatically fix all review issues by running Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal. |
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
docs/ja-JP/skills/quarkus-verification/SKILL.md (1)
13-483:⚠️ Potential issue | 🟠 MajorAdd explicit
How It WorksandExamplestop-level sections.The document is detailed, but it still misses the canonical section headings required for skill docs. Please add explicit
## How It Worksand## Examplessections (you can nest/reuse the existing phase flow and sample snippets under them).Based on learnings:
skills/**/*.mdmust have clear sections for “When to Use”, “How It Works”, and “Examples”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ja-JP/skills/quarkus-verification/SKILL.md` around lines 13 - 483, Rename or add canonical top-level headings in SKILL.md so the doc contains "## When to Use", "## How It Works", and "## Examples": convert the existing "## When to Activate" content into "## When to Use" (or add a new "When to Use" section with the same bullets), create a new "## How It Works" section that summarizes the phase flow (Phase 1–10) and the verification checklist, and create a new "## Examples" section that collects the code snippets (build commands, test examples, REST Assured, native troubleshooting, K6 load-test, CI YAML) so examples are clearly grouped; keep the original content but move/duplicate bits under these headings and ensure headings are exact "How It Works" and "Examples" for the skills docs validator.docs/tr/skills/quarkus-verification/SKILL.md (1)
13-13: 🛠️ Refactor suggestion | 🟠 MajorRestructure to required skill schema.
The heading uses "When to Activate" but should be "When to Use". Additionally, the document lacks explicit "How It Works" and "Examples" sections required by the skill format. As per coding guidelines, skills should be formatted as Markdown files with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'.
📋 Suggested schema outline
-## When to Activate +## When to Use - Before opening a pull request for a Quarkus service - After major refactoring or dependency upgrades ... +## How It Works + +The verification loop runs through 10 phases: build validation, static analysis, test execution with coverage enforcement, security scanning, native compilation testing, performance validation, health checks, container image building, configuration validation, and documentation review. Each phase must pass before proceeding. + +## Examples + ## Phase 1: Build ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/skills/quarkus-verification/SKILL.md` at line 13, Rename the header "## When to Activate" to "## When to Use" in SKILL.md, then add two required top-level sections "## How It Works" and "## Examples" (use H2 headings) after the "When to Use" section; populate "How It Works" with a short paragraph describing the verification flow and key steps, and add at least one concrete example (code block or usage snippet) under "Examples" to demonstrate expected input/output or commands; ensure headings match the skill schema and update any local TOC or links if present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/tr/skills/quarkus-tdd/SKILL.md`:
- Around line 13-31: Rename the "Workflow" heading to "How It Works" and add a
new "Examples" heading before the existing code samples; specifically update the
section title string "Workflow" to "How It Works" and insert a top-level "##
Examples" heading immediately above the code blocks or sample snippets that
follow, ensuring the doc now contains three explicit sections: "When to Use",
"How It Works", and "Examples".
In `@docs/zh-CN/skills/quarkus-tdd/SKILL.md`:
- Around line 863-866: The guidance currently contradicts itself by mandating
"Always use AssertJ (`assertThat`)" while earlier examples use JUnit's
`assertThrows`/`assertDoesNotThrow`; update the text to recommend preferring
AssertJ's fluent API (`assertThat`, `assertThatThrownBy`) for readability but
allow JUnit assertions (`assertThrows`, `assertDoesNotThrow`) in specific cases
where AssertJ is less clear or idiomatic; mention using `assertThatThrownBy` as
the AssertJ equivalent to `assertThrows` and keep other AssertJ examples
(`extracting()`, `filteredOn()`, `containsExactly()`) as preferred patterns.
- Line 878: Update the phrase "Test error handling routes separately" to use the
correct compound-modifier hyphenation by replacing it with "Test error-handling
routes separately" so the documentation uses "error-handling routes"
consistently.
- Around line 13-31: The doc currently has a "When to Use" section but is
missing the required "How it works" and "Examples" headings and the "When to
Use" heading uses different casing; update the headings so the file contains
three top-level sections exactly named "When to use", "How it works", and
"Examples" (rename "When to Use" → "When to use"), move the workflow and any
implementation details under "How it works" (e.g., the numbered workflow and
notes about `@Nested` organization), and put concrete usage snippets or test
examples under "Examples" (create new sections titled "How it works" and
"Examples" and relocate content accordingly while keeping the "Unit Tests with
`@Nested` Organization" text under the appropriate section).
---
Duplicate comments:
In `@docs/ja-JP/skills/quarkus-verification/SKILL.md`:
- Around line 13-483: Rename or add canonical top-level headings in SKILL.md so
the doc contains "## When to Use", "## How It Works", and "## Examples": convert
the existing "## When to Activate" content into "## When to Use" (or add a new
"When to Use" section with the same bullets), create a new "## How It Works"
section that summarizes the phase flow (Phase 1–10) and the verification
checklist, and create a new "## Examples" section that collects the code
snippets (build commands, test examples, REST Assured, native troubleshooting,
K6 load-test, CI YAML) so examples are clearly grouped; keep the original
content but move/duplicate bits under these headings and ensure headings are
exact "How It Works" and "Examples" for the skills docs validator.
In `@docs/tr/skills/quarkus-verification/SKILL.md`:
- Line 13: Rename the header "## When to Activate" to "## When to Use" in
SKILL.md, then add two required top-level sections "## How It Works" and "##
Examples" (use H2 headings) after the "When to Use" section; populate "How It
Works" with a short paragraph describing the verification flow and key steps,
and add at least one concrete example (code block or usage snippet) under
"Examples" to demonstrate expected input/output or commands; ensure headings
match the skill schema and update any local TOC or links if present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 83f4c29b-f606-4249-9da6-edc43134ef8a
📒 Files selected for processing (12)
docs/ja-JP/skills/quarkus-patterns/SKILL.mddocs/ja-JP/skills/quarkus-security/SKILL.mddocs/ja-JP/skills/quarkus-tdd/SKILL.mddocs/ja-JP/skills/quarkus-verification/SKILL.mddocs/tr/skills/quarkus-patterns/SKILL.mddocs/tr/skills/quarkus-security/SKILL.mddocs/tr/skills/quarkus-tdd/SKILL.mddocs/tr/skills/quarkus-verification/SKILL.mddocs/zh-CN/skills/quarkus-patterns/SKILL.mddocs/zh-CN/skills/quarkus-security/SKILL.mddocs/zh-CN/skills/quarkus-tdd/SKILL.mddocs/zh-CN/skills/quarkus-verification/SKILL.md
✅ Files skipped from review due to trivial changes (5)
- docs/ja-JP/skills/quarkus-security/SKILL.md
- docs/zh-CN/skills/quarkus-verification/SKILL.md
- docs/zh-CN/skills/quarkus-patterns/SKILL.md
- docs/zh-CN/skills/quarkus-security/SKILL.md
- docs/ja-JP/skills/quarkus-patterns/SKILL.md
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
docs/tr/skills/quarkus-tdd/SKILL.md (1)
22-28: 🛠️ Refactor suggestion | 🟠 MajorRename section to "Nasıl Çalışır" and add explicit "Örnekler" heading.
The current "İş Akışı" (Workflow) heading should be "Nasıl Çalışır" (How It Works), and all code examples below should be grouped under a new "## Örnekler" section heading to match the required skill format.
As per coding guidelines: "docs/tr/skills/**/*.md: Skills should be formatted as Markdown files with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'."
📝 Suggested restructuring
-## İş Akışı +## Nasıl Çalışır 1. Önce testleri yazın (başarısız olmalılar) 2. Geçmek için minimal kod uygulayın 3. Testleri yeşil tutarken refactor edin 4. JaCoCo ile kapsamı zorlayın (%80+ hedef) +## Örnekler + ## `@Nested` Organizasyonlu Unit Testler🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/skills/quarkus-tdd/SKILL.md` around lines 22 - 28, Rename the "İş Akışı" section heading to "Nasıl Çalışır" and then create a new "## Örnekler" section to group all code examples that follow; specifically update the existing heading text "İş Akışı" to "Nasıl Çalışır" and insert a new "## Örnekler" heading immediately before the block(s) containing code examples so the file follows the required "When to Use / How It Works / Examples" format.docs/tr/skills/quarkus-security/SKILL.md (1)
11-21: 🛠️ Refactor suggestion | 🟠 MajorRestructure to required skill section schema.
The heading "Ne Zaman Aktif Edilir" should be "Ne Zaman Kullanılır" (When to Use). Additionally, the document must include explicit "## Nasıl Çalışır" (How It Works) and "## Örnekler" (Examples) sections to conform to repository skill format standards.
As per coding guidelines: "docs/tr/skills/**/*.md: Skills should be formatted as Markdown files with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'."
📋 Suggested restructuring outline
-## Ne Zaman Aktif Edilir +## Ne Zaman Kullanılır +## Nasıl Çalışır + +(Summary of Quarkus security mechanisms: JWT/OIDC, SecurityIdentity, +Bean Validation, RBAC annotations, etc.) + +## Örnekler + ## Kimlik DoğrulamaMove all existing code blocks (JWT Authentication, RBAC, Input Validation, etc.) under the "Örnekler" section.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/skills/quarkus-security/SKILL.md` around lines 11 - 21, Rename the heading "Ne Zaman Aktif Edilir" to "Ne Zaman Kullanılır" and add two new top-level sections "## Nasıl Çalışır" and "## Örnekler"; move the current bullet points (JWT Authentication, `@RolesAllowed`/SecurityIdentity, Bean Validation/özel doğrulayıcılar, CORS/security headers, secret management, rate limiting/brute-force protection, dependency CVE scanning, MicroProfile/SmallRye JWT) under the new "## Örnekler" section as appropriate example subsections or code blocks, and add a concise explanatory paragraph in "## Nasıl Çalışır" describing the high-level mechanism of Quarkus security (authentication, authorization flow, token validation, and integration points). Ensure headings use exact Turkish labels "Ne Zaman Kullanılır", "Nasıl Çalışır", and "Örnekler" to match repository schema.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/tr/skills/quarkus-verification/SKILL.md`:
- Around line 11-19: Rename the header "Ne Zaman Aktif Edilir" to "Ne Zaman
Kullanılır", add two new section headers "## Nasıl Çalışır" and "## Örnekler",
and move the existing bulleted checklist (the ten phases/scripts/checks
currently under the original header) under the new "## Örnekler" section; ensure
the file follows the required skill format with three top-level sections: "Ne
Zaman Kullanılır", "Nasıl Çalışır", and "Örnekler" and keep the checklist
content intact under "Örnekler".
---
Duplicate comments:
In `@docs/tr/skills/quarkus-security/SKILL.md`:
- Around line 11-21: Rename the heading "Ne Zaman Aktif Edilir" to "Ne Zaman
Kullanılır" and add two new top-level sections "## Nasıl Çalışır" and "##
Örnekler"; move the current bullet points (JWT Authentication,
`@RolesAllowed`/SecurityIdentity, Bean Validation/özel doğrulayıcılar,
CORS/security headers, secret management, rate limiting/brute-force protection,
dependency CVE scanning, MicroProfile/SmallRye JWT) under the new "## Örnekler"
section as appropriate example subsections or code blocks, and add a concise
explanatory paragraph in "## Nasıl Çalışır" describing the high-level mechanism
of Quarkus security (authentication, authorization flow, token validation, and
integration points). Ensure headings use exact Turkish labels "Ne Zaman
Kullanılır", "Nasıl Çalışır", and "Örnekler" to match repository schema.
In `@docs/tr/skills/quarkus-tdd/SKILL.md`:
- Around line 22-28: Rename the "İş Akışı" section heading to "Nasıl Çalışır"
and then create a new "## Örnekler" section to group all code examples that
follow; specifically update the existing heading text "İş Akışı" to "Nasıl
Çalışır" and insert a new "## Örnekler" heading immediately before the block(s)
containing code examples so the file follows the required "When to Use / How It
Works / Examples" format.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f9fa2888-857f-4330-9c23-d648031a79a4
📒 Files selected for processing (12)
docs/ja-JP/skills/quarkus-patterns/SKILL.mddocs/ja-JP/skills/quarkus-security/SKILL.mddocs/ja-JP/skills/quarkus-tdd/SKILL.mddocs/ja-JP/skills/quarkus-verification/SKILL.mddocs/tr/skills/quarkus-patterns/SKILL.mddocs/tr/skills/quarkus-security/SKILL.mddocs/tr/skills/quarkus-tdd/SKILL.mddocs/tr/skills/quarkus-verification/SKILL.mddocs/zh-CN/skills/quarkus-patterns/SKILL.mddocs/zh-CN/skills/quarkus-security/SKILL.mddocs/zh-CN/skills/quarkus-tdd/SKILL.mddocs/zh-CN/skills/quarkus-verification/SKILL.md
✅ Files skipped from review due to trivial changes (7)
- docs/zh-CN/skills/quarkus-verification/SKILL.md
- docs/ja-JP/skills/quarkus-tdd/SKILL.md
- docs/tr/skills/quarkus-patterns/SKILL.md
- docs/zh-CN/skills/quarkus-security/SKILL.md
- docs/ja-JP/skills/quarkus-security/SKILL.md
- docs/ja-JP/skills/quarkus-patterns/SKILL.md
- docs/zh-CN/skills/quarkus-patterns/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/ja-JP/skills/quarkus-verification/SKILL.md
- docs/zh-CN/skills/quarkus-tdd/SKILL.md
There was a problem hiding this comment.
2 issues found across 12 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/zh-CN/skills/quarkus-tdd/SKILL.md">
<violation number="1" location="docs/zh-CN/skills/quarkus-tdd/SKILL.md:199">
P2: The documentation example uses `testPayload` without declaring or initializing it, making the snippet invalid for readers.</violation>
</file>
<file name="docs/ja-JP/skills/quarkus-tdd/SKILL.md">
<violation number="1" location="docs/ja-JP/skills/quarkus-tdd/SKILL.md:199">
P2: `testPayload` is used in the Camel route test example but is never declared or initialized in the snippet, so the documentation example is invalid and won’t compile as written.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
The custom auth filter only rejected invalid tokens but silently passed through requests without an Authorization header, creating a complete auth bypass. Inverted the guard to reject-first: abort immediately when header is absent or malformed, then validate.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Tests assert null-payload and blank-error-message guards but the implementation had none. Added Objects.requireNonNull for payload and blank check for errorMessage. Also added missing objectMapper field to locale copies.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
The route calls .marshal().json() before reaching the mock endpoint, so the body is a JSON String, not a POJO. Removed expectedBodiesReceived with POJO and getBody(BusinessRulesPayload.class), replaced with String body assertion using contains().
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
writeValueAsString throws checked JsonProcessingException which was unhandled, causing a compile error. Wrapped in try/catch, rethrowing as IllegalStateException.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 43 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
…ample Panache persist() returns void, so when().thenReturn() won't compile. Replaced with doNothing().when().persist() which is the correct Mockito pattern for void methods.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
docs/ja-JP/skills/quarkus-security/SKILL.md (1)
11-11:⚠️ Potential issue | 🟠 MajorSkill heading structure still does not comply with required format.
This issue was already flagged in a previous review comment. The file currently uses
## いつアクティブにするか("When to Activate") at Line 11, but the required structure is:
## いつ使うか("When to Use")## 仕組み("How It Works")## 例("Examples")The current structure also lacks the required "How It Works" and "Examples" top-level sections. The authentication, authorization, input validation, and other security topics should be reorganized under these three main headings.
As per coding guidelines: "Skills must be formatted as Markdown files with clear sections: 'When to use', 'How it works', and 'Examples'."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ja-JP/skills/quarkus-security/SKILL.md` at line 11, Replace the incorrect heading "## いつアクティブにするか" with the required top-level heading "## いつ使うか", and create the missing top-level headings "## 仕組み" and "## 例"; then reorganize the existing security content (authentication, authorization, input validation, etc.) so that decision/usage guidance goes under "## いつ使うか", implementation details and mechanisms go under "## 仕組み", and concrete code/configuration snippets or scenario examples go under "## 例" to match the mandated Skill heading structure.docs/tr/skills/quarkus-verification/SKILL.md (1)
11-479: 🛠️ Refactor suggestion | 🟠 MajorGerekli skill bölümlerini ekleyin.
Satır 11'deki başlık "Ne Zaman Kullanılır" olmalı ("Ne Zaman Aktif Edilir" değil) ve belge, skill formatına uymak için açık "## Nasıl Çalışır" ve "## Örnekler" bölüm başlıklarını içermelidir.
As per coding guidelines: Skills should be formatted as Markdown files with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'.
📋 Önerilen yapılandırma
-## Ne Zaman Aktif Edilir +## Ne Zaman Kullanılır - Quarkus servisi için pull request açmadan önce ... +## Nasıl Çalışır + +10 fazlı doğrulama döngüsü: Build → Static Analiz → Testler + Kapsam → Güvenlik +Taraması → Native Derleme → Performans Testi → Sağlık Kontrolleri → Container +Image Build → Yapılandırma Doğrulama → Dokümantasyon İncelemesi + +## Örnekler + ## Faz 1: Build ...Tüm faz açıklamalarını, script'leri ve kontrol listelerini "Örnekler" altında gruplandırın.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/skills/quarkus-verification/SKILL.md` around lines 11 - 479, Rename the top-level heading "Ne Zaman Aktif Edilir" to "Ne Zaman Kullanılır", add two new section headings "## Nasıl Çalışır" and "## Örnekler" in the document, and move/group all phase explanations, scripts, and the verification/checklist content (Faz 1..Faz 10, Otomatik Doğrulama Script'i, CI/CD örneği, Doğrulama Kontrol Listesi) under the new "## Örnekler" section; ensure the new "## Nasıl Çalışır" section contains a short description of the verification workflow (build → static analiz → testler → güvenlik → native → deploy checks) and that headings use the exact Turkish titles ("Ne Zaman Kullanılır", "## Nasıl Çalışır", "## Örnekler") so the file conforms to the required skill format.docs/tr/skills/quarkus-security/SKILL.md (1)
11-464: 🛠️ Refactor suggestion | 🟠 MajorAdd required "Nasıl Çalışır" and "Örnekler" sections.
The heading at line 11 should be "Ne Zaman Kullanılır" (not "Ne Zaman Aktif Edilir"), and the document must include explicit "## Nasıl Çalışır" (How It Works) and "## Örnekler" (Examples) section headings to comply with the skill format.
As per coding guidelines: Skills should be formatted with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'.
📋 Suggested restructuring
-## Ne Zaman Aktif Edilir +## Ne Zaman Kullanılır - Kimlik doğrulama ekleme (JWT, OIDC, Basic Auth) ... +## Nasıl Çalışır + +Bu skill, Quarkus uygulamalarında güvenlik katmanlarını (kimlik doğrulama, yetkilendirme, +girdi doğrulama, SQL injection koruması, gizli bilgi yönetimi, rate limiting, güvenlik +başlıkları) uygulamak için Quarkus Security, MicroProfile JWT, Bean Validation, ve +SecurityIdentity API'lerini kullanır. + +## Örnekler ## Kimlik Doğrulama ...Group all existing implementation sections (Kimlik Doğrulama through Bağımlılık Güvenliği Taraması) under "Örnekler".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tr/skills/quarkus-security/SKILL.md` around lines 11 - 464, Rename the top heading "Ne Zaman Aktif Edilir" to "Ne Zaman Kullanılır", add two new section headings "## Nasıl Çalışır" and "## Örnekler" after that heading, and move or group the existing implementation subsections (the blocks starting with "Kimlik Doğrulama" through "Bağımlılık Güvenliği Taraması") under the new "## Örnekler" section; ensure the new "## Nasıl Çalışır" contains a brief explanation of the security model (JWT/oidc, SecurityIdentity, filters) and "## Örnekler" wraps the code examples already present so the document follows the required "When to Use / How It Works / Examples" structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/tr/skills/quarkus-tdd/SKILL.md`:
- Around line 438-453: The Turkish test
givenNullPayload_whenCreateSuccessEvent_thenThrowsException asserts
IllegalArgumentException for eventService.createSuccessEvent(nullPayload,
"EVENT_TYPE") but the English version expects NullPointerException; make them
consistent by updating this test to expect NullPointerException (i.e., change
the assertThrows() expected type to NullPointerException) or alternatively
update both language versions to expect IllegalArgumentException—ensure the
change targets the test method
givenNullPayload_whenCreateSuccessEvent_thenThrowsException and the call to
eventService.createSuccessEvent so both localized tests assert the same
exception type.
- Around line 22-912: Rename the "## İş Akışı" heading to "## Nasıl Çalışır" and
move all code blocks and example sections (the large Java code examples and
Camel/REST/Integration snippets shown under the current content) into a new "##
Örnekler" section; ensure the top-level Markdown includes the required sections
"When to Use", "How It Works" (use the renamed "Nasıl Çalışır"), and "Examples"
(## Örnekler) and update any internal references to the old "İş Akışı" heading
so examples (all fenced code blocks and related subsections like "@Nested
Organizasyonlu Unit Testler", "Camel Route Testi", "Event Service Testi", etc.)
are grouped under the new Examples heading.
- Around line 359-386: The test
givenValidationError_whenProcess_thenRoutesToErrorHandler references a
non-existent eventService.validate(...) method; fix by either (A) changing the
mocked call to use an existing EventService method such as createErrorEvent (or
createSuccessEvent) so that when(eventService.createErrorEvent(...)) throws new
ValidationException(...) the route will exercise the error path, or (B) add a
validate(...) signature to the EventService interface/class and implement it so
the test can mock eventService.validate(...) to throw ValidationException;
update the test to mock the chosen existing symbol
(eventService.createErrorEvent or the newly added eventService.validate)
accordingly.
In `@skills/quarkus-tdd/SKILL.md`:
- Around line 361-388: The test references documentValidator.validate(...) but
no mock is declared; either declare and inject a mock for the validator (e.g.,
add an `@InjectMock` or `@Mock` field named documentValidator of type
DocumentValidator and use
when(documentValidator.validate(any())).thenThrow(...)) or, if validation is
performed by the existing eventService, change the mocked call to
when(eventService.validate(any())).thenThrow(...) and keep the rest of the test
unchanged; update the test class to include the chosen mock field
(documentValidator OR reuse eventService) so the when(...) call compiles and the
stubbed exception is applied.
- Around line 22-915: Rename the top-level "Workflow" heading to "How It Works",
add a new "## When to Use" section (brief guidance) above it, and create an
explicit "## Examples" heading; then move all code example sections (any blocks
titled "Unit Tests with `@Nested` Organization", "Testing Camel Routes", "Testing
Event Services", "Testing CompletableFuture", "Resource Layer Tests (REST
Assured)", "Integration Tests with Real Database", etc.) under the new "##
Examples" section so the SKILL.md follows the required "When to Use / How It
Works / Examples" structure and headings.
---
Duplicate comments:
In `@docs/ja-JP/skills/quarkus-security/SKILL.md`:
- Line 11: Replace the incorrect heading "## いつアクティブにするか" with the required
top-level heading "## いつ使うか", and create the missing top-level headings "## 仕組み"
and "## 例"; then reorganize the existing security content (authentication,
authorization, input validation, etc.) so that decision/usage guidance goes
under "## いつ使うか", implementation details and mechanisms go under "## 仕組み", and
concrete code/configuration snippets or scenario examples go under "## 例" to
match the mandated Skill heading structure.
In `@docs/tr/skills/quarkus-security/SKILL.md`:
- Around line 11-464: Rename the top heading "Ne Zaman Aktif Edilir" to "Ne
Zaman Kullanılır", add two new section headings "## Nasıl Çalışır" and "##
Örnekler" after that heading, and move or group the existing implementation
subsections (the blocks starting with "Kimlik Doğrulama" through "Bağımlılık
Güvenliği Taraması") under the new "## Örnekler" section; ensure the new "##
Nasıl Çalışır" contains a brief explanation of the security model (JWT/oidc,
SecurityIdentity, filters) and "## Örnekler" wraps the code examples already
present so the document follows the required "When to Use / How It Works /
Examples" structure.
In `@docs/tr/skills/quarkus-verification/SKILL.md`:
- Around line 11-479: Rename the top-level heading "Ne Zaman Aktif Edilir" to
"Ne Zaman Kullanılır", add two new section headings "## Nasıl Çalışır" and "##
Örnekler" in the document, and move/group all phase explanations, scripts, and
the verification/checklist content (Faz 1..Faz 10, Otomatik Doğrulama Script'i,
CI/CD örneği, Doğrulama Kontrol Listesi) under the new "## Örnekler" section;
ensure the new "## Nasıl Çalışır" section contains a short description of the
verification workflow (build → static analiz → testler → güvenlik → native →
deploy checks) and that headings use the exact Turkish titles ("Ne Zaman
Kullanılır", "## Nasıl Çalışır", "## Örnekler") so the file conforms to the
required skill format.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a11961b8-29ff-4e10-825d-a2c61cdf4ca6
📒 Files selected for processing (20)
.opencode/prompts/agents/java-reviewer.txtagents/java-build-resolver.mddocs/ja-JP/skills/quarkus-patterns/SKILL.mddocs/ja-JP/skills/quarkus-security/SKILL.mddocs/ja-JP/skills/quarkus-tdd/SKILL.mddocs/ja-JP/skills/quarkus-verification/SKILL.mddocs/tr/skills/quarkus-patterns/SKILL.mddocs/tr/skills/quarkus-security/SKILL.mddocs/tr/skills/quarkus-tdd/SKILL.mddocs/tr/skills/quarkus-verification/SKILL.mddocs/zh-CN/skills/prompt-optimizer/SKILL.mddocs/zh-CN/skills/quarkus-patterns/SKILL.mddocs/zh-CN/skills/quarkus-security/SKILL.mddocs/zh-CN/skills/quarkus-tdd/SKILL.mddocs/zh-CN/skills/quarkus-verification/SKILL.mdskills/prompt-optimizer/SKILL.mdskills/quarkus-patterns/SKILL.mdskills/quarkus-security/SKILL.mdskills/quarkus-tdd/SKILL.mdskills/quarkus-verification/SKILL.md
✅ Files skipped from review due to trivial changes (9)
- .opencode/prompts/agents/java-reviewer.txt
- skills/quarkus-patterns/SKILL.md
- docs/tr/skills/quarkus-patterns/SKILL.md
- skills/quarkus-security/SKILL.md
- docs/zh-CN/skills/quarkus-patterns/SKILL.md
- docs/ja-JP/skills/quarkus-verification/SKILL.md
- docs/ja-JP/skills/quarkus-patterns/SKILL.md
- docs/ja-JP/skills/quarkus-tdd/SKILL.md
- docs/zh-CN/skills/quarkus-security/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (5)
- skills/prompt-optimizer/SKILL.md
- skills/quarkus-verification/SKILL.md
- docs/zh-CN/skills/prompt-optimizer/SKILL.md
- docs/zh-CN/skills/quarkus-verification/SKILL.md
- docs/zh-CN/skills/quarkus-tdd/SKILL.md
| ## İş Akışı | ||
|
|
||
| 1. Önce testleri yazın (başarısız olmalılar) | ||
| 2. Geçmek için minimal kod uygulayın | ||
| 3. Testleri yeşil tutarken refactor edin | ||
| 4. JaCoCo ile kapsamı zorlayın (%80+ hedef) | ||
|
|
||
| ## @Nested Organizasyonlu Unit Testler | ||
|
|
||
| Kapsamlı ve okunabilir testler için bu yapılandırılmış yaklaşımı izleyin: | ||
|
|
||
| ```java | ||
| @ExtendWith(MockitoExtension.class) | ||
| @DisplayName("As2ProcessingService Unit Tests") | ||
| class As2ProcessingServiceTest { | ||
|
|
||
| @Mock | ||
| private InvoiceFlowValidator invoiceFlowValidator; | ||
|
|
||
| @Mock | ||
| private EventService eventService; | ||
|
|
||
| @Mock | ||
| private DocumentJobService documentJobService; | ||
|
|
||
| @Mock | ||
| private BusinessRulesPublisher businessRulesPublisher; | ||
|
|
||
| @Mock | ||
| private FileStorageService fileStorageService; | ||
|
|
||
| @InjectMocks | ||
| private As2ProcessingService as2ProcessingService; | ||
|
|
||
| private Path testFilePath; | ||
| private LogContext testLogContext; | ||
| private InvoiceValidationResult validationResult; | ||
| private StoredDocumentInfo documentInfo; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| // ARRANGE - Ortak test verisi | ||
| testFilePath = Path.of("/tmp/test-invoice.xml"); | ||
|
|
||
| testLogContext = new LogContext(); | ||
| testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001"); | ||
| testLogContext.put(As2Constants.FILE_NAME, "invoice.xml"); | ||
| testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001"); | ||
|
|
||
| validationResult = new InvoiceValidationResult(); | ||
| validationResult.setValid(true); | ||
| validationResult.setSize(1024L); | ||
| validationResult.setDocumentHash("abc123"); | ||
|
|
||
| documentInfo = new StoredDocumentInfo(); | ||
| documentInfo.setPath("s3://bucket/path/invoice.xml"); | ||
| documentInfo.setSize(1024L); | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("processFile için testler") | ||
| class ProcessFile { | ||
|
|
||
| @Test | ||
| @DisplayName("CHORUS olmayan dosyayı tüm validasyonlarla başarıyla işlemeli") | ||
| void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception { | ||
| // ARRANGE | ||
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | ||
| CustomLog.setCurrentContext(testLogContext); | ||
|
|
||
| when(invoiceFlowValidator.validateFlowWithConfig( | ||
| eq(testFilePath), | ||
| eq(ValidationFlowConfig.allValidations()), | ||
| eq(EInvoiceSyntaxFormat.UBL), | ||
| any(LogContext.class))) | ||
| .thenReturn(validationResult); | ||
|
|
||
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | ||
| .thenReturn(FlowProfile.BASIC); | ||
|
|
||
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | ||
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | ||
|
|
||
| when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any())) | ||
| .thenReturn(new BusinessRulesPayload()); | ||
|
|
||
| // ACT | ||
| assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); | ||
|
|
||
| // ASSERT | ||
| verify(invoiceFlowValidator).validateFlowWithConfig( | ||
| eq(testFilePath), | ||
| eq(ValidationFlowConfig.allValidations()), | ||
| eq(EInvoiceSyntaxFormat.UBL), | ||
| any(LogContext.class)); | ||
|
|
||
| verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), | ||
| eq("PERSISTENCE_BLOB_EVENT_TYPE")); | ||
| verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class), | ||
| eq("BUSINESS_RULES_MESSAGE_SENT")); | ||
| verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class)); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("CHORUS_FLOW için schematron validasyonu atlanmalı") | ||
| void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception { | ||
| // ARRANGE | ||
| testLogContext.put(As2Constants.CHORUS_FLOW, "true"); | ||
| CustomLog.setCurrentContext(testLogContext); | ||
|
|
||
| when(invoiceFlowValidator.validateFlowWithConfig( | ||
| eq(testFilePath), | ||
| eq(ValidationFlowConfig.xsdOnly()), | ||
| eq(EInvoiceSyntaxFormat.UBL), | ||
| any(LogContext.class))) | ||
| .thenReturn(validationResult); | ||
|
|
||
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | ||
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | ||
|
|
||
| when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), | ||
| eq(FlowProfile.EXTENDED_CTC_FR), any())) | ||
| .thenReturn(new BusinessRulesPayload()); | ||
|
|
||
| // ACT | ||
| assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); | ||
|
|
||
| // ASSERT | ||
| verify(invoiceFlowValidator).validateFlowWithConfig( | ||
| eq(testFilePath), | ||
| eq(ValidationFlowConfig.xsdOnly()), | ||
| eq(EInvoiceSyntaxFormat.UBL), | ||
| any(LogContext.class)); | ||
|
|
||
| verify(documentJobService).createDocumentAndJobEntities( | ||
| any(), any(), any(), | ||
| eq(FlowProfile.EXTENDED_CTC_FR), | ||
| any()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Dosya yükleme başarısız olduğunda hata eventi oluşturulmalı") | ||
| void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception { | ||
| // ARRANGE | ||
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | ||
| CustomLog.setCurrentContext(testLogContext); | ||
|
|
||
| when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) | ||
| .thenReturn(validationResult); | ||
|
|
||
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | ||
| .thenReturn(FlowProfile.BASIC); | ||
|
|
||
| documentInfo.setPath(""); // Boş path hatayı tetikler | ||
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | ||
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | ||
|
|
||
| // ACT & ASSERT | ||
| As2ServerProcessingException exception = assertThrows( | ||
| As2ServerProcessingException.class, | ||
| () -> as2ProcessingService.processFile(testFilePath) | ||
| ); | ||
|
|
||
| assertThat(exception.getMessage()) | ||
| .contains("File path is empty after upload"); | ||
|
|
||
| verify(eventService).createErrorEvent( | ||
| eq(documentInfo), | ||
| eq("FILE_UPLOAD_FAILED"), | ||
| contains("File path is empty")); | ||
|
|
||
| verify(businessRulesPublisher, never()).publishAsync(any()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("CompletableFuture.join() başarısızlığı ele alınmalı") | ||
| void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception { | ||
| // ARRANGE | ||
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | ||
| CustomLog.setCurrentContext(testLogContext); | ||
|
|
||
| when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) | ||
| .thenReturn(validationResult); | ||
|
|
||
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | ||
| .thenReturn(FlowProfile.BASIC); | ||
|
|
||
| CompletableFuture<StoredDocumentInfo> failedFuture = | ||
| CompletableFuture.failedFuture(new StorageException("S3 connection failed")); | ||
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | ||
| .thenReturn(failedFuture); | ||
|
|
||
| // ACT & ASSERT | ||
| assertThrows( | ||
| CompletionException.class, | ||
| () -> as2ProcessingService.processFile(testFilePath) | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Dosya yolu null olduğunda exception fırlatılmalı") | ||
| void givenNullFilePath_whenProcessFile_thenThrowsException() { | ||
| // ARRANGE | ||
| Path nullPath = null; | ||
|
|
||
| // ACT & ASSERT | ||
| NullPointerException exception = assertThrows( | ||
| NullPointerException.class, | ||
| () -> as2ProcessingService.processFile(nullPath) | ||
| ); | ||
|
|
||
| verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any()); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Temel Test Desenleri | ||
|
|
||
| 1. **@Nested Sınıflar**: Testleri test edilen metoda göre gruplandırın | ||
| 2. **@DisplayName**: Test raporlarında okunabilir açıklamalar sağlayın | ||
| 3. **İsimlendirme Kuralı**: Netlik için `givenX_whenY_thenZ` | ||
| 4. **AAA Deseni**: Açık `// ARRANGE`, `// ACT`, `// ASSERT` yorumları | ||
| 5. **@BeforeEach**: Tekrarı azaltmak için ortak test verisi kurulumu | ||
| 6. **assertDoesNotThrow**: Exception yakalamadan başarı senaryolarını test edin | ||
| 7. **assertThrows**: AssertJ kullanarak mesaj doğrulamalı exception senaryolarını test edin | ||
| 8. **Kapsamlı Kapsam**: Mutlu yolları, null girdileri, edge case'leri, exception'ları test edin | ||
| 9. **Etkileşimleri Doğrulama**: Metodların doğru çağrıldığından emin olmak için Mockito `verify()` kullanın | ||
| 10. **Hiçbir Zaman Doğrulama**: Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `never()` kullanın | ||
|
|
||
| ## Camel Route Testi | ||
|
|
||
| ```java | ||
| @QuarkusTest | ||
| @DisplayName("Business Rules Camel Route Tests") | ||
| class BusinessRulesRouteTest { | ||
|
|
||
| @Inject | ||
| CamelContext camelContext; | ||
|
|
||
| @Inject | ||
| ProducerTemplate producerTemplate; | ||
|
|
||
| @InjectMock | ||
| EventService eventService; | ||
|
|
||
| private BusinessRulesPayload testPayload; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| // ARRANGE - Test verisi | ||
| testPayload = new BusinessRulesPayload(); | ||
| testPayload.setDocumentId(1L); | ||
| testPayload.setFlowProfile(FlowProfile.BASIC); | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("business-rules-publisher route için testler") | ||
| class BusinessRulesPublisher { | ||
|
|
||
| @Test | ||
| @DisplayName("Mesajı başarıyla RabbitMQ'ya yayınlamalı") | ||
| void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); | ||
| mockRabbitMQ.expectedMessageCount(1); | ||
|
|
||
| // Test için gerçek endpoint'i mock ile değiştir | ||
| camelContext.getRouteController().stopRoute("business-rules-publisher"); | ||
| AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { | ||
| advice.replaceFromWith("direct:business-rules-publisher"); | ||
| advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("business-rules-publisher"); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBody("direct:business-rules-publisher", testPayload); | ||
|
|
||
| // ASSERT — .marshal().json() sonrası body JSON String'dir | ||
| mockRabbitMQ.assertIsSatisfied(5000); | ||
|
|
||
| assertThat(mockRabbitMQ.getExchanges()).hasSize(1); | ||
| String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); | ||
| assertThat(body).contains("\"documentId\":1"); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("JSON'a marshalling'i ele almalı") | ||
| void givenPayload_whenPublish_thenMarshalledToJson() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); | ||
| camelContext.addEndpoint("mock:marshal", mockMarshal); | ||
| mockMarshal.expectedMessageCount(1); | ||
|
|
||
| camelContext.getRouteController().stopRoute("business-rules-publisher"); | ||
| AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { | ||
| advice.weaveAddLast().to("mock:marshal"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("business-rules-publisher"); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBody("direct:business-rules-publisher", testPayload); | ||
|
|
||
| // ASSERT | ||
| mockMarshal.assertIsSatisfied(5000); | ||
|
|
||
| String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); | ||
| assertThat(body).contains("\"documentId\":1"); | ||
| assertThat(body).contains("\"flowProfile\":\"BASIC\""); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("document-processing route için testler") | ||
| class DocumentProcessing { | ||
|
|
||
| @Test | ||
| @DisplayName("Faturayı doğru işlemciye yönlendirmeli") | ||
| void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); | ||
| mockInvoice.expectedMessageCount(1); | ||
|
|
||
| camelContext.getRouteController().stopRoute("document-processing"); | ||
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | ||
| advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("document-processing"); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBodyAndHeader("direct:process-document", | ||
| testPayload, "documentType", "INVOICE"); | ||
|
|
||
| // ASSERT | ||
| mockInvoice.assertIsSatisfied(5000); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Validasyon hatalarını zarif şekilde ele almalı") | ||
| void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); | ||
| mockError.expectedMessageCount(1); | ||
|
|
||
| camelContext.getRouteController().stopRoute("document-processing"); | ||
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | ||
| advice.weaveByToString(".*direct:validation-error-handler.*") | ||
| .replace().to("mock:error"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("document-processing"); | ||
|
|
||
| // Validator'ı exception fırlatacak şekilde mock'la | ||
| when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document")); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBody("direct:process-document", testPayload); | ||
|
|
||
| // ASSERT | ||
| mockError.assertIsSatisfied(5000); | ||
|
|
||
| Exception exception = mockError.getExchanges().get(0).getException(); | ||
| assertThat(exception).isInstanceOf(ValidationException.class); | ||
| assertThat(exception.getMessage()).contains("Invalid document"); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Event Service Testi | ||
|
|
||
| ```java | ||
| @ExtendWith(MockitoExtension.class) | ||
| @DisplayName("EventService Unit Tests") | ||
| class EventServiceTest { | ||
|
|
||
| @Mock | ||
| private EventRepository eventRepository; | ||
|
|
||
| @Mock | ||
| private ObjectMapper objectMapper; | ||
|
|
||
| @InjectMocks | ||
| private EventService eventService; | ||
|
|
||
| private BusinessRulesPayload testPayload; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| // ARRANGE | ||
| testPayload = new BusinessRulesPayload(); | ||
| testPayload.setDocumentId(1L); | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("createSuccessEvent için testler") | ||
| class CreateSuccessEvent { | ||
|
|
||
| @Test | ||
| @DisplayName("Doğru niteliklerle başarı eventi oluşturulmalı") | ||
| void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { | ||
| // ARRANGE | ||
| when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); | ||
|
|
||
| // ACT | ||
| assertDoesNotThrow(() -> | ||
| eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); | ||
|
|
||
| // ASSERT | ||
| verify(eventRepository).persist(argThat(event -> | ||
| event.getType().equals("DOCUMENT_PROCESSED") && | ||
| event.getStatus() == EventStatus.SUCCESS && | ||
| event.getPayload().equals("{\"documentId\":1}") && | ||
| event.getTimestamp() != null | ||
| )); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Payload null olduğunda exception fırlatılmalı") | ||
| void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { | ||
| // ARRANGE | ||
| Object nullPayload = null; | ||
|
|
||
| // ACT & ASSERT | ||
| IllegalArgumentException exception = assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") | ||
| ); | ||
|
|
||
| assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); | ||
| verify(eventRepository, never()).persist(any()); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("createErrorEvent için testler") | ||
| class CreateErrorEvent { | ||
|
|
||
| @Test | ||
| @DisplayName("Hata mesajıyla hata eventi oluşturulmalı") | ||
| void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { | ||
| // ARRANGE | ||
| String errorMessage = "Processing failed"; | ||
| when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); | ||
|
|
||
| // ACT | ||
| assertDoesNotThrow(() -> | ||
| eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); | ||
|
|
||
| // ASSERT | ||
| verify(eventRepository).persist(argThat(event -> | ||
| event.getType().equals("PROCESSING_ERROR") && | ||
| event.getStatus() == EventStatus.ERROR && | ||
| event.getErrorMessage().equals(errorMessage) && | ||
| event.getPayload().equals("{\"documentId\":1}") | ||
| )); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @DisplayName("Geçersiz hata mesajları reddedilmeli") | ||
| @ValueSource(strings = {"", " "}) | ||
| void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) { | ||
| // ACT & ASSERT | ||
| IllegalArgumentException exception = assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) | ||
| ); | ||
|
|
||
| assertThat(exception.getMessage()).contains("Error message cannot be blank"); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## CompletableFuture Testi | ||
|
|
||
| ```java | ||
| @ExtendWith(MockitoExtension.class) | ||
| @DisplayName("FileStorageService Unit Tests") | ||
| class FileStorageServiceTest { | ||
|
|
||
| @Mock | ||
| private S3Client s3Client; | ||
|
|
||
| @Mock | ||
| private ExecutorService executorService; | ||
|
|
||
| @InjectMocks | ||
| private FileStorageService fileStorageService; | ||
|
|
||
| private InputStream testInputStream; | ||
| private LogContext testLogContext; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| // ARRANGE | ||
| testInputStream = new ByteArrayInputStream("test content".getBytes()); | ||
| testLogContext = new LogContext(); | ||
| testLogContext.put("traceId", "trace-123"); | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("uploadOriginalFile için testler") | ||
| class UploadOriginalFile { | ||
|
|
||
| @Test | ||
| @DisplayName("Dosyayı başarıyla yüklemeli ve belge bilgisi döndürmeli") | ||
| void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { | ||
| // ARRANGE | ||
| doAnswer(invocation -> { | ||
| ((Runnable) invocation.getArgument(0)).run(); | ||
| return null; | ||
| }).when(executorService).execute(any(Runnable.class)); | ||
|
|
||
| when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | ||
| .thenReturn(PutObjectResponse.builder().build()); | ||
|
|
||
| // ACT | ||
| CompletableFuture<StoredDocumentInfo> future = | ||
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | ||
| testLogContext, InvoiceFormat.UBL); | ||
|
|
||
| StoredDocumentInfo result = future.join(); | ||
|
|
||
| // ASSERT | ||
| assertThat(result).isNotNull(); | ||
| assertThat(result.getPath()).isNotBlank(); | ||
| assertThat(result.getSize()).isEqualTo(1024L); | ||
| assertThat(result.getUploadedAt()).isNotNull(); | ||
|
|
||
| verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("S3 yükleme başarısızlığını ele almalı") | ||
| void givenS3Failure_whenUpload_thenCompletableFutureFails() { | ||
| // ARRANGE | ||
| doAnswer(invocation -> { | ||
| ((Runnable) invocation.getArgument(0)).run(); | ||
| return null; | ||
| }).when(executorService).execute(any(Runnable.class)); | ||
|
|
||
| when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | ||
| .thenThrow(new StorageException("S3 unavailable")); | ||
|
|
||
| // ACT | ||
| CompletableFuture<StoredDocumentInfo> future = | ||
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | ||
| testLogContext, InvoiceFormat.UBL); | ||
|
|
||
| // ASSERT | ||
| assertThatThrownBy(() -> future.join()) | ||
| .isInstanceOf(CompletionException.class) | ||
| .hasCauseInstanceOf(StorageException.class) | ||
| .hasMessageContaining("S3 unavailable"); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("LogContext'i async işleme yaymalı") | ||
| void givenLogContext_whenUpload_thenContextPropagated() throws Exception { | ||
| // ARRANGE | ||
| AtomicReference<LogContext> capturedContext = new AtomicReference<>(); | ||
|
|
||
| doAnswer(invocation -> { | ||
| capturedContext.set(CustomLog.getCurrentContext()); | ||
| ((Runnable) invocation.getArgument(0)).run(); | ||
| return null; | ||
| }).when(executorService).execute(any(Runnable.class)); | ||
|
|
||
| // ACT | ||
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | ||
| testLogContext, InvoiceFormat.UBL).join(); | ||
|
|
||
| // ASSERT | ||
| assertThat(capturedContext.get()).isNotNull(); | ||
| assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Resource Katmanı Testleri (REST Assured) | ||
|
|
||
| ```java | ||
| @QuarkusTest | ||
| @DisplayName("DocumentResource API Tests") | ||
| class DocumentResourceTest { | ||
|
|
||
| @InjectMock | ||
| DocumentService documentService; | ||
|
|
||
| @Nested | ||
| @DisplayName("GET /api/documents için testler") | ||
| class ListDocuments { | ||
|
|
||
| @Test | ||
| @DisplayName("Belge listesini döndürmeli") | ||
| void givenDocumentsExist_whenList_thenReturnsOk() { | ||
| // ARRANGE | ||
| List<Document> documents = List.of(createDocument(1L, "DOC-001")); | ||
| when(documentService.list(0, 20)).thenReturn(documents); | ||
|
|
||
| // ACT & ASSERT | ||
| given() | ||
| .when().get("/api/documents") | ||
| .then() | ||
| .statusCode(200) | ||
| .body("$.size()", is(1)) | ||
| .body("[0].referenceNumber", equalTo("DOC-001")); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("POST /api/documents için testler") | ||
| class CreateDocument { | ||
|
|
||
| @Test | ||
| @DisplayName("Belge oluşturmalı ve 201 döndürmeli") | ||
| void givenValidRequest_whenCreate_thenReturns201() { | ||
| // ARRANGE | ||
| Document document = createDocument(1L, "DOC-001"); | ||
| when(documentService.create(any())).thenReturn(document); | ||
|
|
||
| // ACT & ASSERT | ||
| given() | ||
| .contentType(ContentType.JSON) | ||
| .body(""" | ||
| { | ||
| "referenceNumber": "DOC-001", | ||
| "description": "Test document", | ||
| "validUntil": "2030-01-01T00:00:00Z", | ||
| "categories": ["test"] | ||
| } | ||
| """) | ||
| .when().post("/api/documents") | ||
| .then() | ||
| .statusCode(201) | ||
| .header("Location", containsString("/api/documents/1")) | ||
| .body("referenceNumber", equalTo("DOC-001")); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Geçersiz girdi için 400 döndürmeli") | ||
| void givenInvalidRequest_whenCreate_thenReturns400() { | ||
| // ACT & ASSERT | ||
| given() | ||
| .contentType(ContentType.JSON) | ||
| .body(""" | ||
| { | ||
| "referenceNumber": "", | ||
| "description": "Test" | ||
| } | ||
| """) | ||
| .when().post("/api/documents") | ||
| .then() | ||
| .statusCode(400); | ||
| } | ||
| } | ||
|
|
||
| private Document createDocument(Long id, String referenceNumber) { | ||
| Document document = new Document(); | ||
| document.setId(id); | ||
| document.setReferenceNumber(referenceNumber); | ||
| document.setStatus(DocumentStatus.PENDING); | ||
| return document; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Gerçek Veritabanıyla Entegrasyon Testleri | ||
|
|
||
| ```java | ||
| @QuarkusTest | ||
| @TestProfile(IntegrationTestProfile.class) | ||
| @DisplayName("Document Integration Tests") | ||
| class DocumentIntegrationTest { | ||
|
|
||
| @Test | ||
| @Transactional | ||
| @DisplayName("Belge oluşturulmalı ve API üzerinden alınabilmeli") | ||
| void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() { | ||
| // ACT - API üzerinden oluştur | ||
| Long id = given() | ||
| .contentType(ContentType.JSON) | ||
| .body(""" | ||
| { | ||
| "referenceNumber": "INT-001", | ||
| "description": "Integration test", | ||
| "validUntil": "2030-01-01T00:00:00Z", | ||
| "categories": ["test"] | ||
| } | ||
| """) | ||
| .when().post("/api/documents") | ||
| .then() | ||
| .statusCode(201) | ||
| .extract().path("id"); | ||
|
|
||
| // ASSERT - API üzerinden al | ||
| given() | ||
| .when().get("/api/documents/" + id) | ||
| .then() | ||
| .statusCode(200) | ||
| .body("referenceNumber", equalTo("INT-001")); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## JaCoCo ile Kapsam | ||
|
|
||
| ### Maven Yapılandırması (Tam) | ||
|
|
||
| ```xml | ||
| <plugin> | ||
| <groupId>org.jacoco</groupId> | ||
| <artifactId>jacoco-maven-plugin</artifactId> | ||
| <version>0.8.13</version> | ||
| <executions> | ||
| <!-- Test yürütmesi için agent'ı hazırla --> | ||
| <execution> | ||
| <id>prepare-agent</id> | ||
| <goals> | ||
| <goal>prepare-agent</goal> | ||
| </goals> | ||
| </execution> | ||
|
|
||
| <!-- Kapsam raporu oluştur --> | ||
| <execution> | ||
| <id>report</id> | ||
| <phase>verify</phase> | ||
| <goals> | ||
| <goal>report</goal> | ||
| </goals> | ||
| </execution> | ||
|
|
||
| <!-- Kapsam eşiklerini zorla --> | ||
| <execution> | ||
| <id>check</id> | ||
| <goals> | ||
| <goal>check</goal> | ||
| </goals> | ||
| <configuration> | ||
| <rules> | ||
| <rule> | ||
| <element>BUNDLE</element> | ||
| <limits> | ||
| <limit> | ||
| <counter>LINE</counter> | ||
| <value>COVEREDRATIO</value> | ||
| <minimum>0.80</minimum> | ||
| </limit> | ||
| <limit> | ||
| <counter>BRANCH</counter> | ||
| <value>COVEREDRATIO</value> | ||
| <minimum>0.70</minimum> | ||
| </limit> | ||
| </limits> | ||
| </rule> | ||
| </rules> | ||
| </configuration> | ||
| </execution> | ||
| </executions> | ||
| </plugin> | ||
| ``` | ||
|
|
||
| Kapsam ile testleri çalıştırın: | ||
| ```bash | ||
| mvn clean test | ||
| mvn jacoco:report | ||
| mvn jacoco:check | ||
|
|
||
| # Rapor: target/site/jacoco/index.html | ||
| ``` | ||
|
|
||
| ## Test Bağımlılıkları | ||
|
|
||
| ```xml | ||
| <dependencies> | ||
| <!-- Quarkus Test --> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-junit5</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-junit5-mockito</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <!-- Mockito --> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-core</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <!-- AssertJ (JUnit assertion'larına tercih edilir) --> | ||
| <dependency> | ||
| <groupId>org.assertj</groupId> | ||
| <artifactId>assertj-core</artifactId> | ||
| <version>3.24.2</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <!-- REST Assured --> | ||
| <dependency> | ||
| <groupId>io.rest-assured</groupId> | ||
| <artifactId>rest-assured</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <!-- Camel Test --> | ||
| <dependency> | ||
| <groupId>org.apache.camel.quarkus</groupId> | ||
| <artifactId>camel-quarkus-junit5</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> | ||
| ``` | ||
|
|
||
| ## En İyi Uygulamalar | ||
|
|
||
| ### Test Organizasyonu | ||
| - Testleri test edilen metoda göre gruplandırmak için `@Nested` sınıflar kullanın | ||
| - Raporlarda görünür okunabilir açıklamalar için `@DisplayName` kullanın | ||
| - Test metodları için `givenX_whenY_thenZ` isimlendirme kuralını izleyin | ||
| - Tekrarı azaltmak için ortak test verisi kurulumunda `@BeforeEach` kullanın | ||
|
|
||
| ### Test Yapısı | ||
| - Açık yorumlarla AAA desenini izleyin (`// ARRANGE`, `// ACT`, `// ASSERT`) | ||
| - Başarı senaryoları için `assertDoesNotThrow` kullanın | ||
| - Mesaj doğrulamalı exception senaryoları için `assertThrows` kullanın | ||
| - AssertJ `contains()` veya `isEqualTo()` kullanarak exception mesajlarının beklenen değerlerle eşleştiğini doğrulayın | ||
|
|
||
| ### Test Kapsamı | ||
| - Tüm public metodlar için mutlu yolları test edin | ||
| - Null girdi işlemeyi test edin | ||
| - Edge case'leri test edin (boş koleksiyonlar, sınır değerleri, negatif ID'ler, boş string'ler) | ||
| - Exception senaryolarını kapsamlı biçimde test edin | ||
| - Tüm harici bağımlılıkları mock'layın (repository'ler, servisler, Camel endpoint'leri) | ||
| - %80+ satır kapsamı, %70+ branch kapsamı hedefleyin | ||
|
|
||
| ### Assertion'lar | ||
| - Değer kontrolleri için JUnit assertion'ları yerine **AssertJ'yi tercih edin** (`assertThat`) | ||
| - Okunabilirlik için akıcı AssertJ API'si kullanın: `assertThat(list).hasSize(3).contains(item)` | ||
| - Exception'lar için: JUnit `assertThrows` ile yakalayın, ardından AssertJ ile mesajı doğrulayın | ||
| - Fırlatılmayan başarı yolları için: JUnit `assertDoesNotThrow` kullanın | ||
| - Koleksiyonlar için: `extracting()`, `filteredOn()`, `containsExactly()` | ||
|
|
||
| ### Entegrasyon Testi | ||
| - Entegrasyon testleri için `@QuarkusTest` kullanın | ||
| - Quarkus testlerinde bağımlılıkları mock'lamak için `@InjectMock` kullanın | ||
| - API testi için REST Assured'ı tercih edin | ||
| - Test'e özel yapılandırma için `@TestProfile` kullanın | ||
|
|
||
| ### Event-Driven Test | ||
| - `AdviceWith` ve `MockEndpoint` ile Camel route'larını test edin | ||
| - `@CamelQuarkusTest` annotasyonu kullanın (bağımsız Camel testleri kullanıyorsanız) | ||
| - Mesaj içeriğini, başlıklarını ve yönlendirme mantığını doğrulayın | ||
| - Hata işleme route'larını ayrı ayrı test edin | ||
| - Unit testlerde harici sistemleri (RabbitMQ, S3, veritabanları) mock'layın | ||
|
|
||
| ### Camel Route Testi | ||
| - Mesaj akışını doğrulamak için `MockEndpoint` kullanın | ||
| - Test için route'ları değiştirmek üzere `AdviceWith` kullanın (endpoint'leri mock'larla değiştirin) | ||
| - Mesaj dönüşümünü ve marshalling'i test edin | ||
| - Exception işleme ve dead letter queue'ları test edin | ||
|
|
||
| ### Async İşlem Testi | ||
| - CompletableFuture başarı ve başarısızlık senaryolarını test edin | ||
| - Async tamamlanmayı beklemek için testlerde `.join()` kullanın | ||
| - CompletableFuture'dan exception yayılımını test edin | ||
| - LogContext yayılımını async işlemlere doğrulayın | ||
|
|
||
| ### Performans | ||
| - Testleri hızlı ve izole tutun | ||
| - Testleri sürekli modda çalıştırın: `mvn quarkus:test` | ||
| - Girdi varyasyonları için parametreli testler (`@ParameterizedTest`) kullanın | ||
| - Yeniden kullanılabilir test verisi builder'ları veya factory metodları oluşturun | ||
|
|
||
| ### Quarkus'a Özgü | ||
| - En son LTS sürümünde kalın (Quarkus 3.x) | ||
| - Native derleme uyumluluğunu periyodik olarak test edin | ||
| - Farklı senaryolar için Quarkus test profillerini kullanın | ||
| - Yerel test için Quarkus dev servislerinden yararlanın | ||
| - `@MockBean` yerine `@InjectMock` kullanın (Quarkus'a özgü) | ||
|
|
||
| ### Doğrulama En İyi Uygulamaları | ||
| - Mock'lanmış bağımlılıklardaki etkileşimleri her zaman doğrulayın | ||
| - Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `verify(mock, never())` kullanın | ||
| - Karmaşık argüman eşleştirme için `argThat()` kullanın | ||
| - Önem taşıdığında çağrı sırasını doğrulayın: Mockito'dan `InOrder` |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Skill yapısını düzeltmek için yeniden yapılandırın.
"İş Akışı" başlığı (satır 22) "Nasıl Çalışır" olmalı ve tüm kod örnekleri açık bir "## Örnekler" bölümü altında gruplanmalıdır.
As per coding guidelines: Skills should be formatted as Markdown files with clearly defined sections: 'When to Use', 'How It Works', and 'Examples'.
📋 Önerilen yeniden yapılandırma
-## İş Akışı
+## Nasıl Çalışır
1. Önce testleri yazın (başarısız olmalılar)
2. Geçmek için minimal kod uygulayın
3. Testleri yeşil tutarken refactor edin
4. JaCoCo ile kapsamı zorlayın (%80+ hedef)
+## Örnekler
+
## `@Nested` Organizasyonlu Unit Testler
...Sonraki tüm bölümleri "Örnekler" başlığı altına taşıyın.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## İş Akışı | |
| 1. Önce testleri yazın (başarısız olmalılar) | |
| 2. Geçmek için minimal kod uygulayın | |
| 3. Testleri yeşil tutarken refactor edin | |
| 4. JaCoCo ile kapsamı zorlayın (%80+ hedef) | |
| ## @Nested Organizasyonlu Unit Testler | |
| Kapsamlı ve okunabilir testler için bu yapılandırılmış yaklaşımı izleyin: | |
| ```java | |
| @ExtendWith(MockitoExtension.class) | |
| @DisplayName("As2ProcessingService Unit Tests") | |
| class As2ProcessingServiceTest { | |
| @Mock | |
| private InvoiceFlowValidator invoiceFlowValidator; | |
| @Mock | |
| private EventService eventService; | |
| @Mock | |
| private DocumentJobService documentJobService; | |
| @Mock | |
| private BusinessRulesPublisher businessRulesPublisher; | |
| @Mock | |
| private FileStorageService fileStorageService; | |
| @InjectMocks | |
| private As2ProcessingService as2ProcessingService; | |
| private Path testFilePath; | |
| private LogContext testLogContext; | |
| private InvoiceValidationResult validationResult; | |
| private StoredDocumentInfo documentInfo; | |
| @BeforeEach | |
| void setUp() { | |
| // ARRANGE - Ortak test verisi | |
| testFilePath = Path.of("/tmp/test-invoice.xml"); | |
| testLogContext = new LogContext(); | |
| testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001"); | |
| testLogContext.put(As2Constants.FILE_NAME, "invoice.xml"); | |
| testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001"); | |
| validationResult = new InvoiceValidationResult(); | |
| validationResult.setValid(true); | |
| validationResult.setSize(1024L); | |
| validationResult.setDocumentHash("abc123"); | |
| documentInfo = new StoredDocumentInfo(); | |
| documentInfo.setPath("s3://bucket/path/invoice.xml"); | |
| documentInfo.setSize(1024L); | |
| } | |
| @Nested | |
| @DisplayName("processFile için testler") | |
| class ProcessFile { | |
| @Test | |
| @DisplayName("CHORUS olmayan dosyayı tüm validasyonlarla başarıyla işlemeli") | |
| void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception { | |
| // ARRANGE | |
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | |
| CustomLog.setCurrentContext(testLogContext); | |
| when(invoiceFlowValidator.validateFlowWithConfig( | |
| eq(testFilePath), | |
| eq(ValidationFlowConfig.allValidations()), | |
| eq(EInvoiceSyntaxFormat.UBL), | |
| any(LogContext.class))) | |
| .thenReturn(validationResult); | |
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | |
| .thenReturn(FlowProfile.BASIC); | |
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | |
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | |
| when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any())) | |
| .thenReturn(new BusinessRulesPayload()); | |
| // ACT | |
| assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); | |
| // ASSERT | |
| verify(invoiceFlowValidator).validateFlowWithConfig( | |
| eq(testFilePath), | |
| eq(ValidationFlowConfig.allValidations()), | |
| eq(EInvoiceSyntaxFormat.UBL), | |
| any(LogContext.class)); | |
| verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), | |
| eq("PERSISTENCE_BLOB_EVENT_TYPE")); | |
| verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class), | |
| eq("BUSINESS_RULES_MESSAGE_SENT")); | |
| verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class)); | |
| } | |
| @Test | |
| @DisplayName("CHORUS_FLOW için schematron validasyonu atlanmalı") | |
| void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception { | |
| // ARRANGE | |
| testLogContext.put(As2Constants.CHORUS_FLOW, "true"); | |
| CustomLog.setCurrentContext(testLogContext); | |
| when(invoiceFlowValidator.validateFlowWithConfig( | |
| eq(testFilePath), | |
| eq(ValidationFlowConfig.xsdOnly()), | |
| eq(EInvoiceSyntaxFormat.UBL), | |
| any(LogContext.class))) | |
| .thenReturn(validationResult); | |
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | |
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | |
| when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), | |
| eq(FlowProfile.EXTENDED_CTC_FR), any())) | |
| .thenReturn(new BusinessRulesPayload()); | |
| // ACT | |
| assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); | |
| // ASSERT | |
| verify(invoiceFlowValidator).validateFlowWithConfig( | |
| eq(testFilePath), | |
| eq(ValidationFlowConfig.xsdOnly()), | |
| eq(EInvoiceSyntaxFormat.UBL), | |
| any(LogContext.class)); | |
| verify(documentJobService).createDocumentAndJobEntities( | |
| any(), any(), any(), | |
| eq(FlowProfile.EXTENDED_CTC_FR), | |
| any()); | |
| } | |
| @Test | |
| @DisplayName("Dosya yükleme başarısız olduğunda hata eventi oluşturulmalı") | |
| void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception { | |
| // ARRANGE | |
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | |
| CustomLog.setCurrentContext(testLogContext); | |
| when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) | |
| .thenReturn(validationResult); | |
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | |
| .thenReturn(FlowProfile.BASIC); | |
| documentInfo.setPath(""); // Boş path hatayı tetikler | |
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | |
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | |
| // ACT & ASSERT | |
| As2ServerProcessingException exception = assertThrows( | |
| As2ServerProcessingException.class, | |
| () -> as2ProcessingService.processFile(testFilePath) | |
| ); | |
| assertThat(exception.getMessage()) | |
| .contains("File path is empty after upload"); | |
| verify(eventService).createErrorEvent( | |
| eq(documentInfo), | |
| eq("FILE_UPLOAD_FAILED"), | |
| contains("File path is empty")); | |
| verify(businessRulesPublisher, never()).publishAsync(any()); | |
| } | |
| @Test | |
| @DisplayName("CompletableFuture.join() başarısızlığı ele alınmalı") | |
| void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception { | |
| // ARRANGE | |
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | |
| CustomLog.setCurrentContext(testLogContext); | |
| when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) | |
| .thenReturn(validationResult); | |
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | |
| .thenReturn(FlowProfile.BASIC); | |
| CompletableFuture<StoredDocumentInfo> failedFuture = | |
| CompletableFuture.failedFuture(new StorageException("S3 connection failed")); | |
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | |
| .thenReturn(failedFuture); | |
| // ACT & ASSERT | |
| assertThrows( | |
| CompletionException.class, | |
| () -> as2ProcessingService.processFile(testFilePath) | |
| ); | |
| } | |
| @Test | |
| @DisplayName("Dosya yolu null olduğunda exception fırlatılmalı") | |
| void givenNullFilePath_whenProcessFile_thenThrowsException() { | |
| // ARRANGE | |
| Path nullPath = null; | |
| // ACT & ASSERT | |
| NullPointerException exception = assertThrows( | |
| NullPointerException.class, | |
| () -> as2ProcessingService.processFile(nullPath) | |
| ); | |
| verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any()); | |
| } | |
| } | |
| } | |
| ``` | |
| ### Temel Test Desenleri | |
| 1. **@Nested Sınıflar**: Testleri test edilen metoda göre gruplandırın | |
| 2. **@DisplayName**: Test raporlarında okunabilir açıklamalar sağlayın | |
| 3. **İsimlendirme Kuralı**: Netlik için `givenX_whenY_thenZ` | |
| 4. **AAA Deseni**: Açık `// ARRANGE`, `// ACT`, `// ASSERT` yorumları | |
| 5. **@BeforeEach**: Tekrarı azaltmak için ortak test verisi kurulumu | |
| 6. **assertDoesNotThrow**: Exception yakalamadan başarı senaryolarını test edin | |
| 7. **assertThrows**: AssertJ kullanarak mesaj doğrulamalı exception senaryolarını test edin | |
| 8. **Kapsamlı Kapsam**: Mutlu yolları, null girdileri, edge case'leri, exception'ları test edin | |
| 9. **Etkileşimleri Doğrulama**: Metodların doğru çağrıldığından emin olmak için Mockito `verify()` kullanın | |
| 10. **Hiçbir Zaman Doğrulama**: Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `never()` kullanın | |
| ## Camel Route Testi | |
| ```java | |
| @QuarkusTest | |
| @DisplayName("Business Rules Camel Route Tests") | |
| class BusinessRulesRouteTest { | |
| @Inject | |
| CamelContext camelContext; | |
| @Inject | |
| ProducerTemplate producerTemplate; | |
| @InjectMock | |
| EventService eventService; | |
| private BusinessRulesPayload testPayload; | |
| @BeforeEach | |
| void setUp() { | |
| // ARRANGE - Test verisi | |
| testPayload = new BusinessRulesPayload(); | |
| testPayload.setDocumentId(1L); | |
| testPayload.setFlowProfile(FlowProfile.BASIC); | |
| } | |
| @Nested | |
| @DisplayName("business-rules-publisher route için testler") | |
| class BusinessRulesPublisher { | |
| @Test | |
| @DisplayName("Mesajı başarıyla RabbitMQ'ya yayınlamalı") | |
| void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception { | |
| // ARRANGE | |
| MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); | |
| mockRabbitMQ.expectedMessageCount(1); | |
| // Test için gerçek endpoint'i mock ile değiştir | |
| camelContext.getRouteController().stopRoute("business-rules-publisher"); | |
| AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { | |
| advice.replaceFromWith("direct:business-rules-publisher"); | |
| advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); | |
| }); | |
| camelContext.getRouteController().startRoute("business-rules-publisher"); | |
| // ACT | |
| producerTemplate.sendBody("direct:business-rules-publisher", testPayload); | |
| // ASSERT — .marshal().json() sonrası body JSON String'dir | |
| mockRabbitMQ.assertIsSatisfied(5000); | |
| assertThat(mockRabbitMQ.getExchanges()).hasSize(1); | |
| String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); | |
| assertThat(body).contains("\"documentId\":1"); | |
| } | |
| @Test | |
| @DisplayName("JSON'a marshalling'i ele almalı") | |
| void givenPayload_whenPublish_thenMarshalledToJson() throws Exception { | |
| // ARRANGE | |
| MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); | |
| camelContext.addEndpoint("mock:marshal", mockMarshal); | |
| mockMarshal.expectedMessageCount(1); | |
| camelContext.getRouteController().stopRoute("business-rules-publisher"); | |
| AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { | |
| advice.weaveAddLast().to("mock:marshal"); | |
| }); | |
| camelContext.getRouteController().startRoute("business-rules-publisher"); | |
| // ACT | |
| producerTemplate.sendBody("direct:business-rules-publisher", testPayload); | |
| // ASSERT | |
| mockMarshal.assertIsSatisfied(5000); | |
| String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); | |
| assertThat(body).contains("\"documentId\":1"); | |
| assertThat(body).contains("\"flowProfile\":\"BASIC\""); | |
| } | |
| } | |
| @Nested | |
| @DisplayName("document-processing route için testler") | |
| class DocumentProcessing { | |
| @Test | |
| @DisplayName("Faturayı doğru işlemciye yönlendirmeli") | |
| void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception { | |
| // ARRANGE | |
| MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); | |
| mockInvoice.expectedMessageCount(1); | |
| camelContext.getRouteController().stopRoute("document-processing"); | |
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | |
| advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); | |
| }); | |
| camelContext.getRouteController().startRoute("document-processing"); | |
| // ACT | |
| producerTemplate.sendBodyAndHeader("direct:process-document", | |
| testPayload, "documentType", "INVOICE"); | |
| // ASSERT | |
| mockInvoice.assertIsSatisfied(5000); | |
| } | |
| @Test | |
| @DisplayName("Validasyon hatalarını zarif şekilde ele almalı") | |
| void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception { | |
| // ARRANGE | |
| MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); | |
| mockError.expectedMessageCount(1); | |
| camelContext.getRouteController().stopRoute("document-processing"); | |
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | |
| advice.weaveByToString(".*direct:validation-error-handler.*") | |
| .replace().to("mock:error"); | |
| }); | |
| camelContext.getRouteController().startRoute("document-processing"); | |
| // Validator'ı exception fırlatacak şekilde mock'la | |
| when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document")); | |
| // ACT | |
| producerTemplate.sendBody("direct:process-document", testPayload); | |
| // ASSERT | |
| mockError.assertIsSatisfied(5000); | |
| Exception exception = mockError.getExchanges().get(0).getException(); | |
| assertThat(exception).isInstanceOf(ValidationException.class); | |
| assertThat(exception.getMessage()).contains("Invalid document"); | |
| } | |
| } | |
| } | |
| ``` | |
| ## Event Service Testi | |
| ```java | |
| @ExtendWith(MockitoExtension.class) | |
| @DisplayName("EventService Unit Tests") | |
| class EventServiceTest { | |
| @Mock | |
| private EventRepository eventRepository; | |
| @Mock | |
| private ObjectMapper objectMapper; | |
| @InjectMocks | |
| private EventService eventService; | |
| private BusinessRulesPayload testPayload; | |
| @BeforeEach | |
| void setUp() { | |
| // ARRANGE | |
| testPayload = new BusinessRulesPayload(); | |
| testPayload.setDocumentId(1L); | |
| } | |
| @Nested | |
| @DisplayName("createSuccessEvent için testler") | |
| class CreateSuccessEvent { | |
| @Test | |
| @DisplayName("Doğru niteliklerle başarı eventi oluşturulmalı") | |
| void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { | |
| // ARRANGE | |
| when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); | |
| // ACT | |
| assertDoesNotThrow(() -> | |
| eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); | |
| // ASSERT | |
| verify(eventRepository).persist(argThat(event -> | |
| event.getType().equals("DOCUMENT_PROCESSED") && | |
| event.getStatus() == EventStatus.SUCCESS && | |
| event.getPayload().equals("{\"documentId\":1}") && | |
| event.getTimestamp() != null | |
| )); | |
| } | |
| @Test | |
| @DisplayName("Payload null olduğunda exception fırlatılmalı") | |
| void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { | |
| // ARRANGE | |
| Object nullPayload = null; | |
| // ACT & ASSERT | |
| IllegalArgumentException exception = assertThrows( | |
| IllegalArgumentException.class, | |
| () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") | |
| ); | |
| assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); | |
| verify(eventRepository, never()).persist(any()); | |
| } | |
| } | |
| @Nested | |
| @DisplayName("createErrorEvent için testler") | |
| class CreateErrorEvent { | |
| @Test | |
| @DisplayName("Hata mesajıyla hata eventi oluşturulmalı") | |
| void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { | |
| // ARRANGE | |
| String errorMessage = "Processing failed"; | |
| when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); | |
| // ACT | |
| assertDoesNotThrow(() -> | |
| eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); | |
| // ASSERT | |
| verify(eventRepository).persist(argThat(event -> | |
| event.getType().equals("PROCESSING_ERROR") && | |
| event.getStatus() == EventStatus.ERROR && | |
| event.getErrorMessage().equals(errorMessage) && | |
| event.getPayload().equals("{\"documentId\":1}") | |
| )); | |
| } | |
| @ParameterizedTest | |
| @DisplayName("Geçersiz hata mesajları reddedilmeli") | |
| @ValueSource(strings = {"", " "}) | |
| void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) { | |
| // ACT & ASSERT | |
| IllegalArgumentException exception = assertThrows( | |
| IllegalArgumentException.class, | |
| () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) | |
| ); | |
| assertThat(exception.getMessage()).contains("Error message cannot be blank"); | |
| } | |
| } | |
| } | |
| ``` | |
| ## CompletableFuture Testi | |
| ```java | |
| @ExtendWith(MockitoExtension.class) | |
| @DisplayName("FileStorageService Unit Tests") | |
| class FileStorageServiceTest { | |
| @Mock | |
| private S3Client s3Client; | |
| @Mock | |
| private ExecutorService executorService; | |
| @InjectMocks | |
| private FileStorageService fileStorageService; | |
| private InputStream testInputStream; | |
| private LogContext testLogContext; | |
| @BeforeEach | |
| void setUp() { | |
| // ARRANGE | |
| testInputStream = new ByteArrayInputStream("test content".getBytes()); | |
| testLogContext = new LogContext(); | |
| testLogContext.put("traceId", "trace-123"); | |
| } | |
| @Nested | |
| @DisplayName("uploadOriginalFile için testler") | |
| class UploadOriginalFile { | |
| @Test | |
| @DisplayName("Dosyayı başarıyla yüklemeli ve belge bilgisi döndürmeli") | |
| void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { | |
| // ARRANGE | |
| doAnswer(invocation -> { | |
| ((Runnable) invocation.getArgument(0)).run(); | |
| return null; | |
| }).when(executorService).execute(any(Runnable.class)); | |
| when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | |
| .thenReturn(PutObjectResponse.builder().build()); | |
| // ACT | |
| CompletableFuture<StoredDocumentInfo> future = | |
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | |
| testLogContext, InvoiceFormat.UBL); | |
| StoredDocumentInfo result = future.join(); | |
| // ASSERT | |
| assertThat(result).isNotNull(); | |
| assertThat(result.getPath()).isNotBlank(); | |
| assertThat(result.getSize()).isEqualTo(1024L); | |
| assertThat(result.getUploadedAt()).isNotNull(); | |
| verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); | |
| } | |
| @Test | |
| @DisplayName("S3 yükleme başarısızlığını ele almalı") | |
| void givenS3Failure_whenUpload_thenCompletableFutureFails() { | |
| // ARRANGE | |
| doAnswer(invocation -> { | |
| ((Runnable) invocation.getArgument(0)).run(); | |
| return null; | |
| }).when(executorService).execute(any(Runnable.class)); | |
| when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | |
| .thenThrow(new StorageException("S3 unavailable")); | |
| // ACT | |
| CompletableFuture<StoredDocumentInfo> future = | |
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | |
| testLogContext, InvoiceFormat.UBL); | |
| // ASSERT | |
| assertThatThrownBy(() -> future.join()) | |
| .isInstanceOf(CompletionException.class) | |
| .hasCauseInstanceOf(StorageException.class) | |
| .hasMessageContaining("S3 unavailable"); | |
| } | |
| @Test | |
| @DisplayName("LogContext'i async işleme yaymalı") | |
| void givenLogContext_whenUpload_thenContextPropagated() throws Exception { | |
| // ARRANGE | |
| AtomicReference<LogContext> capturedContext = new AtomicReference<>(); | |
| doAnswer(invocation -> { | |
| capturedContext.set(CustomLog.getCurrentContext()); | |
| ((Runnable) invocation.getArgument(0)).run(); | |
| return null; | |
| }).when(executorService).execute(any(Runnable.class)); | |
| // ACT | |
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | |
| testLogContext, InvoiceFormat.UBL).join(); | |
| // ASSERT | |
| assertThat(capturedContext.get()).isNotNull(); | |
| assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); | |
| } | |
| } | |
| } | |
| ``` | |
| ## Resource Katmanı Testleri (REST Assured) | |
| ```java | |
| @QuarkusTest | |
| @DisplayName("DocumentResource API Tests") | |
| class DocumentResourceTest { | |
| @InjectMock | |
| DocumentService documentService; | |
| @Nested | |
| @DisplayName("GET /api/documents için testler") | |
| class ListDocuments { | |
| @Test | |
| @DisplayName("Belge listesini döndürmeli") | |
| void givenDocumentsExist_whenList_thenReturnsOk() { | |
| // ARRANGE | |
| List<Document> documents = List.of(createDocument(1L, "DOC-001")); | |
| when(documentService.list(0, 20)).thenReturn(documents); | |
| // ACT & ASSERT | |
| given() | |
| .when().get("/api/documents") | |
| .then() | |
| .statusCode(200) | |
| .body("$.size()", is(1)) | |
| .body("[0].referenceNumber", equalTo("DOC-001")); | |
| } | |
| } | |
| @Nested | |
| @DisplayName("POST /api/documents için testler") | |
| class CreateDocument { | |
| @Test | |
| @DisplayName("Belge oluşturmalı ve 201 döndürmeli") | |
| void givenValidRequest_whenCreate_thenReturns201() { | |
| // ARRANGE | |
| Document document = createDocument(1L, "DOC-001"); | |
| when(documentService.create(any())).thenReturn(document); | |
| // ACT & ASSERT | |
| given() | |
| .contentType(ContentType.JSON) | |
| .body(""" | |
| { | |
| "referenceNumber": "DOC-001", | |
| "description": "Test document", | |
| "validUntil": "2030-01-01T00:00:00Z", | |
| "categories": ["test"] | |
| } | |
| """) | |
| .when().post("/api/documents") | |
| .then() | |
| .statusCode(201) | |
| .header("Location", containsString("/api/documents/1")) | |
| .body("referenceNumber", equalTo("DOC-001")); | |
| } | |
| @Test | |
| @DisplayName("Geçersiz girdi için 400 döndürmeli") | |
| void givenInvalidRequest_whenCreate_thenReturns400() { | |
| // ACT & ASSERT | |
| given() | |
| .contentType(ContentType.JSON) | |
| .body(""" | |
| { | |
| "referenceNumber": "", | |
| "description": "Test" | |
| } | |
| """) | |
| .when().post("/api/documents") | |
| .then() | |
| .statusCode(400); | |
| } | |
| } | |
| private Document createDocument(Long id, String referenceNumber) { | |
| Document document = new Document(); | |
| document.setId(id); | |
| document.setReferenceNumber(referenceNumber); | |
| document.setStatus(DocumentStatus.PENDING); | |
| return document; | |
| } | |
| } | |
| ``` | |
| ## Gerçek Veritabanıyla Entegrasyon Testleri | |
| ```java | |
| @QuarkusTest | |
| @TestProfile(IntegrationTestProfile.class) | |
| @DisplayName("Document Integration Tests") | |
| class DocumentIntegrationTest { | |
| @Test | |
| @Transactional | |
| @DisplayName("Belge oluşturulmalı ve API üzerinden alınabilmeli") | |
| void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() { | |
| // ACT - API üzerinden oluştur | |
| Long id = given() | |
| .contentType(ContentType.JSON) | |
| .body(""" | |
| { | |
| "referenceNumber": "INT-001", | |
| "description": "Integration test", | |
| "validUntil": "2030-01-01T00:00:00Z", | |
| "categories": ["test"] | |
| } | |
| """) | |
| .when().post("/api/documents") | |
| .then() | |
| .statusCode(201) | |
| .extract().path("id"); | |
| // ASSERT - API üzerinden al | |
| given() | |
| .when().get("/api/documents/" + id) | |
| .then() | |
| .statusCode(200) | |
| .body("referenceNumber", equalTo("INT-001")); | |
| } | |
| } | |
| ``` | |
| ## JaCoCo ile Kapsam | |
| ### Maven Yapılandırması (Tam) | |
| ```xml | |
| <plugin> | |
| <groupId>org.jacoco</groupId> | |
| <artifactId>jacoco-maven-plugin</artifactId> | |
| <version>0.8.13</version> | |
| <executions> | |
| <!-- Test yürütmesi için agent'ı hazırla --> | |
| <execution> | |
| <id>prepare-agent</id> | |
| <goals> | |
| <goal>prepare-agent</goal> | |
| </goals> | |
| </execution> | |
| <!-- Kapsam raporu oluştur --> | |
| <execution> | |
| <id>report</id> | |
| <phase>verify</phase> | |
| <goals> | |
| <goal>report</goal> | |
| </goals> | |
| </execution> | |
| <!-- Kapsam eşiklerini zorla --> | |
| <execution> | |
| <id>check</id> | |
| <goals> | |
| <goal>check</goal> | |
| </goals> | |
| <configuration> | |
| <rules> | |
| <rule> | |
| <element>BUNDLE</element> | |
| <limits> | |
| <limit> | |
| <counter>LINE</counter> | |
| <value>COVEREDRATIO</value> | |
| <minimum>0.80</minimum> | |
| </limit> | |
| <limit> | |
| <counter>BRANCH</counter> | |
| <value>COVEREDRATIO</value> | |
| <minimum>0.70</minimum> | |
| </limit> | |
| </limits> | |
| </rule> | |
| </rules> | |
| </configuration> | |
| </execution> | |
| </executions> | |
| </plugin> | |
| ``` | |
| Kapsam ile testleri çalıştırın: | |
| ```bash | |
| mvn clean test | |
| mvn jacoco:report | |
| mvn jacoco:check | |
| # Rapor: target/site/jacoco/index.html | |
| ``` | |
| ## Test Bağımlılıkları | |
| ```xml | |
| <dependencies> | |
| <!-- Quarkus Test --> | |
| <dependency> | |
| <groupId>io.quarkus</groupId> | |
| <artifactId>quarkus-junit5</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| <dependency> | |
| <groupId>io.quarkus</groupId> | |
| <artifactId>quarkus-junit5-mockito</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| <!-- Mockito --> | |
| <dependency> | |
| <groupId>org.mockito</groupId> | |
| <artifactId>mockito-core</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| <!-- AssertJ (JUnit assertion'larına tercih edilir) --> | |
| <dependency> | |
| <groupId>org.assertj</groupId> | |
| <artifactId>assertj-core</artifactId> | |
| <version>3.24.2</version> | |
| <scope>test</scope> | |
| </dependency> | |
| <!-- REST Assured --> | |
| <dependency> | |
| <groupId>io.rest-assured</groupId> | |
| <artifactId>rest-assured</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| <!-- Camel Test --> | |
| <dependency> | |
| <groupId>org.apache.camel.quarkus</groupId> | |
| <artifactId>camel-quarkus-junit5</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| </dependencies> | |
| ``` | |
| ## En İyi Uygulamalar | |
| ### Test Organizasyonu | |
| - Testleri test edilen metoda göre gruplandırmak için `@Nested` sınıflar kullanın | |
| - Raporlarda görünür okunabilir açıklamalar için `@DisplayName` kullanın | |
| - Test metodları için `givenX_whenY_thenZ` isimlendirme kuralını izleyin | |
| - Tekrarı azaltmak için ortak test verisi kurulumunda `@BeforeEach` kullanın | |
| ### Test Yapısı | |
| - Açık yorumlarla AAA desenini izleyin (`// ARRANGE`, `// ACT`, `// ASSERT`) | |
| - Başarı senaryoları için `assertDoesNotThrow` kullanın | |
| - Mesaj doğrulamalı exception senaryoları için `assertThrows` kullanın | |
| - AssertJ `contains()` veya `isEqualTo()` kullanarak exception mesajlarının beklenen değerlerle eşleştiğini doğrulayın | |
| ### Test Kapsamı | |
| - Tüm public metodlar için mutlu yolları test edin | |
| - Null girdi işlemeyi test edin | |
| - Edge case'leri test edin (boş koleksiyonlar, sınır değerleri, negatif ID'ler, boş string'ler) | |
| - Exception senaryolarını kapsamlı biçimde test edin | |
| - Tüm harici bağımlılıkları mock'layın (repository'ler, servisler, Camel endpoint'leri) | |
| - %80+ satır kapsamı, %70+ branch kapsamı hedefleyin | |
| ### Assertion'lar | |
| - Değer kontrolleri için JUnit assertion'ları yerine **AssertJ'yi tercih edin** (`assertThat`) | |
| - Okunabilirlik için akıcı AssertJ API'si kullanın: `assertThat(list).hasSize(3).contains(item)` | |
| - Exception'lar için: JUnit `assertThrows` ile yakalayın, ardından AssertJ ile mesajı doğrulayın | |
| - Fırlatılmayan başarı yolları için: JUnit `assertDoesNotThrow` kullanın | |
| - Koleksiyonlar için: `extracting()`, `filteredOn()`, `containsExactly()` | |
| ### Entegrasyon Testi | |
| - Entegrasyon testleri için `@QuarkusTest` kullanın | |
| - Quarkus testlerinde bağımlılıkları mock'lamak için `@InjectMock` kullanın | |
| - API testi için REST Assured'ı tercih edin | |
| - Test'e özel yapılandırma için `@TestProfile` kullanın | |
| ### Event-Driven Test | |
| - `AdviceWith` ve `MockEndpoint` ile Camel route'larını test edin | |
| - `@CamelQuarkusTest` annotasyonu kullanın (bağımsız Camel testleri kullanıyorsanız) | |
| - Mesaj içeriğini, başlıklarını ve yönlendirme mantığını doğrulayın | |
| - Hata işleme route'larını ayrı ayrı test edin | |
| - Unit testlerde harici sistemleri (RabbitMQ, S3, veritabanları) mock'layın | |
| ### Camel Route Testi | |
| - Mesaj akışını doğrulamak için `MockEndpoint` kullanın | |
| - Test için route'ları değiştirmek üzere `AdviceWith` kullanın (endpoint'leri mock'larla değiştirin) | |
| - Mesaj dönüşümünü ve marshalling'i test edin | |
| - Exception işleme ve dead letter queue'ları test edin | |
| ### Async İşlem Testi | |
| - CompletableFuture başarı ve başarısızlık senaryolarını test edin | |
| - Async tamamlanmayı beklemek için testlerde `.join()` kullanın | |
| - CompletableFuture'dan exception yayılımını test edin | |
| - LogContext yayılımını async işlemlere doğrulayın | |
| ### Performans | |
| - Testleri hızlı ve izole tutun | |
| - Testleri sürekli modda çalıştırın: `mvn quarkus:test` | |
| - Girdi varyasyonları için parametreli testler (`@ParameterizedTest`) kullanın | |
| - Yeniden kullanılabilir test verisi builder'ları veya factory metodları oluşturun | |
| ### Quarkus'a Özgü | |
| - En son LTS sürümünde kalın (Quarkus 3.x) | |
| - Native derleme uyumluluğunu periyodik olarak test edin | |
| - Farklı senaryolar için Quarkus test profillerini kullanın | |
| - Yerel test için Quarkus dev servislerinden yararlanın | |
| - `@MockBean` yerine `@InjectMock` kullanın (Quarkus'a özgü) | |
| ### Doğrulama En İyi Uygulamaları | |
| - Mock'lanmış bağımlılıklardaki etkileşimleri her zaman doğrulayın | |
| - Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `verify(mock, never())` kullanın | |
| - Karmaşık argüman eşleştirme için `argThat()` kullanın | |
| - Önem taşıdığında çağrı sırasını doğrulayın: Mockito'dan `InOrder` | |
| ## Nasıl Çalışır | |
| 1. Önce testleri yazın (başarısız olmalılar) | |
| 2. Geçmek için minimal kod uygulayın | |
| 3. Testleri yeşil tutarken refactor edin | |
| 4. JaCoCo ile kapsamı zorlayın (%80+ hedef) | |
| ## Örnekler | |
| ## `@Nested` Organizasyonlu Unit Testler | |
| Kapsamlı ve okunabilir testler için bu yapılandırılmış yaklaşımı izleyin: | |
🧰 Tools
🪛 LanguageTool
[grammar] ~252-~252: Ensure spelling is correct
Context: ...için never() kullanın ## Camel Route Testi java `@QuarkusTest` `@DisplayName`("Business Rules Camel Route Tests") class BusinessRulesRouteTest { `@Inject` CamelContext camelContext; `@Inject` ProducerTemplate producerTemplate; `@InjectMock` EventService eventService; private BusinessRulesPayload testPayload; `@BeforeEach` void setUp() { // ARRANGE - Test verisi testPayload = new BusinessRulesPayload(); testPayload.setDocumentId(1L); testPayload.setFlowProfile(FlowProfile.BASIC); } `@Nested` `@DisplayName`("business-rules-publisher route için testler") class BusinessRulesPublisher { `@Test` `@DisplayName`("Mesajı başarıyla RabbitMQ'ya yayınlamalı") void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception { // ARRANGE MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); mockRabbitMQ.expectedMessageCount(1); // Test için gerçek endpoint'i mock ile değiştir camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { advice.replaceFromWith("direct:business-rules-publisher"); advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); // ASSERT — .marshal().json() sonrası body JSON String'dir mockRabbitMQ.assertIsSatisfied(5000); assertThat(mockRabbitMQ.getExchanges()).hasSize(1); String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); } `@Test` `@DisplayName`("JSON'a marshalling'i ele almalı") void givenPayload_whenPublish_thenMarshalledToJson() throws Exception { // ARRANGE MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); camelContext.addEndpoint("mock:marshal", mockMarshal); mockMarshal.expectedMessageCount(1); camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { advice.weaveAddLast().to("mock:marshal"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); // ASSERT mockMarshal.assertIsSatisfied(5000); String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); assertThat(body).contains("\"flowProfile\":\"BASIC\""); } } `@Nested` `@DisplayName`("document-processing route için testler") class DocumentProcessing { `@Test` `@DisplayName`("Faturayı doğru işlemciye yönlendirmeli") void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception { // ARRANGE MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); mockInvoice.expectedMessageCount(1); camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); }); camelContext.getRouteController().startRoute("document-processing"); // ACT producerTemplate.sendBodyAndHeader("direct:process-document", testPayload, "documentType", "INVOICE"); // ASSERT mockInvoice.assertIsSatisfied(5000); } `@Test` `@DisplayName`("Validasyon hatalarını zarif şekilde ele almalı") void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception { // ARRANGE MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); mockError.expectedMessageCount(1); camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:validation-error-handler.*") .replace().to("mock:error"); }); camelContext.getRouteController().startRoute("document-processing"); // Validator'ı exception fırlatacak şekilde mock'la when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document")); // ACT producerTemplate.sendBody("direct:process-document", testPayload); // ASSERT mockError.assertIsSatisfied(5000); Exception exception = mockError.getExchanges().get(0).getException(); assertThat(exception).isInstanceOf(ValidationException.class); assertThat(exception.getMessage()).contains("Invalid document"); } } } ## Event Service Testi ```java @ExtendWith...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[grammar] ~390-~390: Ensure spelling is correct
Context: ...nt"); } } } ## Event Service Testi java @ExtendWith(MockitoExtension.class) @DisplayName("EventService Unit Tests") class EventServiceTest { @Mock private EventRepository eventRepository; @Mock private ObjectMapper objectMapper; @InjectMocks private EventService eventService; private BusinessRulesPayload testPayload; @BeforeEach void setUp() { // ARRANGE testPayload = new BusinessRulesPayload(); testPayload.setDocumentId(1L); } @Nested @DisplayName("createSuccessEvent için testler") class CreateSuccessEvent { @Test @DisplayName("Doğru niteliklerle başarı eventi oluşturulmalı") void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { // ARRANGE when(objectMapper.writeValueAsString(testPayload)).thenReturn("{"documentId":1}"); // ACT assertDoesNotThrow(() -> eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); // ASSERT verify(eventRepository).persist(argThat(event -> event.getType().equals("DOCUMENT_PROCESSED") && event.getStatus() == EventStatus.SUCCESS && event.getPayload().equals("{"documentId":1}") && event.getTimestamp() != null )); } @Test @DisplayName("Payload null olduğunda exception fırlatılmalı") void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { // ARRANGE Object nullPayload = null; // ACT & ASSERT IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") ); assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); verify(eventRepository, never()).persist(any()); } } @Nested @DisplayName("createErrorEvent için testler") class CreateErrorEvent { @Test @DisplayName("Hata mesajıyla hata eventi oluşturulmalı") void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { // ARRANGE String errorMessage = "Processing failed"; when(objectMapper.writeValueAsString(testPayload)).thenReturn("{"documentId":1}"); // ACT assertDoesNotThrow(() -> eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); // ASSERT verify(eventRepository).persist(argThat(event -> event.getType().equals("PROCESSING_ERROR") && event.getStatus() == EventStatus.ERROR && event.getErrorMessage().equals(errorMessage) && event.getPayload().equals("{"documentId":1}") )); } @ParameterizedTest @DisplayName("Geçersiz hata mesajları reddedilmeli") @ValueSource(strings = {"", " "}) void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) { // ACT & ASSERT IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) ); assertThat(exception.getMessage()).contains("Error message cannot be blank"); } } } ## CompletableFuture Testi java @Extend...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[grammar] ~495-~495: Ensure spelling is correct
Context: ...; } } } ## CompletableFuture Testi java @ExtendWith(MockitoExtension.class) @DisplayName("FileStorageService Unit Tests") class FileStorageServiceTest { @Mock private S3Client s3Client; @Mock private ExecutorService executorService; @InjectMocks private FileStorageService fileStorageService; private InputStream testInputStream; private LogContext testLogContext; @BeforeEach void setUp() { // ARRANGE testInputStream = new ByteArrayInputStream("test content".getBytes()); testLogContext = new LogContext(); testLogContext.put("traceId", "trace-123"); } @Nested @DisplayName("uploadOriginalFile için testler") class UploadOriginalFile { @Test @DisplayName("Dosyayı başarıyla yüklemeli ve belge bilgisi döndürmeli") void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { // ARRANGE doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenReturn(PutObjectResponse.builder().build()); // ACT CompletableFuture future = fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); StoredDocumentInfo result = future.join(); // ASSERT assertThat(result).isNotNull(); assertThat(result.getPath()).isNotBlank(); assertThat(result.getSize()).isEqualTo(1024L); assertThat(result.getUploadedAt()).isNotNull(); verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); } @Test @DisplayName("S3 yükleme başarısızlığını ele almalı") void givenS3Failure_whenUpload_thenCompletableFutureFails() { // ARRANGE doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenThrow(new StorageException("S3 unavailable")); // ACT CompletableFuture future = fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); // ASSERT assertThatThrownBy(() -> future.join()) .isInstanceOf(CompletionException.class) .hasCauseInstanceOf(StorageException.class) .hasMessageContaining("S3 unavailable"); } @Test @DisplayName("LogContext'i async işleme yaymalı") void givenLogContext_whenUpload_thenContextPropagated() throws Exception { // ARRANGE AtomicReference capturedContext = new AtomicReference<>(); doAnswer(invocation -> { capturedContext.set(CustomLog.getCurrentContext()); ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); // ACT fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL).join(); // ASSERT assertThat(capturedContext.get()).isNotNull(); assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); } } } ``` ## Resource Katmanı Testleri (REST Assured)...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[grammar] ~895-~895: Ensure spelling is correct
Context: ...ync işlemlere doğrulayın ### Performans - Testleri hızlı ve izole tutun - Testleri sürekli...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/tr/skills/quarkus-tdd/SKILL.md` around lines 22 - 912, Rename the "## İş
Akışı" heading to "## Nasıl Çalışır" and move all code blocks and example
sections (the large Java code examples and Camel/REST/Integration snippets shown
under the current content) into a new "## Örnekler" section; ensure the
top-level Markdown includes the required sections "When to Use", "How It Works"
(use the renamed "Nasıl Çalışır"), and "Examples" (## Örnekler) and update any
internal references to the old "İş Akışı" heading so examples (all fenced code
blocks and related subsections like "@Nested Organizasyonlu Unit Testler",
"Camel Route Testi", "Event Service Testi", etc.) are grouped under the new
Examples heading.
| @Test | ||
| @DisplayName("Validasyon hatalarını zarif şekilde ele almalı") | ||
| void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); | ||
| mockError.expectedMessageCount(1); | ||
|
|
||
| camelContext.getRouteController().stopRoute("document-processing"); | ||
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | ||
| advice.weaveByToString(".*direct:validation-error-handler.*") | ||
| .replace().to("mock:error"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("document-processing"); | ||
|
|
||
| // Validator'ı exception fırlatacak şekilde mock'la | ||
| when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document")); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBody("direct:process-document", testPayload); | ||
|
|
||
| // ASSERT | ||
| mockError.assertIsSatisfied(5000); | ||
|
|
||
| Exception exception = mockError.getExchanges().get(0).getException(); | ||
| assertThat(exception).isInstanceOf(ValidationException.class); | ||
| assertThat(exception.getMessage()).contains("Invalid document"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Test örneğinde tanımsız metod referansı.
Satır 374'te eventService.validate(any()) çağrılıyor ancak EventService'de böyle bir metod tanımlı değil. Diğer örneklere göre EventService'in createSuccessEvent ve createErrorEvent metodları var, validate metodu yok. Bu derleme hatası verecektir.
🔧 Önerilen düzeltme
Seçenek 1: Eksik mock'ı ekleyin:
`@InjectMock`
EventService eventService;
+@InjectMock
+DocumentValidator documentValidator;
+
private BusinessRulesPayload testPayload;Ve satır 374'ü güncelleyin:
- when(eventService.validate(any())).thenThrow(new ValidationException("Invalid document"));
+ when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));Seçenek 2: Camel route'un kendi validation mantığını test edin (mock olmadan).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/tr/skills/quarkus-tdd/SKILL.md` around lines 359 - 386, The test
givenValidationError_whenProcess_thenRoutesToErrorHandler references a
non-existent eventService.validate(...) method; fix by either (A) changing the
mocked call to use an existing EventService method such as createErrorEvent (or
createSuccessEvent) so that when(eventService.createErrorEvent(...)) throws new
ValidationException(...) the route will exercise the error path, or (B) add a
validate(...) signature to the EventService interface/class and implement it so
the test can mock eventService.validate(...) to throw ValidationException;
update the test to mock the chosen existing symbol
(eventService.createErrorEvent or the newly added eventService.validate)
accordingly.
| @Test | ||
| @DisplayName("Payload null olduğunda exception fırlatılmalı") | ||
| void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { | ||
| // ARRANGE | ||
| Object nullPayload = null; | ||
|
|
||
| // ACT & ASSERT | ||
| IllegalArgumentException exception = assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") | ||
| ); | ||
|
|
||
| assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); | ||
| verify(eventRepository, never()).persist(any()); | ||
| } | ||
| } |
There was a problem hiding this comment.
İngilizce versiyonla tutarsız exception tipi.
Satır 445'te null payload için IllegalArgumentException beklenirken, İngilizce versiyonda (skills/quarkus-tdd/SKILL.md satır 448) aynı senaryo için NullPointerException bekleniyor. Yerelleştirmeler arasında tutarlılık için aynı exception tipi kullanılmalı.
🔄 Tutarlılık için düzeltme
İngilizce versiyonla eşleştirmek için:
- IllegalArgumentException exception = assertThrows(
- IllegalArgumentException.class,
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
);Veya her iki versiyonu da IllegalArgumentException kullanacak şekilde güncelleyin (daha semantik olarak doğru).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Test | |
| @DisplayName("Payload null olduğunda exception fırlatılmalı") | |
| void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { | |
| // ARRANGE | |
| Object nullPayload = null; | |
| // ACT & ASSERT | |
| IllegalArgumentException exception = assertThrows( | |
| IllegalArgumentException.class, | |
| () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") | |
| ); | |
| assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); | |
| verify(eventRepository, never()).persist(any()); | |
| } | |
| } | |
| `@Test` | |
| `@DisplayName`("Payload null olduğunda exception fırlatılmalı") | |
| void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { | |
| // ARRANGE | |
| Object nullPayload = null; | |
| // ACT & ASSERT | |
| NullPointerException exception = assertThrows( | |
| NullPointerException.class, | |
| () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") | |
| ); | |
| assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); | |
| verify(eventRepository, never()).persist(any()); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/tr/skills/quarkus-tdd/SKILL.md` around lines 438 - 453, The Turkish test
givenNullPayload_whenCreateSuccessEvent_thenThrowsException asserts
IllegalArgumentException for eventService.createSuccessEvent(nullPayload,
"EVENT_TYPE") but the English version expects NullPointerException; make them
consistent by updating this test to expect NullPointerException (i.e., change
the assertThrows() expected type to NullPointerException) or alternatively
update both language versions to expect IllegalArgumentException—ensure the
change targets the test method
givenNullPayload_whenCreateSuccessEvent_thenThrowsException and the call to
eventService.createSuccessEvent so both localized tests assert the same
exception type.
| ## Workflow | ||
|
|
||
| 1. Write tests first (they should fail) | ||
| 2. Implement minimal code to pass | ||
| 3. Refactor with tests green | ||
| 4. Enforce coverage with JaCoCo (80%+ target) | ||
|
|
||
| ## Unit Tests with @Nested Organization | ||
|
|
||
| Follow this structured approach for comprehensive, readable tests: | ||
|
|
||
| ```java | ||
| @ExtendWith(MockitoExtension.class) | ||
| @DisplayName("As2ProcessingService Unit Tests") | ||
| class As2ProcessingServiceTest { | ||
|
|
||
| @Mock | ||
| private InvoiceFlowValidator invoiceFlowValidator; | ||
|
|
||
| @Mock | ||
| private EventService eventService; | ||
|
|
||
| @Mock | ||
| private DocumentJobService documentJobService; | ||
|
|
||
| @Mock | ||
| private BusinessRulesPublisher businessRulesPublisher; | ||
|
|
||
| @Mock | ||
| private FileStorageService fileStorageService; | ||
|
|
||
| @InjectMocks | ||
| private As2ProcessingService as2ProcessingService; | ||
|
|
||
| private Path testFilePath; | ||
| private LogContext testLogContext; | ||
| private InvoiceValidationResult validationResult; | ||
| private StoredDocumentInfo documentInfo; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| // ARRANGE - Common test data | ||
| testFilePath = Path.of("/tmp/test-invoice.xml"); | ||
|
|
||
| testLogContext = new LogContext(); | ||
| testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001"); | ||
| testLogContext.put(As2Constants.FILE_NAME, "invoice.xml"); | ||
| testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001"); | ||
|
|
||
| validationResult = new InvoiceValidationResult(); | ||
| validationResult.setValid(true); | ||
| validationResult.setSize(1024L); | ||
| validationResult.setDocumentHash("abc123"); | ||
|
|
||
| documentInfo = new StoredDocumentInfo(); | ||
| documentInfo.setPath("s3://bucket/path/invoice.xml"); | ||
| documentInfo.setSize(1024L); | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("Tests for processFile") | ||
| class ProcessFile { | ||
|
|
||
| @Test | ||
| @DisplayName("Should successfully process non-CHORUS file with all validations") | ||
| void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception { | ||
| // ARRANGE | ||
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | ||
| CustomLog.setCurrentContext(testLogContext); | ||
|
|
||
| when(invoiceFlowValidator.validateFlowWithConfig( | ||
| eq(testFilePath), | ||
| eq(ValidationFlowConfig.allValidations()), | ||
| eq(EInvoiceSyntaxFormat.UBL), | ||
| any(LogContext.class))) | ||
| .thenReturn(validationResult); | ||
|
|
||
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | ||
| .thenReturn(FlowProfile.BASIC); | ||
|
|
||
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | ||
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | ||
|
|
||
| when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any())) | ||
| .thenReturn(new BusinessRulesPayload()); | ||
|
|
||
| // ACT | ||
| assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); | ||
|
|
||
| // ASSERT | ||
| verify(invoiceFlowValidator).validateFlowWithConfig( | ||
| eq(testFilePath), | ||
| eq(ValidationFlowConfig.allValidations()), | ||
| eq(EInvoiceSyntaxFormat.UBL), | ||
| any(LogContext.class)); | ||
|
|
||
| verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), | ||
| eq("PERSISTENCE_BLOB_EVENT_TYPE")); | ||
| verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class), | ||
| eq("BUSINESS_RULES_MESSAGE_SENT")); | ||
| verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class)); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should bypass schematron validation for CHORUS_FLOW") | ||
| void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception { | ||
| // ARRANGE | ||
| testLogContext.put(As2Constants.CHORUS_FLOW, "true"); | ||
| CustomLog.setCurrentContext(testLogContext); | ||
|
|
||
| when(invoiceFlowValidator.validateFlowWithConfig( | ||
| eq(testFilePath), | ||
| eq(ValidationFlowConfig.xsdOnly()), | ||
| eq(EInvoiceSyntaxFormat.UBL), | ||
| any(LogContext.class))) | ||
| .thenReturn(validationResult); | ||
|
|
||
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | ||
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | ||
|
|
||
| when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), | ||
| eq(FlowProfile.EXTENDED_CTC_FR), any())) | ||
| .thenReturn(new BusinessRulesPayload()); | ||
|
|
||
| // ACT | ||
| assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); | ||
|
|
||
| // ASSERT | ||
| verify(invoiceFlowValidator).validateFlowWithConfig( | ||
| eq(testFilePath), | ||
| eq(ValidationFlowConfig.xsdOnly()), | ||
| eq(EInvoiceSyntaxFormat.UBL), | ||
| any(LogContext.class)); | ||
|
|
||
| verify(documentJobService).createDocumentAndJobEntities( | ||
| any(), any(), any(), | ||
| eq(FlowProfile.EXTENDED_CTC_FR), | ||
| any()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should create error event when file upload fails") | ||
| void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception { | ||
| // ARRANGE | ||
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | ||
| CustomLog.setCurrentContext(testLogContext); | ||
|
|
||
| when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) | ||
| .thenReturn(validationResult); | ||
|
|
||
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | ||
| .thenReturn(FlowProfile.BASIC); | ||
|
|
||
| documentInfo.setPath(""); // Blank path triggers error | ||
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | ||
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | ||
|
|
||
| // ACT & ASSERT | ||
| As2ServerProcessingException exception = assertThrows( | ||
| As2ServerProcessingException.class, | ||
| () -> as2ProcessingService.processFile(testFilePath) | ||
| ); | ||
|
|
||
| assertThat(exception.getMessage()) | ||
| .contains("File path is empty after upload"); | ||
|
|
||
| verify(eventService).createErrorEvent( | ||
| eq(documentInfo), | ||
| eq("FILE_UPLOAD_FAILED"), | ||
| contains("File path is empty")); | ||
|
|
||
| verify(businessRulesPublisher, never()).publishAsync(any()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should handle CompletableFuture.join() failure") | ||
| void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception { | ||
| // ARRANGE | ||
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | ||
| CustomLog.setCurrentContext(testLogContext); | ||
|
|
||
| when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) | ||
| .thenReturn(validationResult); | ||
|
|
||
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | ||
| .thenReturn(FlowProfile.BASIC); | ||
|
|
||
| CompletableFuture<StoredDocumentInfo> failedFuture = | ||
| CompletableFuture.failedFuture(new StorageException("S3 connection failed")); | ||
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | ||
| .thenReturn(failedFuture); | ||
|
|
||
| // ACT & ASSERT | ||
| assertThrows( | ||
| CompletionException.class, | ||
| () -> as2ProcessingService.processFile(testFilePath) | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should throw exception when file path is null") | ||
| void givenNullFilePath_whenProcessFile_thenThrowsException() { | ||
| // ARRANGE | ||
| Path nullPath = null; | ||
|
|
||
| // ACT & ASSERT | ||
| NullPointerException exception = assertThrows( | ||
| NullPointerException.class, | ||
| () -> as2ProcessingService.processFile(nullPath) | ||
| ); | ||
|
|
||
| verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any()); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Key Testing Patterns | ||
|
|
||
| 1. **@Nested Classes**: Group tests by method being tested | ||
| 2. **@DisplayName**: Provide readable test descriptions for test reports | ||
| 3. **Naming Convention**: `givenX_whenY_thenZ` for clarity | ||
| 4. **AAA Pattern**: Explicit `// ARRANGE`, `// ACT`, `// ASSERT` comments | ||
| 5. **@BeforeEach**: Setup common test data to reduce duplication | ||
| 6. **assertDoesNotThrow**: Test success scenarios without catching exceptions | ||
| 7. **assertThrows**: Test exception scenarios with message validation using AssertJ | ||
| 8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions | ||
| 9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly | ||
| 10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios | ||
|
|
||
| ## Testing Camel Routes | ||
|
|
||
| ```java | ||
| @QuarkusTest | ||
| @DisplayName("Business Rules Camel Route Tests") | ||
| class BusinessRulesRouteTest { | ||
|
|
||
| @Inject | ||
| CamelContext camelContext; | ||
|
|
||
| @Inject | ||
| ProducerTemplate producerTemplate; | ||
|
|
||
| @InjectMock | ||
| EventService eventService; | ||
|
|
||
| @InjectMock | ||
| DocumentValidator documentValidator; | ||
|
|
||
| private BusinessRulesPayload testPayload; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| // ARRANGE - Test data | ||
| testPayload = new BusinessRulesPayload(); | ||
| testPayload.setDocumentId(1L); | ||
| testPayload.setFlowProfile(FlowProfile.BASIC); | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("Tests for business-rules-publisher route") | ||
| class BusinessRulesPublisher { | ||
|
|
||
| @Test | ||
| @DisplayName("Should successfully publish message to RabbitMQ") | ||
| void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); | ||
| mockRabbitMQ.expectedMessageCount(1); | ||
|
|
||
| // Replace real endpoint with mock for testing | ||
| camelContext.getRouteController().stopRoute("business-rules-publisher"); | ||
| AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { | ||
| advice.replaceFromWith("direct:business-rules-publisher"); | ||
| advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("business-rules-publisher"); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBody("direct:business-rules-publisher", testPayload); | ||
|
|
||
| // ASSERT — body is a JSON String after .marshal().json(JsonLibrary.Jackson) | ||
| mockRabbitMQ.assertIsSatisfied(5000); | ||
|
|
||
| assertThat(mockRabbitMQ.getExchanges()).hasSize(1); | ||
| String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); | ||
| assertThat(body).contains("\"documentId\":1"); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should handle marshalling to JSON") | ||
| void givenPayload_whenPublish_thenMarshalledToJson() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); | ||
| camelContext.addEndpoint("mock:marshal", mockMarshal); | ||
| mockMarshal.expectedMessageCount(1); | ||
|
|
||
| camelContext.getRouteController().stopRoute("business-rules-publisher"); | ||
| AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { | ||
| advice.weaveAddLast().to("mock:marshal"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("business-rules-publisher"); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBody("direct:business-rules-publisher", testPayload); | ||
|
|
||
| // ASSERT | ||
| mockMarshal.assertIsSatisfied(5000); | ||
|
|
||
| String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); | ||
| assertThat(body).contains("\"documentId\":1"); | ||
| assertThat(body).contains("\"flowProfile\":\"BASIC\""); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("Tests for document-processing route") | ||
| class DocumentProcessing { | ||
|
|
||
| @Test | ||
| @DisplayName("Should route invoice to correct processor") | ||
| void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); | ||
| mockInvoice.expectedMessageCount(1); | ||
|
|
||
| camelContext.getRouteController().stopRoute("document-processing"); | ||
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | ||
| advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("document-processing"); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBodyAndHeader("direct:process-document", | ||
| testPayload, "documentType", "INVOICE"); | ||
|
|
||
| // ASSERT | ||
| mockInvoice.assertIsSatisfied(5000); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should handle validation errors gracefully") | ||
| void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); | ||
| mockError.expectedMessageCount(1); | ||
|
|
||
| camelContext.getRouteController().stopRoute("document-processing"); | ||
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | ||
| advice.weaveByToString(".*direct:validation-error-handler.*") | ||
| .replace().to("mock:error"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("document-processing"); | ||
|
|
||
| // Mock validator bean to throw exception | ||
| when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document")); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBody("direct:process-document", testPayload); | ||
|
|
||
| // ASSERT | ||
| mockError.assertIsSatisfied(5000); | ||
|
|
||
| Exception exception = mockError.getExchanges().get(0).getException(); | ||
| assertThat(exception).isInstanceOf(ValidationException.class); | ||
| assertThat(exception.getMessage()).contains("Invalid document"); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Testing Event Services | ||
|
|
||
| ```java | ||
| @ExtendWith(MockitoExtension.class) | ||
| @DisplayName("EventService Unit Tests") | ||
| class EventServiceTest { | ||
|
|
||
| @Mock | ||
| private EventRepository eventRepository; | ||
|
|
||
| @Mock | ||
| private ObjectMapper objectMapper; | ||
|
|
||
| @InjectMocks | ||
| private EventService eventService; | ||
|
|
||
| private BusinessRulesPayload testPayload; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| // ARRANGE | ||
| testPayload = new BusinessRulesPayload(); | ||
| testPayload.setDocumentId(1L); | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("Tests for createSuccessEvent") | ||
| class CreateSuccessEvent { | ||
|
|
||
| @Test | ||
| @DisplayName("Should create success event with correct attributes") | ||
| void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { | ||
| // ARRANGE | ||
| when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); | ||
|
|
||
| // ACT | ||
| assertDoesNotThrow(() -> | ||
| eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); | ||
|
|
||
| // ASSERT | ||
| verify(eventRepository).persist(argThat(event -> | ||
| event.getType().equals("DOCUMENT_PROCESSED") && | ||
| event.getStatus() == EventStatus.SUCCESS && | ||
| event.getPayload().equals("{\"documentId\":1}") && | ||
| event.getTimestamp() != null | ||
| )); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should throw exception when payload is null") | ||
| void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { | ||
| // ARRANGE | ||
| Object nullPayload = null; | ||
|
|
||
| // ACT & ASSERT | ||
| NullPointerException exception = assertThrows( | ||
| NullPointerException.class, | ||
| () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") | ||
| ); | ||
|
|
||
| assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); | ||
| verify(eventRepository, never()).persist(any()); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("Tests for createErrorEvent") | ||
| class CreateErrorEvent { | ||
|
|
||
| @Test | ||
| @DisplayName("Should create error event with error message") | ||
| void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { | ||
| // ARRANGE | ||
| String errorMessage = "Processing failed"; | ||
| when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); | ||
|
|
||
| // ACT | ||
| assertDoesNotThrow(() -> | ||
| eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); | ||
|
|
||
| // ASSERT | ||
| verify(eventRepository).persist(argThat(event -> | ||
| event.getType().equals("PROCESSING_ERROR") && | ||
| event.getStatus() == EventStatus.ERROR && | ||
| event.getErrorMessage().equals(errorMessage) && | ||
| event.getPayload().equals("{\"documentId\":1}") | ||
| )); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @DisplayName("Should reject invalid error messages") | ||
| @ValueSource(strings = {"", " "}) | ||
| void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) { | ||
| // ACT & ASSERT | ||
| IllegalArgumentException exception = assertThrows( | ||
| IllegalArgumentException.class, | ||
| () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) | ||
| ); | ||
|
|
||
| assertThat(exception.getMessage()).contains("Error message cannot be blank"); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Testing CompletableFuture | ||
|
|
||
| ```java | ||
| @ExtendWith(MockitoExtension.class) | ||
| @DisplayName("FileStorageService Unit Tests") | ||
| class FileStorageServiceTest { | ||
|
|
||
| @Mock | ||
| private S3Client s3Client; | ||
|
|
||
| @Mock | ||
| private ExecutorService executorService; | ||
|
|
||
| @InjectMocks | ||
| private FileStorageService fileStorageService; | ||
|
|
||
| private InputStream testInputStream; | ||
| private LogContext testLogContext; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| // ARRANGE | ||
| testInputStream = new ByteArrayInputStream("test content".getBytes()); | ||
| testLogContext = new LogContext(); | ||
| testLogContext.put("traceId", "trace-123"); | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("Tests for uploadOriginalFile") | ||
| class UploadOriginalFile { | ||
|
|
||
| @Test | ||
| @DisplayName("Should successfully upload file and return document info") | ||
| void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { | ||
| // ARRANGE | ||
| doAnswer(invocation -> { | ||
| ((Runnable) invocation.getArgument(0)).run(); | ||
| return null; | ||
| }).when(executorService).execute(any(Runnable.class)); | ||
|
|
||
| when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | ||
| .thenReturn(PutObjectResponse.builder().build()); | ||
|
|
||
| // ACT | ||
| CompletableFuture<StoredDocumentInfo> future = | ||
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | ||
| testLogContext, InvoiceFormat.UBL); | ||
|
|
||
| StoredDocumentInfo result = future.join(); | ||
|
|
||
| // ASSERT | ||
| assertThat(result).isNotNull(); | ||
| assertThat(result.getPath()).isNotBlank(); | ||
| assertThat(result.getSize()).isEqualTo(1024L); | ||
| assertThat(result.getUploadedAt()).isNotNull(); | ||
|
|
||
| verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should handle S3 upload failure") | ||
| void givenS3Failure_whenUpload_thenCompletableFutureFails() { | ||
| // ARRANGE — run synchronously so exception propagates through the future | ||
| doAnswer(invocation -> { | ||
| ((Runnable) invocation.getArgument(0)).run(); | ||
| return null; | ||
| }).when(executorService).execute(any(Runnable.class)); | ||
|
|
||
| when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | ||
| .thenThrow(new StorageException("S3 unavailable")); | ||
|
|
||
| // ACT | ||
| CompletableFuture<StoredDocumentInfo> future = | ||
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | ||
| testLogContext, InvoiceFormat.UBL); | ||
|
|
||
| // ASSERT | ||
| assertThatThrownBy(() -> future.join()) | ||
| .isInstanceOf(CompletionException.class) | ||
| .hasCauseInstanceOf(StorageException.class) | ||
| .hasMessageContaining("S3 unavailable"); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should propagate LogContext to async operation") | ||
| void givenLogContext_whenUpload_thenContextPropagated() throws Exception { | ||
| // ARRANGE | ||
| AtomicReference<LogContext> capturedContext = new AtomicReference<>(); | ||
|
|
||
| doAnswer(invocation -> { | ||
| capturedContext.set(CustomLog.getCurrentContext()); | ||
| ((Runnable) invocation.getArgument(0)).run(); | ||
| return null; | ||
| }).when(executorService).execute(any(Runnable.class)); | ||
|
|
||
| // ACT | ||
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | ||
| testLogContext, InvoiceFormat.UBL).join(); | ||
|
|
||
| // ASSERT | ||
| assertThat(capturedContext.get()).isNotNull(); | ||
| assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Resource Layer Tests (REST Assured) | ||
|
|
||
| ```java | ||
| @QuarkusTest | ||
| @DisplayName("DocumentResource API Tests") | ||
| class DocumentResourceTest { | ||
|
|
||
| @InjectMock | ||
| DocumentService documentService; | ||
|
|
||
| @Nested | ||
| @DisplayName("Tests for GET /api/documents") | ||
| class ListDocuments { | ||
|
|
||
| @Test | ||
| @DisplayName("Should return list of documents") | ||
| void givenDocumentsExist_whenList_thenReturnsOk() { | ||
| // ARRANGE | ||
| List<Document> documents = List.of(createDocument(1L, "DOC-001")); | ||
| when(documentService.list(0, 20)).thenReturn(documents); | ||
|
|
||
| // ACT & ASSERT | ||
| given() | ||
| .when().get("/api/documents") | ||
| .then() | ||
| .statusCode(200) | ||
| .body("$.size()", is(1)) | ||
| .body("[0].referenceNumber", equalTo("DOC-001")); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("Tests for POST /api/documents") | ||
| class CreateDocument { | ||
|
|
||
| @Test | ||
| @DisplayName("Should create document and return 201") | ||
| void givenValidRequest_whenCreate_thenReturns201() { | ||
| // ARRANGE | ||
| Document document = createDocument(1L, "DOC-001"); | ||
| when(documentService.create(any())).thenReturn(document); | ||
|
|
||
| // ACT & ASSERT | ||
| given() | ||
| .contentType(ContentType.JSON) | ||
| .body(""" | ||
| { | ||
| "referenceNumber": "DOC-001", | ||
| "description": "Test document", | ||
| "validUntil": "2030-01-01T00:00:00Z", | ||
| "categories": ["test"] | ||
| } | ||
| """) | ||
| .when().post("/api/documents") | ||
| .then() | ||
| .statusCode(201) | ||
| .header("Location", containsString("/api/documents/1")) | ||
| .body("referenceNumber", equalTo("DOC-001")); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should return 400 for invalid input") | ||
| void givenInvalidRequest_whenCreate_thenReturns400() { | ||
| // ACT & ASSERT | ||
| given() | ||
| .contentType(ContentType.JSON) | ||
| .body(""" | ||
| { | ||
| "referenceNumber": "", | ||
| "description": "Test" | ||
| } | ||
| """) | ||
| .when().post("/api/documents") | ||
| .then() | ||
| .statusCode(400); | ||
| } | ||
| } | ||
|
|
||
| private Document createDocument(Long id, String referenceNumber) { | ||
| Document document = new Document(); | ||
| document.setId(id); | ||
| document.setReferenceNumber(referenceNumber); | ||
| document.setStatus(DocumentStatus.PENDING); | ||
| return document; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Integration Tests with Real Database | ||
|
|
||
| ```java | ||
| @QuarkusTest | ||
| @TestProfile(IntegrationTestProfile.class) | ||
| @DisplayName("Document Integration Tests") | ||
| class DocumentIntegrationTest { | ||
|
|
||
| @Test | ||
| @Transactional | ||
| @DisplayName("Should create and retrieve document via API") | ||
| void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() { | ||
| // ACT - Create via API | ||
| Long id = given() | ||
| .contentType(ContentType.JSON) | ||
| .body(""" | ||
| { | ||
| "referenceNumber": "INT-001", | ||
| "description": "Integration test", | ||
| "validUntil": "2030-01-01T00:00:00Z", | ||
| "categories": ["test"] | ||
| } | ||
| """) | ||
| .when().post("/api/documents") | ||
| .then() | ||
| .statusCode(201) | ||
| .extract().path("id"); | ||
|
|
||
| // ASSERT - Retrieve via API | ||
| given() | ||
| .when().get("/api/documents/" + id) | ||
| .then() | ||
| .statusCode(200) | ||
| .body("referenceNumber", equalTo("INT-001")); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Coverage with JaCoCo | ||
|
|
||
| ### Maven Configuration (Complete) | ||
|
|
||
| ```xml | ||
| <plugin> | ||
| <groupId>org.jacoco</groupId> | ||
| <artifactId>jacoco-maven-plugin</artifactId> | ||
| <version>0.8.13</version> | ||
| <executions> | ||
| <!-- Prepare agent for test execution --> | ||
| <execution> | ||
| <id>prepare-agent</id> | ||
| <goals> | ||
| <goal>prepare-agent</goal> | ||
| </goals> | ||
| </execution> | ||
|
|
||
| <!-- Generate coverage report --> | ||
| <execution> | ||
| <id>report</id> | ||
| <phase>verify</phase> | ||
| <goals> | ||
| <goal>report</goal> | ||
| </goals> | ||
| </execution> | ||
|
|
||
| <!-- Enforce coverage thresholds --> | ||
| <execution> | ||
| <id>check</id> | ||
| <goals> | ||
| <goal>check</goal> | ||
| </goals> | ||
| <configuration> | ||
| <rules> | ||
| <rule> | ||
| <element>BUNDLE</element> | ||
| <limits> | ||
| <limit> | ||
| <counter>LINE</counter> | ||
| <value>COVEREDRATIO</value> | ||
| <minimum>0.80</minimum> | ||
| </limit> | ||
| <limit> | ||
| <counter>BRANCH</counter> | ||
| <value>COVEREDRATIO</value> | ||
| <minimum>0.70</minimum> | ||
| </limit> | ||
| </limits> | ||
| </rule> | ||
| </rules> | ||
| </configuration> | ||
| </execution> | ||
| </executions> | ||
| </plugin> | ||
| ``` | ||
|
|
||
| Run tests with coverage: | ||
| ```bash | ||
| mvn clean test | ||
| mvn jacoco:report | ||
| mvn jacoco:check | ||
|
|
||
| # Report at: target/site/jacoco/index.html | ||
| ``` | ||
|
|
||
| ## Test Dependencies | ||
|
|
||
| ```xml | ||
| <dependencies> | ||
| <!-- Quarkus Testing --> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-junit5</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-junit5-mockito</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <!-- Mockito --> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-core</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <!-- AssertJ (preferred over JUnit assertions) --> | ||
| <dependency> | ||
| <groupId>org.assertj</groupId> | ||
| <artifactId>assertj-core</artifactId> | ||
| <version>3.24.2</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <!-- REST Assured --> | ||
| <dependency> | ||
| <groupId>io.rest-assured</groupId> | ||
| <artifactId>rest-assured</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <!-- Camel Testing --> | ||
| <dependency> | ||
| <groupId>org.apache.camel.quarkus</groupId> | ||
| <artifactId>camel-quarkus-junit5</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> | ||
| ``` | ||
|
|
||
| ## Best Practices | ||
|
|
||
| ### Test Organization | ||
| - Use `@Nested` classes to group tests by method being tested | ||
| - Use `@DisplayName` for readable test descriptions visible in reports | ||
| - Follow `givenX_whenY_thenZ` naming convention for test methods | ||
| - Use `@BeforeEach` for common test data setup to reduce duplication | ||
|
|
||
| ### Test Structure | ||
| - Follow AAA pattern with explicit comments (`// ARRANGE`, `// ACT`, `// ASSERT`) | ||
| - Use `assertDoesNotThrow` for success scenarios | ||
| - Use `assertThrows` for exception scenarios with message validation | ||
| - Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()` | ||
|
|
||
| ### Test Coverage | ||
| - Test happy paths for all public methods | ||
| - Test null input handling | ||
| - Test edge cases (empty collections, boundary values, negative IDs, blank strings) | ||
| - Test exception scenarios comprehensively | ||
| - Mock all external dependencies (repositories, services, Camel endpoints) | ||
| - Aim for 80%+ line coverage, 70%+ branch coverage | ||
|
|
||
| ### Assertions | ||
| - **Prefer AssertJ** (`assertThat`) over JUnit assertions for value checks | ||
| - Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)` | ||
| - For exceptions: use JUnit `assertThrows` to capture, then AssertJ to validate the message | ||
| - For non-throwing success paths: use JUnit `assertDoesNotThrow` | ||
| - For collections: `extracting()`, `filteredOn()`, `containsExactly()` | ||
|
|
||
| ### Testing Integration | ||
| - Use `@QuarkusTest` for integration tests | ||
| - Use `@InjectMock` to mock dependencies in Quarkus tests | ||
| - Prefer REST Assured for API testing | ||
| - Use `@TestProfile` for test-specific configuration | ||
|
|
||
| ### Event-Driven Testing | ||
| - Test Camel routes with `AdviceWith` and `MockEndpoint` | ||
| - Use `@CamelQuarkusTest` annotation (if using standalone Camel tests) | ||
| - Verify message content, headers, and routing logic | ||
| - Test error handling routes separately | ||
| - Mock external systems (RabbitMQ, S3, databases) in unit tests | ||
|
|
||
| ### Camel Route Testing | ||
| - Use `MockEndpoint` for asserting message flow | ||
| - Use `AdviceWith` to modify routes for testing (replace endpoints with mocks) | ||
| - Test message transformation and marshalling | ||
| - Test exception handling and dead letter queues | ||
|
|
||
| ### Testing Async Operations | ||
| - Test CompletableFuture success and failure scenarios | ||
| - Use `.join()` in tests to wait for async completion | ||
| - Test exception propagation from CompletableFuture | ||
| - Verify LogContext propagation to async operations | ||
|
|
||
| ### Performance | ||
| - Keep tests fast and isolated | ||
| - Run tests in continuous mode: `mvn quarkus:test` | ||
| - Use parameterized tests (`@ParameterizedTest`) for input variations | ||
| - Build reusable test data builders or factory methods | ||
|
|
||
| ### Quarkus-Specific | ||
| - Stay on latest LTS version (Quarkus 3.x) | ||
| - Test native compilation compatibility periodically | ||
| - Use Quarkus test profiles for different scenarios | ||
| - Leverage Quarkus dev services for local testing | ||
| - Use `@InjectMock` instead of `@MockBean` (Quarkus-specific) | ||
|
|
||
| ### Verification Best Practices | ||
| - Always verify interactions on mocked dependencies | ||
| - Use `verify(mock, never())` to ensure methods are NOT called in error scenarios | ||
| - Use `argThat()` for complex argument matching | ||
| - Verify the order of calls when it matters: `InOrder` from Mockito |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Restructure to match required skill format.
The "Workflow" heading at line 22 should be "How It Works", and all code examples should be grouped under an explicit "## Examples" section to comply with skill documentation standards.
As per coding guidelines: Skill format must be Markdown with clear sections for When to Use, How It Works, and Examples.
📋 Suggested restructuring
-## Workflow
+## How It Works
1. Write tests first (they should fail)
2. Implement minimal code to pass
3. Refactor with tests green
4. Enforce coverage with JaCoCo (80%+ target)
+## Examples
+
## Unit Tests with `@Nested` Organization
...Move all subsequent sections (Unit Tests, Camel Routes, Event Services, etc.) under the "Examples" heading.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## Workflow | |
| 1. Write tests first (they should fail) | |
| 2. Implement minimal code to pass | |
| 3. Refactor with tests green | |
| 4. Enforce coverage with JaCoCo (80%+ target) | |
| ## Unit Tests with @Nested Organization | |
| Follow this structured approach for comprehensive, readable tests: | |
| ```java | |
| @ExtendWith(MockitoExtension.class) | |
| @DisplayName("As2ProcessingService Unit Tests") | |
| class As2ProcessingServiceTest { | |
| @Mock | |
| private InvoiceFlowValidator invoiceFlowValidator; | |
| @Mock | |
| private EventService eventService; | |
| @Mock | |
| private DocumentJobService documentJobService; | |
| @Mock | |
| private BusinessRulesPublisher businessRulesPublisher; | |
| @Mock | |
| private FileStorageService fileStorageService; | |
| @InjectMocks | |
| private As2ProcessingService as2ProcessingService; | |
| private Path testFilePath; | |
| private LogContext testLogContext; | |
| private InvoiceValidationResult validationResult; | |
| private StoredDocumentInfo documentInfo; | |
| @BeforeEach | |
| void setUp() { | |
| // ARRANGE - Common test data | |
| testFilePath = Path.of("/tmp/test-invoice.xml"); | |
| testLogContext = new LogContext(); | |
| testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001"); | |
| testLogContext.put(As2Constants.FILE_NAME, "invoice.xml"); | |
| testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001"); | |
| validationResult = new InvoiceValidationResult(); | |
| validationResult.setValid(true); | |
| validationResult.setSize(1024L); | |
| validationResult.setDocumentHash("abc123"); | |
| documentInfo = new StoredDocumentInfo(); | |
| documentInfo.setPath("s3://bucket/path/invoice.xml"); | |
| documentInfo.setSize(1024L); | |
| } | |
| @Nested | |
| @DisplayName("Tests for processFile") | |
| class ProcessFile { | |
| @Test | |
| @DisplayName("Should successfully process non-CHORUS file with all validations") | |
| void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception { | |
| // ARRANGE | |
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | |
| CustomLog.setCurrentContext(testLogContext); | |
| when(invoiceFlowValidator.validateFlowWithConfig( | |
| eq(testFilePath), | |
| eq(ValidationFlowConfig.allValidations()), | |
| eq(EInvoiceSyntaxFormat.UBL), | |
| any(LogContext.class))) | |
| .thenReturn(validationResult); | |
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | |
| .thenReturn(FlowProfile.BASIC); | |
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | |
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | |
| when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any())) | |
| .thenReturn(new BusinessRulesPayload()); | |
| // ACT | |
| assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); | |
| // ASSERT | |
| verify(invoiceFlowValidator).validateFlowWithConfig( | |
| eq(testFilePath), | |
| eq(ValidationFlowConfig.allValidations()), | |
| eq(EInvoiceSyntaxFormat.UBL), | |
| any(LogContext.class)); | |
| verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), | |
| eq("PERSISTENCE_BLOB_EVENT_TYPE")); | |
| verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class), | |
| eq("BUSINESS_RULES_MESSAGE_SENT")); | |
| verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class)); | |
| } | |
| @Test | |
| @DisplayName("Should bypass schematron validation for CHORUS_FLOW") | |
| void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception { | |
| // ARRANGE | |
| testLogContext.put(As2Constants.CHORUS_FLOW, "true"); | |
| CustomLog.setCurrentContext(testLogContext); | |
| when(invoiceFlowValidator.validateFlowWithConfig( | |
| eq(testFilePath), | |
| eq(ValidationFlowConfig.xsdOnly()), | |
| eq(EInvoiceSyntaxFormat.UBL), | |
| any(LogContext.class))) | |
| .thenReturn(validationResult); | |
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | |
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | |
| when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), | |
| eq(FlowProfile.EXTENDED_CTC_FR), any())) | |
| .thenReturn(new BusinessRulesPayload()); | |
| // ACT | |
| assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); | |
| // ASSERT | |
| verify(invoiceFlowValidator).validateFlowWithConfig( | |
| eq(testFilePath), | |
| eq(ValidationFlowConfig.xsdOnly()), | |
| eq(EInvoiceSyntaxFormat.UBL), | |
| any(LogContext.class)); | |
| verify(documentJobService).createDocumentAndJobEntities( | |
| any(), any(), any(), | |
| eq(FlowProfile.EXTENDED_CTC_FR), | |
| any()); | |
| } | |
| @Test | |
| @DisplayName("Should create error event when file upload fails") | |
| void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception { | |
| // ARRANGE | |
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | |
| CustomLog.setCurrentContext(testLogContext); | |
| when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) | |
| .thenReturn(validationResult); | |
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | |
| .thenReturn(FlowProfile.BASIC); | |
| documentInfo.setPath(""); // Blank path triggers error | |
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | |
| .thenReturn(CompletableFuture.completedFuture(documentInfo)); | |
| // ACT & ASSERT | |
| As2ServerProcessingException exception = assertThrows( | |
| As2ServerProcessingException.class, | |
| () -> as2ProcessingService.processFile(testFilePath) | |
| ); | |
| assertThat(exception.getMessage()) | |
| .contains("File path is empty after upload"); | |
| verify(eventService).createErrorEvent( | |
| eq(documentInfo), | |
| eq("FILE_UPLOAD_FAILED"), | |
| contains("File path is empty")); | |
| verify(businessRulesPublisher, never()).publishAsync(any()); | |
| } | |
| @Test | |
| @DisplayName("Should handle CompletableFuture.join() failure") | |
| void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception { | |
| // ARRANGE | |
| testLogContext.put(As2Constants.CHORUS_FLOW, "false"); | |
| CustomLog.setCurrentContext(testLogContext); | |
| when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) | |
| .thenReturn(validationResult); | |
| when(invoiceFlowValidator.computeFlowProfile(any(), any())) | |
| .thenReturn(FlowProfile.BASIC); | |
| CompletableFuture<StoredDocumentInfo> failedFuture = | |
| CompletableFuture.failedFuture(new StorageException("S3 connection failed")); | |
| when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) | |
| .thenReturn(failedFuture); | |
| // ACT & ASSERT | |
| assertThrows( | |
| CompletionException.class, | |
| () -> as2ProcessingService.processFile(testFilePath) | |
| ); | |
| } | |
| @Test | |
| @DisplayName("Should throw exception when file path is null") | |
| void givenNullFilePath_whenProcessFile_thenThrowsException() { | |
| // ARRANGE | |
| Path nullPath = null; | |
| // ACT & ASSERT | |
| NullPointerException exception = assertThrows( | |
| NullPointerException.class, | |
| () -> as2ProcessingService.processFile(nullPath) | |
| ); | |
| verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any()); | |
| } | |
| } | |
| } | |
| ``` | |
| ### Key Testing Patterns | |
| 1. **@Nested Classes**: Group tests by method being tested | |
| 2. **@DisplayName**: Provide readable test descriptions for test reports | |
| 3. **Naming Convention**: `givenX_whenY_thenZ` for clarity | |
| 4. **AAA Pattern**: Explicit `// ARRANGE`, `// ACT`, `// ASSERT` comments | |
| 5. **@BeforeEach**: Setup common test data to reduce duplication | |
| 6. **assertDoesNotThrow**: Test success scenarios without catching exceptions | |
| 7. **assertThrows**: Test exception scenarios with message validation using AssertJ | |
| 8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions | |
| 9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly | |
| 10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios | |
| ## Testing Camel Routes | |
| ```java | |
| @QuarkusTest | |
| @DisplayName("Business Rules Camel Route Tests") | |
| class BusinessRulesRouteTest { | |
| @Inject | |
| CamelContext camelContext; | |
| @Inject | |
| ProducerTemplate producerTemplate; | |
| @InjectMock | |
| EventService eventService; | |
| @InjectMock | |
| DocumentValidator documentValidator; | |
| private BusinessRulesPayload testPayload; | |
| @BeforeEach | |
| void setUp() { | |
| // ARRANGE - Test data | |
| testPayload = new BusinessRulesPayload(); | |
| testPayload.setDocumentId(1L); | |
| testPayload.setFlowProfile(FlowProfile.BASIC); | |
| } | |
| @Nested | |
| @DisplayName("Tests for business-rules-publisher route") | |
| class BusinessRulesPublisher { | |
| @Test | |
| @DisplayName("Should successfully publish message to RabbitMQ") | |
| void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception { | |
| // ARRANGE | |
| MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); | |
| mockRabbitMQ.expectedMessageCount(1); | |
| // Replace real endpoint with mock for testing | |
| camelContext.getRouteController().stopRoute("business-rules-publisher"); | |
| AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { | |
| advice.replaceFromWith("direct:business-rules-publisher"); | |
| advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); | |
| }); | |
| camelContext.getRouteController().startRoute("business-rules-publisher"); | |
| // ACT | |
| producerTemplate.sendBody("direct:business-rules-publisher", testPayload); | |
| // ASSERT — body is a JSON String after .marshal().json(JsonLibrary.Jackson) | |
| mockRabbitMQ.assertIsSatisfied(5000); | |
| assertThat(mockRabbitMQ.getExchanges()).hasSize(1); | |
| String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); | |
| assertThat(body).contains("\"documentId\":1"); | |
| } | |
| @Test | |
| @DisplayName("Should handle marshalling to JSON") | |
| void givenPayload_whenPublish_thenMarshalledToJson() throws Exception { | |
| // ARRANGE | |
| MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); | |
| camelContext.addEndpoint("mock:marshal", mockMarshal); | |
| mockMarshal.expectedMessageCount(1); | |
| camelContext.getRouteController().stopRoute("business-rules-publisher"); | |
| AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { | |
| advice.weaveAddLast().to("mock:marshal"); | |
| }); | |
| camelContext.getRouteController().startRoute("business-rules-publisher"); | |
| // ACT | |
| producerTemplate.sendBody("direct:business-rules-publisher", testPayload); | |
| // ASSERT | |
| mockMarshal.assertIsSatisfied(5000); | |
| String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); | |
| assertThat(body).contains("\"documentId\":1"); | |
| assertThat(body).contains("\"flowProfile\":\"BASIC\""); | |
| } | |
| } | |
| @Nested | |
| @DisplayName("Tests for document-processing route") | |
| class DocumentProcessing { | |
| @Test | |
| @DisplayName("Should route invoice to correct processor") | |
| void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception { | |
| // ARRANGE | |
| MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); | |
| mockInvoice.expectedMessageCount(1); | |
| camelContext.getRouteController().stopRoute("document-processing"); | |
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | |
| advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); | |
| }); | |
| camelContext.getRouteController().startRoute("document-processing"); | |
| // ACT | |
| producerTemplate.sendBodyAndHeader("direct:process-document", | |
| testPayload, "documentType", "INVOICE"); | |
| // ASSERT | |
| mockInvoice.assertIsSatisfied(5000); | |
| } | |
| @Test | |
| @DisplayName("Should handle validation errors gracefully") | |
| void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception { | |
| // ARRANGE | |
| MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); | |
| mockError.expectedMessageCount(1); | |
| camelContext.getRouteController().stopRoute("document-processing"); | |
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | |
| advice.weaveByToString(".*direct:validation-error-handler.*") | |
| .replace().to("mock:error"); | |
| }); | |
| camelContext.getRouteController().startRoute("document-processing"); | |
| // Mock validator bean to throw exception | |
| when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document")); | |
| // ACT | |
| producerTemplate.sendBody("direct:process-document", testPayload); | |
| // ASSERT | |
| mockError.assertIsSatisfied(5000); | |
| Exception exception = mockError.getExchanges().get(0).getException(); | |
| assertThat(exception).isInstanceOf(ValidationException.class); | |
| assertThat(exception.getMessage()).contains("Invalid document"); | |
| } | |
| } | |
| } | |
| ``` | |
| ## Testing Event Services | |
| ```java | |
| @ExtendWith(MockitoExtension.class) | |
| @DisplayName("EventService Unit Tests") | |
| class EventServiceTest { | |
| @Mock | |
| private EventRepository eventRepository; | |
| @Mock | |
| private ObjectMapper objectMapper; | |
| @InjectMocks | |
| private EventService eventService; | |
| private BusinessRulesPayload testPayload; | |
| @BeforeEach | |
| void setUp() { | |
| // ARRANGE | |
| testPayload = new BusinessRulesPayload(); | |
| testPayload.setDocumentId(1L); | |
| } | |
| @Nested | |
| @DisplayName("Tests for createSuccessEvent") | |
| class CreateSuccessEvent { | |
| @Test | |
| @DisplayName("Should create success event with correct attributes") | |
| void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { | |
| // ARRANGE | |
| when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); | |
| // ACT | |
| assertDoesNotThrow(() -> | |
| eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); | |
| // ASSERT | |
| verify(eventRepository).persist(argThat(event -> | |
| event.getType().equals("DOCUMENT_PROCESSED") && | |
| event.getStatus() == EventStatus.SUCCESS && | |
| event.getPayload().equals("{\"documentId\":1}") && | |
| event.getTimestamp() != null | |
| )); | |
| } | |
| @Test | |
| @DisplayName("Should throw exception when payload is null") | |
| void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { | |
| // ARRANGE | |
| Object nullPayload = null; | |
| // ACT & ASSERT | |
| NullPointerException exception = assertThrows( | |
| NullPointerException.class, | |
| () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") | |
| ); | |
| assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); | |
| verify(eventRepository, never()).persist(any()); | |
| } | |
| } | |
| @Nested | |
| @DisplayName("Tests for createErrorEvent") | |
| class CreateErrorEvent { | |
| @Test | |
| @DisplayName("Should create error event with error message") | |
| void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { | |
| // ARRANGE | |
| String errorMessage = "Processing failed"; | |
| when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); | |
| // ACT | |
| assertDoesNotThrow(() -> | |
| eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); | |
| // ASSERT | |
| verify(eventRepository).persist(argThat(event -> | |
| event.getType().equals("PROCESSING_ERROR") && | |
| event.getStatus() == EventStatus.ERROR && | |
| event.getErrorMessage().equals(errorMessage) && | |
| event.getPayload().equals("{\"documentId\":1}") | |
| )); | |
| } | |
| @ParameterizedTest | |
| @DisplayName("Should reject invalid error messages") | |
| @ValueSource(strings = {"", " "}) | |
| void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) { | |
| // ACT & ASSERT | |
| IllegalArgumentException exception = assertThrows( | |
| IllegalArgumentException.class, | |
| () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) | |
| ); | |
| assertThat(exception.getMessage()).contains("Error message cannot be blank"); | |
| } | |
| } | |
| } | |
| ``` | |
| ## Testing CompletableFuture | |
| ```java | |
| @ExtendWith(MockitoExtension.class) | |
| @DisplayName("FileStorageService Unit Tests") | |
| class FileStorageServiceTest { | |
| @Mock | |
| private S3Client s3Client; | |
| @Mock | |
| private ExecutorService executorService; | |
| @InjectMocks | |
| private FileStorageService fileStorageService; | |
| private InputStream testInputStream; | |
| private LogContext testLogContext; | |
| @BeforeEach | |
| void setUp() { | |
| // ARRANGE | |
| testInputStream = new ByteArrayInputStream("test content".getBytes()); | |
| testLogContext = new LogContext(); | |
| testLogContext.put("traceId", "trace-123"); | |
| } | |
| @Nested | |
| @DisplayName("Tests for uploadOriginalFile") | |
| class UploadOriginalFile { | |
| @Test | |
| @DisplayName("Should successfully upload file and return document info") | |
| void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { | |
| // ARRANGE | |
| doAnswer(invocation -> { | |
| ((Runnable) invocation.getArgument(0)).run(); | |
| return null; | |
| }).when(executorService).execute(any(Runnable.class)); | |
| when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | |
| .thenReturn(PutObjectResponse.builder().build()); | |
| // ACT | |
| CompletableFuture<StoredDocumentInfo> future = | |
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | |
| testLogContext, InvoiceFormat.UBL); | |
| StoredDocumentInfo result = future.join(); | |
| // ASSERT | |
| assertThat(result).isNotNull(); | |
| assertThat(result.getPath()).isNotBlank(); | |
| assertThat(result.getSize()).isEqualTo(1024L); | |
| assertThat(result.getUploadedAt()).isNotNull(); | |
| verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); | |
| } | |
| @Test | |
| @DisplayName("Should handle S3 upload failure") | |
| void givenS3Failure_whenUpload_thenCompletableFutureFails() { | |
| // ARRANGE — run synchronously so exception propagates through the future | |
| doAnswer(invocation -> { | |
| ((Runnable) invocation.getArgument(0)).run(); | |
| return null; | |
| }).when(executorService).execute(any(Runnable.class)); | |
| when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) | |
| .thenThrow(new StorageException("S3 unavailable")); | |
| // ACT | |
| CompletableFuture<StoredDocumentInfo> future = | |
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | |
| testLogContext, InvoiceFormat.UBL); | |
| // ASSERT | |
| assertThatThrownBy(() -> future.join()) | |
| .isInstanceOf(CompletionException.class) | |
| .hasCauseInstanceOf(StorageException.class) | |
| .hasMessageContaining("S3 unavailable"); | |
| } | |
| @Test | |
| @DisplayName("Should propagate LogContext to async operation") | |
| void givenLogContext_whenUpload_thenContextPropagated() throws Exception { | |
| // ARRANGE | |
| AtomicReference<LogContext> capturedContext = new AtomicReference<>(); | |
| doAnswer(invocation -> { | |
| capturedContext.set(CustomLog.getCurrentContext()); | |
| ((Runnable) invocation.getArgument(0)).run(); | |
| return null; | |
| }).when(executorService).execute(any(Runnable.class)); | |
| // ACT | |
| fileStorageService.uploadOriginalFile(testInputStream, 1024L, | |
| testLogContext, InvoiceFormat.UBL).join(); | |
| // ASSERT | |
| assertThat(capturedContext.get()).isNotNull(); | |
| assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); | |
| } | |
| } | |
| } | |
| ``` | |
| ## Resource Layer Tests (REST Assured) | |
| ```java | |
| @QuarkusTest | |
| @DisplayName("DocumentResource API Tests") | |
| class DocumentResourceTest { | |
| @InjectMock | |
| DocumentService documentService; | |
| @Nested | |
| @DisplayName("Tests for GET /api/documents") | |
| class ListDocuments { | |
| @Test | |
| @DisplayName("Should return list of documents") | |
| void givenDocumentsExist_whenList_thenReturnsOk() { | |
| // ARRANGE | |
| List<Document> documents = List.of(createDocument(1L, "DOC-001")); | |
| when(documentService.list(0, 20)).thenReturn(documents); | |
| // ACT & ASSERT | |
| given() | |
| .when().get("/api/documents") | |
| .then() | |
| .statusCode(200) | |
| .body("$.size()", is(1)) | |
| .body("[0].referenceNumber", equalTo("DOC-001")); | |
| } | |
| } | |
| @Nested | |
| @DisplayName("Tests for POST /api/documents") | |
| class CreateDocument { | |
| @Test | |
| @DisplayName("Should create document and return 201") | |
| void givenValidRequest_whenCreate_thenReturns201() { | |
| // ARRANGE | |
| Document document = createDocument(1L, "DOC-001"); | |
| when(documentService.create(any())).thenReturn(document); | |
| // ACT & ASSERT | |
| given() | |
| .contentType(ContentType.JSON) | |
| .body(""" | |
| { | |
| "referenceNumber": "DOC-001", | |
| "description": "Test document", | |
| "validUntil": "2030-01-01T00:00:00Z", | |
| "categories": ["test"] | |
| } | |
| """) | |
| .when().post("/api/documents") | |
| .then() | |
| .statusCode(201) | |
| .header("Location", containsString("/api/documents/1")) | |
| .body("referenceNumber", equalTo("DOC-001")); | |
| } | |
| @Test | |
| @DisplayName("Should return 400 for invalid input") | |
| void givenInvalidRequest_whenCreate_thenReturns400() { | |
| // ACT & ASSERT | |
| given() | |
| .contentType(ContentType.JSON) | |
| .body(""" | |
| { | |
| "referenceNumber": "", | |
| "description": "Test" | |
| } | |
| """) | |
| .when().post("/api/documents") | |
| .then() | |
| .statusCode(400); | |
| } | |
| } | |
| private Document createDocument(Long id, String referenceNumber) { | |
| Document document = new Document(); | |
| document.setId(id); | |
| document.setReferenceNumber(referenceNumber); | |
| document.setStatus(DocumentStatus.PENDING); | |
| return document; | |
| } | |
| } | |
| ``` | |
| ## Integration Tests with Real Database | |
| ```java | |
| @QuarkusTest | |
| @TestProfile(IntegrationTestProfile.class) | |
| @DisplayName("Document Integration Tests") | |
| class DocumentIntegrationTest { | |
| @Test | |
| @Transactional | |
| @DisplayName("Should create and retrieve document via API") | |
| void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() { | |
| // ACT - Create via API | |
| Long id = given() | |
| .contentType(ContentType.JSON) | |
| .body(""" | |
| { | |
| "referenceNumber": "INT-001", | |
| "description": "Integration test", | |
| "validUntil": "2030-01-01T00:00:00Z", | |
| "categories": ["test"] | |
| } | |
| """) | |
| .when().post("/api/documents") | |
| .then() | |
| .statusCode(201) | |
| .extract().path("id"); | |
| // ASSERT - Retrieve via API | |
| given() | |
| .when().get("/api/documents/" + id) | |
| .then() | |
| .statusCode(200) | |
| .body("referenceNumber", equalTo("INT-001")); | |
| } | |
| } | |
| ``` | |
| ## Coverage with JaCoCo | |
| ### Maven Configuration (Complete) | |
| ```xml | |
| <plugin> | |
| <groupId>org.jacoco</groupId> | |
| <artifactId>jacoco-maven-plugin</artifactId> | |
| <version>0.8.13</version> | |
| <executions> | |
| <!-- Prepare agent for test execution --> | |
| <execution> | |
| <id>prepare-agent</id> | |
| <goals> | |
| <goal>prepare-agent</goal> | |
| </goals> | |
| </execution> | |
| <!-- Generate coverage report --> | |
| <execution> | |
| <id>report</id> | |
| <phase>verify</phase> | |
| <goals> | |
| <goal>report</goal> | |
| </goals> | |
| </execution> | |
| <!-- Enforce coverage thresholds --> | |
| <execution> | |
| <id>check</id> | |
| <goals> | |
| <goal>check</goal> | |
| </goals> | |
| <configuration> | |
| <rules> | |
| <rule> | |
| <element>BUNDLE</element> | |
| <limits> | |
| <limit> | |
| <counter>LINE</counter> | |
| <value>COVEREDRATIO</value> | |
| <minimum>0.80</minimum> | |
| </limit> | |
| <limit> | |
| <counter>BRANCH</counter> | |
| <value>COVEREDRATIO</value> | |
| <minimum>0.70</minimum> | |
| </limit> | |
| </limits> | |
| </rule> | |
| </rules> | |
| </configuration> | |
| </execution> | |
| </executions> | |
| </plugin> | |
| ``` | |
| Run tests with coverage: | |
| ```bash | |
| mvn clean test | |
| mvn jacoco:report | |
| mvn jacoco:check | |
| # Report at: target/site/jacoco/index.html | |
| ``` | |
| ## Test Dependencies | |
| ```xml | |
| <dependencies> | |
| <!-- Quarkus Testing --> | |
| <dependency> | |
| <groupId>io.quarkus</groupId> | |
| <artifactId>quarkus-junit5</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| <dependency> | |
| <groupId>io.quarkus</groupId> | |
| <artifactId>quarkus-junit5-mockito</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| <!-- Mockito --> | |
| <dependency> | |
| <groupId>org.mockito</groupId> | |
| <artifactId>mockito-core</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| <!-- AssertJ (preferred over JUnit assertions) --> | |
| <dependency> | |
| <groupId>org.assertj</groupId> | |
| <artifactId>assertj-core</artifactId> | |
| <version>3.24.2</version> | |
| <scope>test</scope> | |
| </dependency> | |
| <!-- REST Assured --> | |
| <dependency> | |
| <groupId>io.rest-assured</groupId> | |
| <artifactId>rest-assured</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| <!-- Camel Testing --> | |
| <dependency> | |
| <groupId>org.apache.camel.quarkus</groupId> | |
| <artifactId>camel-quarkus-junit5</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| </dependencies> | |
| ``` | |
| ## Best Practices | |
| ### Test Organization | |
| - Use `@Nested` classes to group tests by method being tested | |
| - Use `@DisplayName` for readable test descriptions visible in reports | |
| - Follow `givenX_whenY_thenZ` naming convention for test methods | |
| - Use `@BeforeEach` for common test data setup to reduce duplication | |
| ### Test Structure | |
| - Follow AAA pattern with explicit comments (`// ARRANGE`, `// ACT`, `// ASSERT`) | |
| - Use `assertDoesNotThrow` for success scenarios | |
| - Use `assertThrows` for exception scenarios with message validation | |
| - Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()` | |
| ### Test Coverage | |
| - Test happy paths for all public methods | |
| - Test null input handling | |
| - Test edge cases (empty collections, boundary values, negative IDs, blank strings) | |
| - Test exception scenarios comprehensively | |
| - Mock all external dependencies (repositories, services, Camel endpoints) | |
| - Aim for 80%+ line coverage, 70%+ branch coverage | |
| ### Assertions | |
| - **Prefer AssertJ** (`assertThat`) over JUnit assertions for value checks | |
| - Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)` | |
| - For exceptions: use JUnit `assertThrows` to capture, then AssertJ to validate the message | |
| - For non-throwing success paths: use JUnit `assertDoesNotThrow` | |
| - For collections: `extracting()`, `filteredOn()`, `containsExactly()` | |
| ### Testing Integration | |
| - Use `@QuarkusTest` for integration tests | |
| - Use `@InjectMock` to mock dependencies in Quarkus tests | |
| - Prefer REST Assured for API testing | |
| - Use `@TestProfile` for test-specific configuration | |
| ### Event-Driven Testing | |
| - Test Camel routes with `AdviceWith` and `MockEndpoint` | |
| - Use `@CamelQuarkusTest` annotation (if using standalone Camel tests) | |
| - Verify message content, headers, and routing logic | |
| - Test error handling routes separately | |
| - Mock external systems (RabbitMQ, S3, databases) in unit tests | |
| ### Camel Route Testing | |
| - Use `MockEndpoint` for asserting message flow | |
| - Use `AdviceWith` to modify routes for testing (replace endpoints with mocks) | |
| - Test message transformation and marshalling | |
| - Test exception handling and dead letter queues | |
| ### Testing Async Operations | |
| - Test CompletableFuture success and failure scenarios | |
| - Use `.join()` in tests to wait for async completion | |
| - Test exception propagation from CompletableFuture | |
| - Verify LogContext propagation to async operations | |
| ### Performance | |
| - Keep tests fast and isolated | |
| - Run tests in continuous mode: `mvn quarkus:test` | |
| - Use parameterized tests (`@ParameterizedTest`) for input variations | |
| - Build reusable test data builders or factory methods | |
| ### Quarkus-Specific | |
| - Stay on latest LTS version (Quarkus 3.x) | |
| - Test native compilation compatibility periodically | |
| - Use Quarkus test profiles for different scenarios | |
| - Leverage Quarkus dev services for local testing | |
| - Use `@InjectMock` instead of `@MockBean` (Quarkus-specific) | |
| ### Verification Best Practices | |
| - Always verify interactions on mocked dependencies | |
| - Use `verify(mock, never())` to ensure methods are NOT called in error scenarios | |
| - Use `argThat()` for complex argument matching | |
| - Verify the order of calls when it matters: `InOrder` from Mockito | |
| ## How It Works | |
| 1. Write tests first (they should fail) | |
| 2. Implement minimal code to pass | |
| 3. Refactor with tests green | |
| 4. Enforce coverage with JaCoCo (80%+ target) | |
| ## Examples | |
| ## Unit Tests with `@Nested` Organization | |
| Follow this structured approach for comprehensive, readable tests: | |
🧰 Tools
🪛 LanguageTool
[grammar] ~883-~883: Use a hyphen to join words.
Context: ... headers, and routing logic - Test error handling routes separately - Mock extern...
(QB_NEW_EN_HYPHEN)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/quarkus-tdd/SKILL.md` around lines 22 - 915, Rename the top-level
"Workflow" heading to "How It Works", add a new "## When to Use" section (brief
guidance) above it, and create an explicit "## Examples" heading; then move all
code example sections (any blocks titled "Unit Tests with `@Nested` Organization",
"Testing Camel Routes", "Testing Event Services", "Testing CompletableFuture",
"Resource Layer Tests (REST Assured)", "Integration Tests with Real Database",
etc.) under the new "## Examples" section so the SKILL.md follows the required
"When to Use / How It Works / Examples" structure and headings.
|
|
||
| @Test | ||
| @DisplayName("Should handle validation errors gracefully") | ||
| void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception { | ||
| // ARRANGE | ||
| MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); | ||
| mockError.expectedMessageCount(1); | ||
|
|
||
| camelContext.getRouteController().stopRoute("document-processing"); | ||
| AdviceWith.adviceWith(camelContext, "document-processing", advice -> { | ||
| advice.weaveByToString(".*direct:validation-error-handler.*") | ||
| .replace().to("mock:error"); | ||
| }); | ||
| camelContext.getRouteController().startRoute("document-processing"); | ||
|
|
||
| // Mock validator bean to throw exception | ||
| when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document")); | ||
|
|
||
| // ACT | ||
| producerTemplate.sendBody("direct:process-document", testPayload); | ||
|
|
||
| // ASSERT | ||
| mockError.assertIsSatisfied(5000); | ||
|
|
||
| Exception exception = mockError.getExchanges().get(0).getException(); | ||
| assertThat(exception).isInstanceOf(ValidationException.class); | ||
| assertThat(exception.getMessage()).contains("Invalid document"); | ||
| } |
There was a problem hiding this comment.
Fix undefined mock reference in test example.
Line 377 calls documentValidator.validate(any()) but documentValidator is not declared or mocked in the test class. The @InjectMock section (lines 265-266) only defines eventService, causing this example to have a compilation error.
🔧 Proposed fix
Option 1: Add the missing mock declaration:
`@InjectMock`
EventService eventService;
+@InjectMock
+DocumentValidator documentValidator;
+
private BusinessRulesPayload testPayload;Option 2: Use the existing eventService mock if the validator logic is part of that service:
- when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));
+ when(eventService.validateDocument(any())).thenThrow(new ValidationException("Invalid document"));Choose the option that matches your actual service architecture.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/quarkus-tdd/SKILL.md` around lines 361 - 388, The test references
documentValidator.validate(...) but no mock is declared; either declare and
inject a mock for the validator (e.g., add an `@InjectMock` or `@Mock` field named
documentValidator of type DocumentValidator and use
when(documentValidator.validate(any())).thenThrow(...)) or, if validation is
performed by the existing eventService, change the mocked call to
when(eventService.validate(any())).thenThrow(...) and keep the rest of the test
unchanged; update the test class to include the chosen mock field
(documentValidator OR reuse eventService) so the when(...) call compiles and the
stubbed exception is applied.
What Changed
Added 4 new Quarkus skills (quarkus-patterns, quarkus-security, quarkus-tdd, quarkus-verification) and propagated them across the entire codebase, mirroring how the existing springboot-* skills are referenced:
Added origin: ECC to all 4 quarkus SKILL.md frontmatters
Why This Change
The quarkus-* skills were created on this branch but only existed as standalone files — they weren't registered in manifests, referenced in rules, listed in READMEs, or included in the installer/optimizer skills. This made them invisible to users browsing the catalog or running /configure-ecc. This PR ensures feature parity with springboot-* skills across all discovery surfaces.
Testing Done
node tests/run-all.js)Type of Change
fix:Bug fixfeat:New featurerefactor:Code refactoringdocs:Documentationtest:Testschore:Maintenance/toolingci:CI/CD changesSecurity & Quality Checklist
Documentation
Summary by cubic
Adds first-class Quarkus support to the Java stack with automatic framework detection, Quarkus-aware agents, and updated coding standards. Updates docs, rules, manifests, and installer to surface Quarkus across the platform.
New Features
quarkus-patterns,quarkus-security,quarkus-tdd,quarkus-verificationwith localized copies (EN, ja-JP, tr, zh-CN).java-reviewerandjava-build-resolver: auto-detect Spring Boot or Quarkus, show Framework output, and link to matching skills; apply framework-specific checks.java-coding-standardsandprompt-optimizer: extended to Quarkus with CDI/Panache/reactive conventions and build-file detection; manifests/installer list Quarkus skills.Bug Fixes
java-build-resolver: added Gradle equivalents for Quarkus commands; split guidance into Maven/Gradle/Common.unsafe-inlinefromscript-srcin CSP; rate limiter now usesgetRemoteAddr(); auth filter rejects missing/malformedAuthorizationheaders.io.quarkus.platform:quarkus-camel-bom.persist()withdoNothing()in examples.Written for commit 46db568. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation