diff --git a/.gitignore b/.gitignore index 0df6242..be6ce2a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ build/ ### Beads ### .beads/ test-plan-*.md +results-*.md plans/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index a7755a6..151398f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,6 +81,8 @@ Required environment variables/arguments: - **Prefer `var`** for local variables when the type is obvious from the right-hand side - **Use `isEmpty()`** instead of `size() > 0` or `size() == 0` for collection checks - **No wildcard imports** - All imports must be explicit. Do not use `import package.*` syntax +- **Simplified mock() syntax** - Use `mock()` without class parameter (Mockito 5.x+). When using `var`, specify the type explicitly: `ClassName mock = mock();` instead of `var mock = mock(ClassName.class);`. For explicit types: `ClassName mock = mock();` instead of `ClassName mock = mock(ClassName.class);` +- **Use AssertJ for test assertions** - Prefer AssertJ's fluent API over JUnit assertions for more readable and expressive tests. Use `assertThat(actual).isEqualTo(expected)` instead of `assertEquals(expected, actual)`, and `assertThat(condition).isTrue()` instead of `assertTrue(condition)` ### Security Considerations @@ -129,6 +131,95 @@ Example: If B must be done after A completes, use `bd dep add B A` (not `bd dep Verify with `bd show ` - dependent tasks show "Depends on", prerequisites show "Blocks". +### Time Tracking for AI Cost Optimization + +This project tracks development time and AI effectiveness to optimize AI spend on code changes. Time tracking uses comments for precise timestamps and labels for categorization. + +**Time Tracking Rules:** + +**Parent Beads (Jira-linked, generate PR):** +- **START:** When parent bead is set to `in_progress` (when coding work begins) +- **END:** When PR is created (after all children complete and branch is ready for review) +- **Duration:** Captures full end-to-end timeline from first work to PR creation +- **Rating:** Asked once when PR is created + +**Child Beads (use parent's branch, no PR):** +- **START:** When child bead is set to `in_progress` (when work on that subtask begins) +- **END:** When child bead is closed (when that specific subtask completes) +- **Duration:** Captures time spent on that specific child +- **Rating:** Asked when each child is closed + +**Comment Format:** +```bash +# Starting work +bd comments add "⏱️ START: 2025-11-13T14:00:00Z - Implementing authentication refactor" + +# Ending work +bd comments add "⏱️ END: 2025-11-13T16:30:00Z - Refactor complete, tests passing" +``` + +**Duration Labels:** +- `duration-0to15min` - Under 15 minutes +- `duration-15to30min` - 15-30 minutes +- `duration-30to60min` - 30-60 minutes +- `duration-60to120min` - 1-2 hours +- `duration-over120min` - Over 2 hours (indicates task should have been broken down) + +**AI Effectiveness Labels:** +- `ai-multiplier` - AI was a force multiplier (saved significant time/effort) +- `ai-helpful` - AI was helpful (saved some time) +- `ai-neutral` - AI was neutral (could have done manually in similar time) +- `ai-friction` - AI added friction (slowed me down, had to correct/redirect) + +**Completing Time Tracking:** + +When ending work on a bead, follow this process to complete time tracking: + +1. **Record END timestamp:** + ```bash + bd comments add "⏱️ END: $(date -u +%Y-%m-%dT%H:%M:%SZ) - [Brief description of what was completed]" + ``` + +2. **Calculate duration estimate** from START/END timestamps in comments + - Parse all START/END timestamps from the bead's comments + - Calculate total elapsed time + - Present estimate to user in human-readable format + +3. **Ask user to confirm duration** using AskUserQuestion tool: + - Show calculated estimate: "Based on timestamps, you worked approximately X minutes/hours" + - Prompt: "Please confirm your active coding time (excluding breaks/interruptions):" + - Options: + 1. Under 15 minutes + 2. 15-30 minutes + 3. 30-60 minutes + 4. 1-2 hours + 5. Over 2 hours + +4. **Ask user about AI effectiveness** using AskUserQuestion tool: + - Prompt: "How effective was AI assistance on this task?" + - Options: + 1. Force multiplier - AI saved significant time/effort + 2. Helpful - AI saved some time + 3. Neutral - Could have done manually in similar time + 4. Added friction - AI slowed me down + +5. **Apply labels** based on user responses: + - Duration label: `duration-0to15min`, `duration-15to30min`, `duration-30to60min`, `duration-60to120min`, or `duration-over120min` + - Effectiveness label: `ai-multiplier`, `ai-helpful`, `ai-neutral`, or `ai-friction` + +**When to complete time tracking:** +- **Parent beads:** When PR is created (after all children complete) +- **Child beads:** When bead is closed (when that specific subtask completes) +- **Note:** Child beads are rated individually when closed; parent bead is rated once when PR is created + +**Data Analysis:** + +Time tracking enables analysis of: +- Which types of code changes benefit most from AI +- Where AI adds friction vs value +- Task breakdown patterns (tasks >2hr indicate insufficient decomposition) +- Actual vs estimated coding time + ## Project Management ### Jira Issue Tracking @@ -199,6 +290,10 @@ Promoting Stacked PR (after base PR merges): **2. Update bead status and labels:** - Set bead status to `in_progress` + - **Record START timestamp for time tracking:** + ```bash + bd comments add "⏱️ START: $(date -u +%Y-%m-%dT%H:%M:%SZ) - [Brief description of what you're starting]" + ``` - Record the branch name in the bead (so it's easily found later) - **If this is a stacked branch** (based on another PR branch): - Label the bead with `stacked-branch` @@ -309,12 +404,16 @@ This workflow creates a standard PR ready for immediate review, targeting the `m **2. Push to remote:** - Push the feature branch to remote repository -**3. Create or update Pull Request:** +**3. Complete time tracking:** + - Follow the **"Completing Time Tracking"** process in the Time Tracking section + - This is for parent beads only (child beads were already rated when closed) + +**4. Create or update Pull Request:** - If PR doesn't exist, create it with base branch `main` - If PR exists, update the description - PR should be ready for review (NOT draft) -**4. Generate comprehensive PR description:** +**5. Generate comprehensive PR description:** - Follow the **"Creating High-Quality PR Descriptions"** section above - Use the standard structure: Why / What / How / Walkthrough / Testing - No special warnings or dependency context needed @@ -337,7 +436,11 @@ This workflow creates a draft PR that depends on another unmerged PR (stacked br **3. Push to remote:** - Push the feature branch: `git push -u origin ` -**4. Create DRAFT Pull Request:** +**4. Complete time tracking:** + - Follow the **"Completing Time Tracking"** process in the Time Tracking section + - This is for parent beads only (child beads were already rated when closed) + +**5. Create DRAFT Pull Request:** - **Base branch**: Set to the parent PR's branch (NOT main) - **Status**: MUST be draft - **Title**: Include `[STACKED]` indicator @@ -360,7 +463,7 @@ This workflow creates a draft PR that depends on another unmerged PR (stacked br - After the warning and dependency context, follow the **"Creating High-Quality PR Descriptions"** section - Use the standard structure: Why / What / How / Walkthrough / Testing -**5. Verify configuration:** +**6. Verify configuration:** - Confirm PR is in draft status - Confirm base branch is the parent PR's branch - Confirm warning and dependency context are prominently displayed @@ -416,13 +519,44 @@ This workflow promotes a draft stacked PR to ready-for-review after its base PR - If NOT merged, inform user and wait **3. Fetch and rebase onto main:** + + **CRITICAL: Use `git rebase --onto` to avoid replaying base commits that are already in main!** + ```bash - git fetch origin + # Fetch latest from origin (including the merged base PR) + git fetch origin --prune + + # Checkout the stacked branch git checkout - git rebase origin/main + + # Find the base commit (last commit of the base branch) + git log --oneline --graph --decorate -20 + # Look for the commit right before your stacked commits started + # This is typically the last commit from the base branch + + # Rebase ONLY the stacked commits onto main + git rebase --onto origin/main + ``` + + **Example:** + If your branch history shows: + ``` + * abc1234 (HEAD) Your stacked commit 3 + * def5678 Your stacked commit 2 + * ghi9012 Your stacked commit 1 + * jkl3456 Last commit from base branch <-- This is your + * mno7890 Base branch commit ``` - - Handle conflicts if they arise (pause and ask user for guidance) - - Clean rebase expected for well-structured stacks + + Use: `git rebase --onto origin/main jkl3456` + + This rebases only commits `ghi9012`, `def5678`, and `abc1234` onto main, + avoiding conflicts from commits that are already merged. + + **Troubleshooting:** + - If you get conflicts about changes already in main, you likely used the wrong base commit + - Handle genuine conflicts by pausing and asking user for guidance + - Clean rebase expected for well-structured stacks with correct base commit **4. Force push safely:** ```bash @@ -527,6 +661,44 @@ mvn clean verify - Verify workflow configuration targets main branch - Check `.github/workflows/` for PR triggers +### After PR is Merged + +**When a PR is merged to main:** + +This workflow completes the development cycle by closing the bead and updating the Jira ticket status. + +**Steps:** + +**1. Close the bead:** + ```bash + bd close + ``` + - Provide a brief reason mentioning the merged PR + - Example: "PR #28 merged to main. Successfully added appID and appName fields to VulnLight record." + +**2. Update Jira status to "Ready to Deploy":** + - Use the Atlassian MCP to transition the Jira ticket + - Transition to "Ready to Deploy" status (NOT "Closed") + - "Closed" status is reserved for when code is actually released/deployed + - Example: + ```python + mcp__atlassian__transitionJiraIssue( + cloudId="https://contrast.atlassian.net", + issueIdOrKey="AIML-XXX", + transition={"id": "51"} # "Ready to Deploy" transition + ) + ``` + +**3. Check for dependent beads/PRs:** + - If this was a base PR for stacked PRs, the dependent PRs can now be promoted + - Check for beads with `stacked-branch` label that depend on this one + - Follow the "Promoting Stacked PR to Ready for Review" workflow for each dependent PR + +**Rationale:** +- "Ready to Deploy" indicates the code is merged and ready for the next release +- "Closed" should only be used when the code is actually deployed/released to production +- This allows tracking what code is ready to go out in the next release vs what's already deployed + ### Landing the Plane **When user says "let's land the plane":** @@ -559,4 +731,6 @@ This workflow is for ending the current session while preserving all state so wo **Cannot close parent beads** if they still have open children. Ensure all child beads are closed first. -Beads typically remain `in_progress` (with `in-review` label) until the PR review is complete and merged. Only close beads when explicitly instructed by the user. \ No newline at end of file +**For child beads:** When closing a child bead, complete time tracking using the **"Completing Time Tracking"** process in the Time Tracking section. This captures the time spent on that specific subtask. + +**For parent beads:** Time tracking is completed when the PR is created, not when the bead is closed. Beads typically remain `in_progress` (with `in-review` label) until the PR review is complete and merged. Only close beads when explicitly instructed by the user. \ No newline at end of file diff --git a/bak-tst-pln.md b/bak-tst-pln.md deleted file mode 100644 index eb204ca..0000000 --- a/bak-tst-pln.md +++ /dev/null @@ -1,631 +0,0 @@ -# Manual Test Plan: list_all_vulnerabilities Tool - -## Overview -This test plan validates the pagination and filtering behavior of the `list_all_vulnerabilities` tool after refactoring to use the params pattern. - -## Test Environment Setup -```bash -# Start the MCP server -java -jar target/mcp-contrast-0.0.15-SNAPSHOT.jar \ - --CONTRAST_HOST_NAME= \ - --CONTRAST_API_KEY= \ - --CONTRAST_SERVICE_KEY= \ - --CONTRAST_USERNAME= \ - --CONTRAST_ORG_ID= -``` - ---- - -## Part 1: Pagination Parameters (Soft Failures) - -### Test 1.1: Valid Pagination - Default Values -**Input:** -```json -{"page": null, "pageSize": null} -``` -**Expected:** -- ✅ Page defaults to 1 -- ✅ PageSize defaults to 50 -- ✅ Results returned successfully -- ✅ No warning messages - -**Validation:** -- Response has `"page": 1` -- Response has `"pageSize": 50` -- Response has `"items"` array with results (if data exists) - ---- - -### Test 1.2: Valid Pagination - Custom Values -**Input:** -```json -{"page": 2, "pageSize": 25} -``` -**Expected:** -- ✅ Page = 2 -- ✅ PageSize = 25 -- ✅ Results returned successfully -- ✅ No warning messages - -**Validation:** -- Response has `"page": 2` -- Response has `"pageSize": 25` -- Correct offset applied (SDK receives offset=25, limit=25) - ---- - -### Test 1.3: Invalid Page - Negative (Soft Failure) -**Input:** -```json -{"page": -5, "pageSize": 50} -``` -**Expected:** -- ⚠️ Page clamped to 1 -- ✅ Results returned (query executes) -- ⚠️ Warning message: "Invalid page number -5, using page 1" - -**Validation:** -- Response has `"page": 1` -- Response has `"message"` containing "Invalid page number -5" -- Response has non-empty `"items"` array (if data exists) - ---- - -### Test 1.4: Invalid Page - Zero (Soft Failure) -**Input:** -```json -{"page": 0, "pageSize": 50} -``` -**Expected:** -- ⚠️ Page clamped to 1 -- ✅ Results returned (query executes) -- ⚠️ Warning message: "Invalid page number 0, using page 1" - -**Validation:** -- Response has `"page": 1` -- Response has `"message"` containing "Invalid page number 0" - ---- - -### Test 1.5: Invalid PageSize - Negative (Soft Failure) -**Input:** -```json -{"page": 1, "pageSize": -10} -``` -**Expected:** -- ⚠️ PageSize clamped to 50 (default) -- ✅ Results returned (query executes) -- ⚠️ Warning message: "Invalid pageSize -10, using default 50" - -**Validation:** -- Response has `"pageSize": 50` -- Response has `"message"` containing "Invalid pageSize -10" - ---- - -### Test 1.6: Invalid PageSize - Zero (Soft Failure) -**Input:** -```json -{"page": 1, "pageSize": 0} -``` -**Expected:** -- ⚠️ PageSize clamped to 50 (default) -- ✅ Results returned (query executes) -- ⚠️ Warning message: "Invalid pageSize 0, using default 50" - -**Validation:** -- Response has `"pageSize": 50` -- Response has `"message"` containing "Invalid pageSize 0" - ---- - -### Test 1.7: Invalid PageSize - Exceeds Maximum (Soft Failure) -**Input:** -```json -{"page": 1, "pageSize": 200} -``` -**Expected:** -- ⚠️ PageSize capped to 100 (maximum) -- ✅ Results returned (query executes) -- ⚠️ Warning message: "Requested pageSize 200 exceeds maximum 100, capped to 100" - -**Validation:** -- Response has `"pageSize": 100` -- Response has `"message"` containing "exceeds maximum 100" - ---- - -### Test 1.8: Multiple Pagination Errors (Soft Failures) -**Input:** -```json -{"page": -5, "pageSize": 200} -``` -**Expected:** -- ⚠️ Page clamped to 1 -- ⚠️ PageSize capped to 100 -- ✅ Results returned (query executes) -- ⚠️ Warning messages for BOTH errors - -**Validation:** -- Response has `"page": 1`, `"pageSize": 100` -- Response has `"message"` containing both "Invalid page number -5" AND "exceeds maximum 100" - ---- - -## Part 2: Filter Parameters - Hard Failures - -### Test 2.1: Invalid Severity - Single (Hard Failure) -**Input:** -```json -{"page": 1, "pageSize": 50, "severities": "SUPER_HIGH"} -``` -**Expected:** -- ❌ Query does NOT execute -- ❌ Empty items array -- ❌ Error message: "Invalid severity 'SUPER_HIGH'. Valid: CRITICAL, HIGH, MEDIUM, LOW, NOTE. Example: 'CRITICAL,HIGH'" - -**Validation:** -- Response has `"items": []` -- Response has `"totalItems": 0` -- Response has `"hasMorePages": false` -- Response has error message in `"message"` -- SDK getTracesInOrg() was NOT called - ---- - -### Test 2.2: Invalid Severity - Mixed Valid/Invalid (Hard Failure) -**Input:** -```json -{"page": 1, "pageSize": 50, "severities": "CRITICAL,SUPER_HIGH,HIGH"} -``` -**Expected:** -- ❌ Query does NOT execute (even though CRITICAL and HIGH are valid) -- ❌ Empty items array -- ❌ Error message: "Invalid severity 'SUPER_HIGH'..." - -**Validation:** -- Response has `"items": []` -- Response has error message mentioning "SUPER_HIGH" - ---- - -### Test 2.3: Invalid Status (Hard Failure) -**Input:** -```json -{"page": 1, "pageSize": 50, "statuses": "Reported,BadStatus"} -``` -**Expected:** -- ❌ Query does NOT execute -- ❌ Empty items array -- ❌ Error message: "Invalid status 'BadStatus'. Valid: Reported, Suspicious, Confirmed, Remediated, Fixed. Example: 'Reported,Confirmed'" - -**Validation:** -- Response has `"items": []` -- Response has error message listing valid statuses - ---- - -### Test 2.4: Invalid Environment (Hard Failure) -**Input:** -```json -{"page": 1, "pageSize": 50, "environments": "PRODUCTION,STAGING"} -``` -**Expected:** -- ❌ Query does NOT execute -- ❌ Empty items array -- ❌ Error message: "Invalid environment 'STAGING'. Valid: DEVELOPMENT, QA, PRODUCTION. Example: 'PRODUCTION,QA'" - -**Validation:** -- Response has `"items": []` -- Response has error message mentioning "STAGING" - ---- - -### Test 2.5: Unparseable Date - Invalid Format (Hard Failure) -**Input:** -```json -{"page": 1, "pageSize": 50, "lastSeenAfter": "Jan 15 2025"} -``` -**Expected:** -- ❌ Query does NOT execute -- ❌ Empty items array -- ❌ Error message: "Invalid lastSeenAfter date 'Jan 15 2025'. Expected ISO format (YYYY-MM-DD) like '2025-01-15' or epoch timestamp like '1705276800000'." - -**Validation:** -- Response has `"items": []` -- Response has error message with example formats - ---- - -### Test 2.6: Unparseable Date - Garbage Input (Hard Failure) -**Input:** -```json -{"page": 1, "pageSize": 50, "lastSeenBefore": "not-a-date"} -``` -**Expected:** -- ❌ Query does NOT execute -- ❌ Empty items array -- ❌ Error message about invalid date format - -**Validation:** -- Response has `"items": []` -- Response has error message for lastSeenBefore - ---- - -### Test 2.7: Date Range Contradiction (Hard Failure) -**Input:** -```json -{"page": 1, "pageSize": 50, "lastSeenAfter": "2025-12-31", "lastSeenBefore": "2025-01-01"} -``` -**Expected:** -- ❌ Query does NOT execute -- ❌ Empty items array -- ❌ Error message: "Invalid date range: lastSeenAfter must be before lastSeenBefore. Example: lastSeenAfter='2025-01-01', lastSeenBefore='2025-12-31'" - -**Validation:** -- Response has `"items": []` -- Response has error message about date range - ---- - -### Test 2.8: Multiple Filter Errors (Hard Failure) -**Input:** -```json -{"page": 1, "pageSize": 50, "severities": "SUPER_HIGH", "statuses": "BadStatus", "environments": "STAGING"} -``` -**Expected:** -- ❌ Query does NOT execute -- ❌ Empty items array -- ❌ Error messages for ALL three validation failures - -**Validation:** -- Response has `"items": []` -- Response has `"message"` containing errors for severity, status, AND environment - ---- - -## Part 3: Filter Parameters - Valid Cases - -### Test 3.1: Valid Severity Filter -**Input:** -```json -{"page": 1, "pageSize": 50, "severities": "CRITICAL,HIGH"} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Results filtered by CRITICAL and HIGH severities -- ✅ No error messages - -**Validation:** -- Response has `"items"` array -- SDK received filter with CRITICAL and HIGH severities -- All returned items have severity in [CRITICAL, HIGH] - ---- - -### Test 3.2: Valid Status Filter - Explicit -**Input:** -```json -{"page": 1, "pageSize": 50, "statuses": "Reported,Confirmed"} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Results filtered by specified statuses -- ✅ NO smart defaults warning (explicit values provided) - -**Validation:** -- Response has `"items"` array -- NO message about "excluding Fixed and Remediated" -- All returned items have status in [Reported, Confirmed] - ---- - -### Test 3.3: Status Filter - Smart Defaults -**Input:** -```json -{"page": 1, "pageSize": 50} -``` -(No statuses parameter provided) - -**Expected:** -- ✅ Query executes successfully -- ⚠️ Warning message: "Showing actionable vulnerabilities only (excluding Fixed and Remediated). To see all statuses, specify statuses parameter explicitly." -- ✅ Results filtered to [Reported, Suspicious, Confirmed] - -**Validation:** -- Response has `"message"` containing "excluding Fixed and Remediated" -- SDK received status filter with [Reported, Suspicious, Confirmed] - ---- - -### Test 3.4: Valid Date Filter - ISO Format -**Input:** -```json -{"page": 1, "pageSize": 50, "lastSeenAfter": "2025-01-01", "lastSeenBefore": "2025-12-31"} -``` -**Expected:** -- ✅ Query executes successfully -- ⚠️ Warning message: "Time filters apply to LAST ACTIVITY DATE (lastTimeSeen), not discovery date." -- ✅ Results filtered by date range - -**Validation:** -- Response has `"items"` array -- Response has `"message"` containing "LAST ACTIVITY DATE" -- SDK received startDate and endDate - ---- - -### Test 3.5: Valid Date Filter - Epoch Timestamp -**Input:** -```json -{"page": 1, "pageSize": 50, "lastSeenAfter": "1704067200000"} -``` -(Epoch timestamp for 2024-01-01) - -**Expected:** -- ✅ Query executes successfully -- ⚠️ Warning message about LAST ACTIVITY DATE -- ✅ Results filtered by date - -**Validation:** -- Response has `"items"` array -- Response has warning message -- SDK received valid Date object - ---- - -### Test 3.6: Valid Environment Filter -**Input:** -```json -{"page": 1, "pageSize": 50, "environments": "PRODUCTION,QA"} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Results filtered by PRODUCTION and QA environments -- ✅ No error messages - -**Validation:** -- Response has `"items"` array -- SDK received environment filter with PRODUCTION and QA - ---- - -### Test 3.7: Valid Vulnerability Types Filter -**Input:** -```json -{"page": 1, "pageSize": 50, "vulnTypes": "sql-injection,xss-reflected"} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Results filtered by specified vulnerability types -- ✅ No validation errors (types are not validated, passed through) - -**Validation:** -- Response has `"items"` array -- SDK received vulnTypes filter with specified values - ---- - -### Test 3.8: Valid Vulnerability Tags Filter -**Input:** -```json -{"page": 1, "pageSize": 50, "vulnTags": "SmartFix Remediated,reviewed"} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Results filtered by specified tags (case-sensitive) -- ✅ No validation errors - -**Validation:** -- Response has `"items"` array -- SDK received filterTags with exact case preserved - ---- - -### Test 3.9: Valid Application ID Filter -**Input:** -```json -{"page": 1, "pageSize": 50, "appId": "your-app-id"} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ App-specific API endpoint used (not org-level) -- ✅ Results filtered to specified app - -**Validation:** -- Response has `"items"` array -- SDK.getTraces(orgId, appId, filterForm) was called (not getTracesInOrg) - ---- - -### Test 3.10: Combined Valid Filters -**Input:** -```json -{ - "page": 1, - "pageSize": 50, - "severities": "CRITICAL,HIGH", - "statuses": "Reported,Confirmed", - "environments": "PRODUCTION", - "vulnTypes": "sql-injection,xss-reflected", - "lastSeenAfter": "2025-01-01" -} -``` -**Expected:** -- ✅ Query executes successfully -- ⚠️ Warning messages: smart defaults NOT used (explicit statuses), time filter note -- ✅ All filters applied correctly - -**Validation:** -- Response has `"items"` array -- Response has `"message"` about time filters (but NOT about smart defaults) -- All filters passed to SDK - ---- - -## Part 4: Edge Cases - -### Test 4.1: Whitespace Handling in Comma-Separated Lists -**Input:** -```json -{"page": 1, "pageSize": 50, "severities": "CRITICAL , HIGH , "} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Whitespace trimmed, empty values filtered out -- ✅ Equivalent to "CRITICAL,HIGH" - -**Validation:** -- Response has `"items"` array -- SDK received clean list: [CRITICAL, HIGH] - ---- - -### Test 4.2: Empty Filter Values -**Input:** -```json -{"page": 1, "pageSize": 50, "severities": ""} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Empty string treated as no filter -- ✅ No validation errors - -**Validation:** -- Response has `"items"` array -- SDK did NOT receive severity filter - ---- - -### Test 4.3: Case Sensitivity - Severities (Case-Insensitive) -**Input:** -```json -{"page": 1, "pageSize": 50, "severities": "critical,HIGH"} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Case normalized to uppercase -- ✅ Equivalent to "CRITICAL,HIGH" - -**Validation:** -- Response has `"items"` array -- SDK received [CRITICAL, HIGH] - ---- - -### Test 4.4: Case Sensitivity - Vulnerability Tags (Case-Sensitive) -**Input:** -```json -{"page": 1, "pageSize": 50, "vulnTags": "SmartFix Remediated,smartfix remediated"} -``` -**Expected:** -- ✅ Query executes successfully -- ✅ Case preserved (treated as two different tags) -- ✅ Results match exact case - -**Validation:** -- Response has `"items"` array -- SDK received both tags with case preserved - ---- - -### Test 4.5: Soft + Hard Failure Combination -**Input:** -```json -{"page": -5, "pageSize": 200, "severities": "SUPER_HIGH"} -``` -**Expected:** -- ❌ Query does NOT execute (hard failure takes precedence) -- ❌ Empty items array -- ❌ Error message about invalid severity - -**Validation:** -- Response has `"items": []` -- Response has error message about severity -- Pagination warnings are NOT included (hard failure stopped execution) - ---- - -## Part 5: Response Format Validation - -### Test 5.1: Successful Response Structure -**For any successful query:** -```json -{ - "items": [...], // Array of VulnLight objects (may be empty) - "page": 1, // 1-based page number - "pageSize": 50, // Actual page size used - "totalItems": 100, // Total count (or null if unavailable) - "hasMorePages": true, // Boolean indicating more pages exist - "message": "..." // Optional warning/info messages -} -``` - -**Validation:** -- `items` is always an array (never null) -- `page` is always ≥ 1 -- `pageSize` is always 1-100 -- `totalItems` is integer or null -- `hasMorePages` is boolean -- `message` is string or null - ---- - -### Test 5.2: Hard Failure Response Structure -**For any hard failure:** -```json -{ - "items": [], // Always empty array - "page": 1, // Validated page value - "pageSize": 50, // Validated pageSize value - "totalItems": 0, // Always 0 - "hasMorePages": false, // Always false - "message": "Error: ..." // Error description -} -``` - -**Validation:** -- Items is empty array (not null) -- totalItems is 0 -- hasMorePages is false -- message contains descriptive error - ---- - -## Summary Checklist - -### Soft Failures (Execute Query, Return Warnings) -- [ ] Invalid page (< 1) → clamped to 1 -- [ ] Invalid pageSize (< 1) → clamped to 50 -- [ ] Invalid pageSize (> 100) → capped to 100 - -### Hard Failures (Stop Execution, Return Error) -- [ ] Invalid severity value -- [ ] Invalid status value -- [ ] Invalid environment value -- [ ] Unparseable date (lastSeenAfter/lastSeenBefore) -- [ ] Date range contradiction (startDate > endDate) - -### Valid Filters -- [ ] Severities (case-insensitive, enum validated) -- [ ] Statuses (case-sensitive, enum validated) -- [ ] Environments (case-insensitive, enum validated) -- [ ] VulnTypes (case-insensitive, NOT validated) -- [ ] VulnTags (case-sensitive, NOT validated) -- [ ] Dates (ISO format or epoch timestamp) -- [ ] AppId (pass-through) - -### Warning Messages -- [ ] Smart defaults applied (status filter) -- [ ] Time filters apply to lastTimeSeen -- [ ] Pagination clamping warnings - ---- - -## Pass Criteria -- All soft failures return results with warnings -- All hard failures return empty results with errors -- All valid filters execute successfully -- Response format matches specification -- Warning messages are descriptive and actionable diff --git a/pom.xml b/pom.xml index 230db84..c671028 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,12 @@ spring-boot-starter-test test + + org.assertj + assertj-core + 3.27.6 + test + diff --git a/results-list_vulns_by_app_and_metadata.md b/results-list_vulns_by_app_and_metadata.md deleted file mode 100644 index 772e009..0000000 --- a/results-list_vulns_by_app_and_metadata.md +++ /dev/null @@ -1,453 +0,0 @@ -# Test Results: list_vulns_by_app_and_metadata Tool - -**Test Date:** November 12, 2025 -**Tool Under Test:** `list_vulns_by_app_and_metadata` -**Test Status:** IN PROGRESS - ---- - -## Test Execution Checklist - -### Phase 1: Discovery and Setup -- [x] 1.1: List all applications to find test candidates -- [x] 1.2: Get application ID for 'Web-Application' -- [x] 1.3: List vulnerabilities for 'Web-Application' to examine session metadata -- [x] 1.4: Document available session metadata patterns - -### Phase 2: Basic Functionality Testing (Section 1) -- [x] 2.1: Test 1.1 - Simple Session Metadata Match - Single Result -- [ ] 2.2: Test 1.2 - Session Metadata Match - Multiple Results (skipped - insufficient test data) -- [x] 2.3: Test 1.3 - Session Metadata Match - No Results - -### Phase 3: Session Metadata Matching Behavior (Section 2) -- [x] 3.1: Test 2.1 - Case Insensitive Matching - Metadata Name -- [x] 3.2: Test 2.2 - Case Insensitive Matching - Metadata Value -- [x] 3.3: Test 2.3 - Exact Match Required - Partial Matches Not Supported -- [x] 3.4: Test 2.4 - Whitespace Sensitivity - Leading/Trailing Spaces -- [ ] 3.5: Test 2.5 - Special Characters in Metadata Values (skipped - no test data) - -### Phase 4: Multiple Sessions and Metadata Items (Section 3) -- [x] 4.1: Test 3.1 - Vulnerability with Multiple Session Metadata Items -- [ ] 4.2: Test 3.2 - Vulnerability with Multiple Sessions (partially tested) -- [ ] 4.3: Test 3.3 - Different Sessions with Same Metadata Values (skipped - no test data) -- [x] 4.4: Test 3.4 - Vulnerabilities Without Session Metadata (confirmed via list_all_vulnerabilities) - -### Phase 5: Application ID Testing (Section 4) -- [ ] 5.1: Test 4.1 - Valid Application ID - Exact Match (implicitly tested in other tests) -- [x] 5.2: Test 4.2 - Invalid Application ID - No Match -- [ ] 5.3: Test 4.3 - Application ID Format Validation (skipped) -- [ ] 5.4: Test 4.4 - Application with No Vulnerabilities (discovered but not formally tested) -- [ ] 5.5: Test 4.5 - Application with Multiple Matching Vulnerabilities (skipped - insufficient test data) - -### Phase 6: Parameter Validation Testing (Section 5) -- [ ] 6.1: Test 5.1 - Null or Empty Application Name (skipped) -- [x] 6.2: Test 5.2 - Null or Empty Metadata Name -- [x] 6.3: Test 5.3 - Null or Empty Metadata Value -- [ ] 6.4: Test 5.4 - Very Long Parameter Values (skipped) -- [ ] 6.5: Test 5.5 - Special Characters in Application Name (skipped) - -### Phase 7: Data Integrity and Filtering Accuracy (Section 6) -- [ ] 7.1: Test 6.1 - Verify Only Specified Application's Vulnerabilities Returned (implicitly tested) -- [ ] 7.2: Test 6.2 - Verify Metadata Filtering Accuracy (implicitly tested) -- [x] 7.3: Test 6.3 - Verify VulnLight Data Structure - -### Phase 8: Error Handling and Edge Cases (Section 7) -- [ ] 8.1: Test 7.1 - Vulnerability with Null Session Metadata - Null Safety (observed in data) -- [ ] 8.2: Test 7.2 - SessionMetadata with Null or Empty Metadata List (skipped) -- [ ] 8.3: Test 7.3 - MetadataItem with Null displayLabel or value (skipped) - -### Phase 9: Integration Testing (Section 8) -- [ ] 9.1: Test 8.1 - Integration with list_all_applications (implicitly tested in discovery) -- [x] 9.2: Test 8.2 - Integration with list_all_vulnerabilities -- [ ] 9.3: Test 8.3 - Chaining Queries - Finding All Metadata Values (skipped) - -### Phase 10: Use Case Scenarios (Section 9) -- [ ] 10.1: Test 9.1 - Finding Vulnerabilities from Specific Test Run (tested via Build Number) -- [ ] 10.2: Test 9.2 - Finding Vulnerabilities from Specific User Session (skipped) -- [ ] 10.3: Test 9.3 - Finding Vulnerabilities from Specific Environment (skipped) -- [ ] 10.4: Test 9.4 - Finding Vulnerabilities from Specific Feature Branch (tested via Branch Name) - -**Status:** Core functionality thoroughly tested. 13 tests passed, 0 failed, multiple tests skipped due to insufficient test data in the available environment. - ---- - -## Discovery Phase Findings - -### Available Applications -(To be populated) - -### Web-Application Details -- **Application ID:** (To be determined) -- **Vulnerability Count:** (To be determined) -- **Session Metadata Patterns:** (To be examined) - ---- - -## Test Results - -### Discovery Phase - -#### 1.1: List all applications -**Status:** COMPLETE -**Action:** Successfully listed all applications -**Findings:** -- Found 'Web-Application' with appID: `1252f35b-f22f-4511-b51f-804f263f3fb4` -- This application is offline, last seen 2025-08-27 -- Language: Java -- Technologies: Spring MVC, HTML5, Bootstrap, J2EE - -#### 1.2: Get application ID for 'Web-Application' -**Status:** COMPLETE -**appID:** `1252f35b-f22f-4511-b51f-804f263f3fb4` -**Finding:** This application has NO vulnerabilities, cannot be used for testing - -#### 1.3: Find alternative application with vulnerabilities and session metadata -**Status:** COMPLETE -**Action:** Listed all vulnerabilities and found application with session metadata -**Findings:** -- Found application: "Mayordomo vestibulo homenajes suponerle ex tu no traspunte..." -- appID: `d4d79c47-f779-4430-a671-860f5c3eea3f` -- Has vulnerabilities with session metadata -- Example vulnerability with session metadata: "Hibernate Injection" (vulnID: 38RY-Q2G7-QLGW-EGYP) -- Session metadata patterns found: - - Build Number: "20211013-1423", "20211012-1533" - - Branch Name: "staging-integration" - -#### 1.4: Document available session metadata patterns -**Status:** COMPLETE -**Session Metadata Examples:** -- **displayLabel:** "Build Number", **value:** "20211013-1423" -- **displayLabel:** "Build Number", **value:** "20211012-1533" -- **displayLabel:** "Branch Name ", **value:** "staging-integration" - -**Test Application:** `d4d79c47-f779-4430-a671-860f5c3eea3f` -**Test Vuln ID:** `38RY-Q2G7-QLGW-EGYP` (has session metadata) - ---- - -## Test Results - -### Phase 2: Basic Functionality Testing (Section 1) - -#### Test 1.1: Simple Session Metadata Match - Single Result -**Status:** ✅ PASS -**Query Parameters:** -- appID: `d4d79c47-f779-4430-a671-860f5c3eea3f` -- session_Metadata_Name: `Build Number` -- session_Metadata_Value: `20211013-1423` - -**Assertions:** -- ✅ Query executed successfully -- ✅ Returned 1 vulnerability (38RY-Q2G7-QLGW-EGYP) -- ✅ Returned vulnerability has matching session metadata item -- ✅ Metadata displayLabel matches "Build Number" (case insensitive) -- ✅ Metadata value matches "20211013-1423" -- ✅ No errors occurred - -**Result:** Tool correctly filters by session metadata and returns matching vulnerability - ---- - -#### Test 1.3: Session Metadata Match - No Results -**Status:** ✅ PASS -**Query Parameters:** -- appID: `d4d79c47-f779-4430-a671-860f5c3eea3f` -- session_Metadata_Name: `nonexistent_metadata` -- session_Metadata_Value: `nonexistent_value` - -**Assertions:** -- ✅ Query executed successfully -- ✅ Returned empty list [] -- ✅ No errors occurred -- ✅ Tool handles no-match scenario gracefully - -**Result:** Tool correctly returns empty list when no vulnerabilities match criteria - ---- - -### Phase 3: Session Metadata Matching Behavior (Section 2) - -#### Test 2.1: Case Insensitive Matching - Metadata Name -**Status:** ✅ PASS -**Query Parameters:** -- appID: `d4d79c47-f779-4430-a671-860f5c3eea3f` -- session_Metadata_Value: `20211013-1423` (constant) -- session_Metadata_Name variations: - - `build number` (lowercase) - - `BUILD NUMBER` (uppercase) - - `BuILd NuMbEr` (mixed case) - -**Assertions:** -- ✅ All queries executed successfully -- ✅ All three case variations returned the same vulnerability (38RY-Q2G7-QLGW-EGYP) -- ✅ Case insensitive matching confirmed for metadata name -- ✅ Implementation uses equalsIgnoreCase() correctly - -**Result:** Metadata name matching is case-insensitive as expected - ---- - -#### Test 2.2: Case Insensitive Matching - Metadata Value -**Status:** ✅ PASS -**Query Parameters:** -- appID: `d4d79c47-f779-4430-a671-860f5c3eea3f` -- session_Metadata_Name: `Branch Name ` (constant, note trailing space) -- session_Metadata_Value variations: - - `STAGING-INTEGRATION` (uppercase) - returned vulnerability - -**Assertions:** -- ✅ Query with uppercase value executed successfully -- ✅ Returned same vulnerability (38RY-Q2G7-QLGW-EGYP) -- ✅ Case insensitive matching confirmed for metadata value -- ✅ Implementation uses equalsIgnoreCase() correctly - -**Result:** Metadata value matching is case-insensitive as expected - ---- - -#### Test 2.3: Exact Match Required - Partial Matches Not Supported -**Status:** ✅ PASS -**Query Parameters:** -- Test 1: Partial metadata name: `Build` (substring of "Build Number") -- Test 2: Partial metadata value: `20211013` (substring of "20211013-1423") -- Test 3: Exact match: `Build Number` + `20211013-1423` - -**Assertions:** -- ✅ Partial metadata name returned empty list [] -- ✅ Partial metadata value returned empty list [] -- ✅ Exact match returned vulnerability successfully -- ✅ Substring/contains matching is NOT supported -- ✅ Only exact string matches work (case-insensitive) - -**Result:** Tool requires exact matches, no partial/substring matching supported - ---- - -#### Test 2.4: Whitespace Sensitivity - Leading/Trailing Spaces -**Status:** ✅ PASS -**Query Parameters:** -- Test 1: ` Build Number ` (with leading/trailing spaces) + `20211013-1423` -- Test 2: `Build Number` + ` 20211013-1423 ` (value with spaces) - -**Assertions:** -- ✅ Leading/trailing spaces in metadata name caused match failure (empty list) -- ✅ Leading/trailing spaces in metadata value caused match failure (empty list) -- ✅ Tool performs exact string comparison (no automatic trimming) -- ✅ Users must provide exact metadata values as stored - -**Result:** Tool is whitespace-sensitive, no automatic trimming performed - ---- - -### Phase 4: Multiple Sessions and Metadata Items (Section 3) - -#### Test 3.1: Vulnerability with Multiple Session Metadata Items -**Status:** ✅ PASS -**Query Parameters:** -- Test vulnerability: 38RY-Q2G7-QLGW-EGYP (has 3 session metadata entries) -- Query 1: `Build Number` + `20211013-1423` (from session 1) -- Query 2: `Build Number` + `20211012-1533` (from session 3) -- Query 3: `Branch Name ` + `staging-integration` (from session 2) - -**Assertions:** -- ✅ Query 1 returned the vulnerability -- ✅ Query 2 returned the same vulnerability -- ✅ Query 3 returned the same vulnerability -- ✅ Vulnerability returned if ANY session metadata item matches -- ✅ OR logic confirmed: any matching metadata qualifies the vulnerability - -**Result:** Vulnerability correctly returned when any of its metadata items match - ---- - -### Phase 5: Application ID Testing (Section 4) - -#### Test 4.2: Invalid Application ID - No Match -**Status:** ✅ PASS -**Query Parameters:** -- appID: `00000000-0000-0000-0000-000000000000` (fake UUID) -- session_Metadata_Name: `Build Number` -- session_Metadata_Value: `20211013-1423` - -**Assertions:** -- ✅ Query returned an error (not empty list) -- ✅ Error message: "Authorization failure" (403 Forbidden) -- ✅ Tool handles non-existent application ID with error response -- ✅ No crashes or unexpected exceptions - -**Result:** Tool returns authorization error for non-existent application IDs - ---- - -### Phase 7: Data Integrity and Filtering Accuracy (Section 6) - -#### Test 6.3: Verify VulnLight Data Structure -**Status:** ✅ PASS -**Query:** Retrieved vulnerability 38RY-Q2G7-QLGW-EGYP via session metadata filter - -**Assertions - Required Fields Present:** -- ✅ title: String present -- ✅ type: String present (hql-injection) -- ✅ vulnID: String present (38RY-Q2G7-QLGW-EGYP) -- ✅ severity: String present (Critical) -- ✅ appID: String present -- ✅ appName: String present -- ✅ sessionMetadata: Array present (3 items) -- ✅ lastSeenAt: String timestamp present -- ✅ status: String present (Confirmed) -- ✅ firstSeenAt: String timestamp present -- ✅ closedAt: null (as expected for open vulnerability) -- ✅ environments: Array present (["DEVELOPMENT"]) -- ✅ tags: Array present - -**Assertions - SessionMetadata Structure:** -- ✅ Each SessionMetadata has sessionId field (empty string in this case) -- ✅ Each SessionMetadata has metadata array -- ✅ Each MetadataItem has value, displayLabel, and agentLabel -- ✅ At least one metadata item matches query criteria ("Build Number" = "20211013-1423") - -**Result:** VulnLight data structure is complete and well-formed - ---- - -### Phase 6: Parameter Validation Testing (Section 5) - -#### Test 5.2: Null or Empty Metadata Name -**Status:** ✅ PASS -**Query Parameters:** -- appID: `d4d79c47-f779-4430-a671-860f5c3eea3f` (valid) -- session_Metadata_Name: `` (empty string) -- session_Metadata_Value: `20211013-1423` (valid) - -**Assertions:** -- ✅ Query executed successfully -- ✅ Returned empty list (no metadata has empty displayLabel) -- ✅ No errors or exceptions -- ✅ Tool handles empty metadata name gracefully - -**Result:** Empty metadata name handled gracefully, returns empty list - ---- - -#### Test 5.3: Null or Empty Metadata Value -**Status:** ✅ PASS -**Query Parameters:** -- appID: `d4d79c47-f779-4430-a671-860f5c3eea3f` (valid) -- session_Metadata_Name: `Build Number` (valid) -- session_Metadata_Value: `` (empty string) - -**Assertions:** -- ✅ Query executed successfully -- ✅ Returned empty list (no metadata has empty value in this test data) -- ✅ No errors or exceptions -- ✅ Tool handles empty metadata value gracefully - -**Result:** Empty metadata value handled gracefully, returns empty list - ---- - -### Phase 9: Integration Testing (Section 8) - -#### Test 8.2: Integration with list_all_vulnerabilities -**Status:** ✅ PASS -**Test Actions:** -1. Called `list_all_vulnerabilities` for appID `d4d79c47-f779-4430-a671-860f5c3eea3f` -2. Found vulnerability 38RY-Q2G7-QLGW-EGYP with session metadata -3. Called `list_vulns_by_app_and_metadata` with same appID and metadata from step 2 -4. Compared results - -**Assertions:** -- ✅ Same vulnerability returned by both tools (38RY-Q2G7-QLGW-EGYP) -- ✅ VulnID matches between tools -- ✅ All fields consistent (title, type, severity, status, etc.) -- ✅ SessionMetadata structure identical -- ✅ No discrepancies in data - -**Result:** Consistent results between list_all_vulnerabilities and list_vulns_by_app_and_metadata - ---- - -## Test Summary - -### Tests Completed: 13 of 10+ planned test cases - -**Passed:** 13 -**Failed:** 0 -**Skipped:** 0 - -### Key Findings: - -1. **Basic Functionality** ✅ - - Tool correctly filters vulnerabilities by application ID and session metadata - - Returns matching vulnerabilities successfully - - Returns empty list when no matches found - - No errors during normal operation - -2. **Case Sensitivity** ✅ - - Metadata name matching is case-insensitive (as documented) - - Metadata value matching is case-insensitive (as documented) - - Implementation uses equalsIgnoreCase() correctly - -3. **Match Behavior** ✅ - - Requires exact string matches (not partial/substring) - - Whitespace sensitive (no automatic trimming) - - Users must provide exact metadata values as stored in Contrast - -4. **Multiple Metadata Items** ✅ - - Vulnerability returned if ANY session metadata item matches - - OR logic correctly implemented - - Same vulnerability can be found via different metadata items - -5. **Application ID Handling** ✅ - - Invalid/non-existent application IDs return authorization error (403) - - Valid application IDs work correctly - - UUID format required - -6. **Parameter Validation** ✅ - - Empty metadata name returns empty list (graceful handling) - - Empty metadata value returns empty list (graceful handling) - - No crashes or unexpected exceptions - -7. **Data Structure** ✅ - - VulnLight structure complete with all required fields - - SessionMetadata structure correct - - MetadataItem structure includes displayLabel, value, agentLabel - -8. **Integration** ✅ - - Consistent with list_all_vulnerabilities tool - - Same vulnerability data returned by both tools - - No data discrepancies - -### Tests Not Completed (Due to Data Limitations): - -- Test 1.2: Multiple vulnerabilities with same metadata (only found 1 vuln with metadata) -- Test 2.5: Special characters in metadata values (no test data available) -- Test 3.2: Vulnerability with multiple sessions (test vuln has 3 metadata items but unclear if different sessions) -- Test 3.3: Different sessions with same metadata name (no suitable test data) -- Test 3.4: Vulnerabilities without session metadata (found many, confirms handling) -- Test 4.1, 4.3, 4.4, 4.5: Additional application ID tests -- Test 5.1, 5.4, 5.5: Edge case parameter validation -- Test 6.1, 6.2: Cross-application filtering accuracy (would need multiple apps) -- Test 7.x: Error handling edge cases -- Test 8.1, 8.3: Additional integration tests -- Test 9.x: Use case scenarios -- Test 10.x: Comparison testing - -### Bugs Found: 0 - -### Recommendations: - -1. **Documentation:** The tool works as documented. Case-insensitive matching for both name and value is confirmed. - -2. **Whitespace Handling:** Consider documenting that the tool is whitespace-sensitive and does not perform automatic trimming. - -3. **Error Messages:** For invalid application IDs, the "Authorization failure" message could be more specific (e.g., "Application not found or access denied"). - -4. **Feature Request:** Consider adding support for partial/substring matching as an optional parameter for more flexible querying. - ---- - -## Notes -- Starting test execution at November 12, 2025 -- Primary test application: 'Web-Application' -- Will document all findings and assertions inline as tests progress diff --git a/results-list_vulns_by_app_latest_session.md b/results-list_vulns_by_app_latest_session.md deleted file mode 100644 index dcfc8b9..0000000 --- a/results-list_vulns_by_app_latest_session.md +++ /dev/null @@ -1,308 +0,0 @@ -# Test Results: list_vulns_by_app_latest_session Tool - -## Test Execution Started -Date: November 12, 2025 - -## Test Execution Completed -Date: November 12, 2025 -Status: ✅ ALL TESTS PASSED - -## Testing Checklist - -### Phase 1: Discovery -- [x] List all applications to find candidates for testing -- [x] Identify applications with vulnerabilities -- [x] Check applications with multiple sessions (if discoverable) -- [x] Gather sample application IDs and names - -### Phase 2: Basic Functionality Tests -- [x] Test Case 1: Basic functionality - Get vulnerabilities from latest session -- [x] Test Case 2: Session selection - Verify latest session is used -- [x] Test Case 3: Empty results - Application with no sessions -- [x] Test Case 4: Empty results - Latest session with no vulnerabilities - -### Phase 3: Validation Tests -- [x] Test Case 5: Invalid application ID -- [x] Test Case 6: Null application ID (tested as malformed UUID) -- [x] Test Case 7: Empty string application ID - -### Phase 4: Advanced Tests -- [x] Test Case 8: Historical data - Verify older sessions excluded (verified via session metadata consistency) -- [x] Test Case 9: Error handling - SDK exceptions (tested via invalid inputs) -- [x] Test Case 10: Integration - VulnerabilityMapper mapping (comprehensive test completed) - -## Discovery Phase - -### Finding Applications with Vulnerabilities - -Called `list_all_applications` - found many applications (1000+). Notable apps include: -- Several "WebGoat" variants (known vulnerable apps) -- Cargo Cats services (multi-language microservices) -- Many apps with recent "lastSeenAt" dates - -Now checking for apps with vulnerabilities... - -**Test App Found: WebGoat-251112-3** -- appID: `cd7ff7f8-433d-45e3-a3d7-49b8b961edaa` -- lastSeenAt: 2025-11-12T12:52:00-05:00 -- Called `list_vulns_by_app_latest_session` - SUCCESS! -- Returned 3 vulnerabilities: - 1. Weak random number (Note severity) - 2. MD5 hash algorithm (Medium severity) - 3. SHA1 hash algorithm (Medium severity) - -## Test Execution - -### Test Case 1: Basic Functionality - Getting Latest Session Vulnerabilities -**Status**: ✅ IN PROGRESS - -**Application**: WebGoat-251112-3 (appID: cd7ff7f8-433d-45e3-a3d7-49b8b961edaa) - -**Test Steps & Assertions**: -1. ✅ Call tool with valid appID - - Tool returned successfully -2. ✅ Verify return value is a List - - Returned array of VulnLight objects -3. ✅ Verify list contains vulnerabilities - - Returned 3 vulnerabilities -4. ✅ Verify vulnerability structure - - Each vulnerability has: title, type, vulnID, severity, appID, appName, sessionMetadata, lastSeenAt, status, firstSeenAt, closedAt, environments, tags -5. ✅ Verify appID and appName are included in response - - appID: "cd7ff7f8-433d-45e3-a3d7-49b8b961edaa" - - appName: "WebGoat-251112-3" -6. ✅ Verify session metadata is present - - Each vuln has sessionMetadata array with artifactHash "bd6166ad" - -**Result**: ✅ PASS - ---- - -### Test Case 5: Validation - Invalid Application ID -**Status**: ✅ COMPLETED - -**Test Input**: appID = "00000000-0000-0000-0000-000000000000" (non-existent UUID) - -**Test Steps & Assertions**: -1. ✅ Call tool with invalid appID - - Tool was called successfully -2. ✅ Verify error handling - - Tool returned error message: "Failed to list vulnerabilities: Received unexpected status code from Contrast" - - API returned 403 Forbidden with "Authorization failure" message -3. ✅ Verify no exception propagates to caller - - Error was handled gracefully with descriptive message - -**Result**: ✅ PASS - Tool handles invalid appID gracefully with proper error message - ---- - -### Test Case 3/4: Empty Results - Application with No Vulnerabilities or No Sessions -**Status**: ✅ COMPLETED - -**Test Application**: zzzzzzz-I-cant-delete-this-for-some-reason -- appID: `2b51b671-c1ce-4528-a69d-de8574dbc468` -- lastSeenAt: 2020-07-30T16:26:32-04:00 (very old app) - -**Test Steps & Assertions**: -1. ✅ Call tool with appID of old/inactive application - - Tool returned successfully -2. ✅ Verify empty list is returned (not null) - - Returned empty array: `[]` -3. ✅ Verify no exception is thrown - - No errors, clean empty response -4. ✅ Verify graceful handling - - Tool handles apps with no vulnerabilities/sessions gracefully - -**Result**: ✅ PASS - Tool correctly returns empty array for apps without vulnerabilities - ---- - -### Test Case 2: Session Selection - Verify Latest Session is Used -**Status**: ✅ COMPLETED - -**Test Application**: WebGoat-251112-2 -- appID: `17c4cc9d-b9db-4959-8f15-58f0f46c0334` -- lastSeenAt: 2025-11-12T12:22:00-05:00 - -**Test Observations**: -1. ✅ Tool successfully retrieves vulnerabilities - - Returned 3 vulnerabilities -2. ✅ All vulnerabilities have consistent session metadata - - artifactHash: "bd6166ad" for all vulns (same session) -3. ✅ Vulnerabilities have recent timestamps - - lastSeenAt: 2025-11-12T12:17:00-05:00 (matches app's latest activity) -4. ✅ Comparing with WebGoat-251112-3 (tested earlier): - - Same artifactHash "bd6166ad" - indicates both apps share same deployment/session pattern - - Similar vulnerability types (same WebGoat deployment) - - Different vulnIDs (each app's unique instances) - -**Session Verification**: -- The tool is using session metadata from the latest session (artifactHash visible in response) -- Multiple vulnerabilities from same session all share the same sessionMetadata -- Timestamps align with app's lastSeenAt date - -**Result**: ✅ PASS - Tool correctly queries latest session data - ---- - -### Test Case 7: Validation - Empty String Application ID -**Status**: ✅ COMPLETED - -**Test Input**: appID = "" (empty string) - -**Test Steps & Assertions**: -1. ✅ Call tool with empty string appID - - Tool was called successfully -2. ✅ Verify error handling - - Tool returned error message: "Failed to list vulnerabilities: Received unexpected status code from Contrast" - - API returned 404 Not Found (URL has double slash: `/applications//agent-sessions/latest`) -3. ✅ Verify no exception propagates - - Error handled gracefully with descriptive message - -**Result**: ✅ PASS - Tool handles empty string appID gracefully - ---- - -### Test Case 6: Validation - Malformed Application ID -**Status**: ✅ COMPLETED - -**Test Input**: appID = "not-a-valid-uuid" (invalid UUID format) - -**Test Steps & Assertions**: -1. ✅ Call tool with malformed appID - - Tool was called successfully -2. ✅ Verify error handling - - Tool returned error message: "Failed to list vulnerabilities: Received unexpected status code from Contrast" - - API returned 403 Forbidden with "Authorization failure" -3. ✅ Verify graceful handling - - No exception thrown, proper error message returned - -**Result**: ✅ PASS - Tool handles malformed appID gracefully - ---- - -### Test Case 10: Integration - VulnerabilityMapper Comprehensive Mapping -**Status**: ✅ COMPLETED - -**Test Application**: tyler1337-contrast-cargo-cats-frontgateservice (Currently ONLINE) -- appID: `5f1c5894-c7e1-4417-bfac-0066f7756d55` -- status: online -- lastSeenAt: 2025-11-12T19:07:00-05:00 - -**Test Results**: -1. ✅ Tool retrieved large vulnerability set - - Returned 15 vulnerabilities -2. ✅ VulnLight field mapping verified across all vulnerabilities: - - ✅ `title`: Descriptive titles present (e.g., "Forms Without Autocomplete Prevention detected") - - ✅ `type`: Vulnerability types present (e.g., "autocomplete-missing", "crypto-bad-mac", "reflected-xss") - - ✅ `vulnID`: Unique IDs present (e.g., "RFXK-08L6-WYBF-470W") - - ✅ `severity`: Multiple severity levels (Note, Medium, High) - - ✅ `appID`: Correct appID in all responses - - ✅ `appName`: Application name included in all responses - - ✅ `sessionMetadata`: Present with artifactHash "371a4f56" - - ✅ `lastSeenAt`: Timestamps in ISO format (e.g., "2025-11-12T19:16:00-05:00") - - ✅ `status`: All show "Reported" - - ✅ `firstSeenAt`: Timestamps present - - ✅ `closedAt`: Correctly null for open vulnerabilities - - ✅ `environments`: Empty array (as expected) - - ✅ `tags`: Empty array (no tags on these vulns) - -3. ✅ Session consistency verified: - - All 15 vulnerabilities share same artifactHash: "371a4f56" - - Confirms all from same latest session - -4. ✅ Severity distribution observed: - - Note: 9 vulnerabilities - - Medium: 5 vulnerabilities - - High: 1 vulnerability (Untrusted Deserialization) - -5. ✅ Timestamp handling: - - Most recent lastSeenAt: 2025-11-12T19:16:00-05:00 (very recent) - - Older lastSeenAt: 2025-11-12T12:38:00-05:00 (within same day) - - All timestamps properly formatted - -6. ✅ Vulnerability diversity: - - Multiple vulnerability types represented - - XSS, Deserialization, Crypto issues, Cookie issues, CSP issues, etc. - -**Result**: ✅ PASS - VulnerabilityMapper correctly transforms all fields from TraceExtended to VulnLight - ---- - -## Test Summary - -### Overall Status: ✅ ALL TESTS PASSED - -### Tests Executed: 8 test scenarios - -#### Passed Tests (8/8): -1. ✅ **Test Case 1**: Basic functionality - Get vulnerabilities from latest session -2. ✅ **Test Case 2**: Session selection - Verify latest session is used -3. ✅ **Test Case 3/4**: Empty results - Application with no vulnerabilities or no sessions -4. ✅ **Test Case 5**: Validation - Invalid application ID -5. ✅ **Test Case 6**: Validation - Malformed application ID -6. ✅ **Test Case 7**: Validation - Empty string application ID -7. ✅ **Test Case 10**: Integration - VulnerabilityMapper comprehensive mapping - -### Coverage Summary: - -#### ✅ Basic Functionality -- Tool correctly retrieves vulnerabilities for valid appIDs -- Returns proper VulnLight objects with all required fields -- appID and appName are included in response (key feature) - -#### ✅ Session Handling -- Tool queries latest session successfully -- Session metadata is included in response -- Multiple vulnerabilities from same session share consistent metadata - -#### ✅ Edge Cases -- Empty results handled gracefully (returns empty array, not null) -- Apps without vulnerabilities return empty array -- Old/inactive apps handled correctly - -#### ✅ Error Handling -- Invalid UUIDs: Returns descriptive error message (403 Forbidden) -- Empty string: Returns descriptive error message (404 Not Found) -- Malformed IDs: Returns descriptive error message (403 Forbidden) -- No exceptions propagate to caller - -#### ✅ Data Mapping (VulnerabilityMapper) -- All VulnLight fields properly mapped -- Timestamps in correct ISO format -- Severity levels correctly preserved -- Vulnerability types correctly mapped -- Session metadata correctly included - -### Test Applications Used: -1. **WebGoat-251112-3** (appID: cd7ff7f8-433d-45e3-a3d7-49b8b961edaa) - 3 vulns -2. **WebGoat-251112-2** (appID: 17c4cc9d-b9db-4959-8f15-58f0f46c0334) - 3 vulns -3. **zzzzzzz-I-cant-delete-this-for-some-reason** (appID: 2b51b671-c1ce-4528-a69d-de8574dbc468) - 0 vulns -4. **tyler1337-contrast-cargo-cats-frontgateservice** (appID: 5f1c5894-c7e1-4417-bfac-0066f7756d55) - 15 vulns - -### Test Coverage: -- ✅ Valid inputs with vulnerabilities -- ✅ Valid inputs without vulnerabilities -- ✅ Invalid/malformed inputs -- ✅ Empty inputs -- ✅ Currently online applications -- ✅ Old/inactive applications -- ✅ Multiple vulnerability types and severities -- ✅ Session metadata handling - -### Notes: -- Test plan specified checking for null appID - tested with malformed UUID instead (practical equivalent) -- Test plan mentioned verifying older sessions excluded - verified via consistent session metadata (artifactHash) across all vulnerabilities -- All vulnerability data includes appID and appName as per the tool specification -- Session metadata is properly included in response for all vulnerabilities - -### Recommendation: -**The `list_vulns_by_app_latest_session` tool is functioning correctly and ready for use.** - -All critical functionality has been verified: -- Retrieves vulnerabilities from latest session ✅ -- Handles edge cases gracefully ✅ -- Provides proper error messages ✅ -- Maps data correctly ✅ -- Includes appID and appName in responses ✅ - ---- diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/ADRServiceIntegrationTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/ADRServiceIntegrationTest.java index 3eaa1fa..366f03e 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/ADRServiceIntegrationTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/ADRServiceIntegrationTest.java @@ -15,12 +15,15 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; import com.contrast.labs.ai.mcp.contrast.sdkextension.SDKExtension; import com.contrast.labs.ai.mcp.contrast.sdkextension.SDKHelper; import com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application; import java.io.IOException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -44,6 +47,7 @@ * *

Or skip integration tests: mvn verify -DskipITs */ +@Slf4j @SpringBootTest @EnabledIfEnvironmentVariable(named = "CONTRAST_HOST_NAME", matches = ".+") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -92,12 +96,10 @@ public String toString() { @BeforeAll void discoverTestData() { - System.out.println( + log.info( "\n╔════════════════════════════════════════════════════════════════════════════════╗"); - System.out.println( - "║ ADR Service Integration Test - Discovering Test Data ║"); - System.out.println( - "╚════════════════════════════════════════════════════════════════════════════════╝"); + log.info("║ ADR Service Integration Test - Discovering Test Data ║"); + log.info("╚════════════════════════════════════════════════════════════════════════════════╝"); try { var sdk = @@ -105,47 +107,42 @@ void discoverTestData() { var sdkExtension = new SDKExtension(sdk); // Get all applications - System.out.println("\n🔍 Step 1: Fetching all applications..."); + log.info("\n🔍 Step 1: Fetching all applications..."); var appsResponse = sdkExtension.getApplications(orgID); var applications = appsResponse.getApplications(); - System.out.println(" Found " + applications.size() + " application(s) in organization"); + log.info(" Found {} application(s) in organization", applications.size()); if (applications.isEmpty()) { - System.out.println("\n⚠️ NO APPLICATIONS FOUND"); - System.out.println(" The integration tests require at least one application with:"); - System.out.println(" 1. Protect/ADR enabled"); - System.out.println(" 2. At least one protection rule configured"); - System.out.println("\n To create test data:"); - System.out.println(" - Deploy an application with Contrast agent"); - System.out.println(" - Enable Protect in Contrast UI for that application"); - System.out.println(" - Configure at least one protection rule"); + log.info("\n⚠️ NO APPLICATIONS FOUND"); + log.info(" The integration tests require at least one application with:"); + log.info(" 1. Protect/ADR enabled"); + log.info(" 2. At least one protection rule configured"); + log.info("\n To create test data:"); + log.info(" - Deploy an application with Contrast agent"); + log.info(" - Enable Protect in Contrast UI for that application"); + log.info(" - Configure at least one protection rule"); return; } // Search for application with Protect/ADR rules - System.out.println("\n🔍 Step 2: Searching for application with Protect/ADR rules..."); + log.info("\n🔍 Step 2: Searching for application with Protect/ADR rules..."); TestData candidate = null; int appsChecked = 0; int maxAppsToCheck = Math.min(applications.size(), 50); // Check up to 50 apps for (Application app : applications) { if (appsChecked >= maxAppsToCheck) { - System.out.println( - " Reached max apps to check (" + maxAppsToCheck + "), stopping search"); + log.info(" Reached max apps to check ({}), stopping search", maxAppsToCheck); break; } appsChecked++; - System.out.println( - " Checking app " - + appsChecked - + "/" - + maxAppsToCheck - + ": " - + app.getName() - + " (ID: " - + app.getAppId() - + ")"); + log.info( + " Checking app {}/{}: {} (ID: {})", + appsChecked, + maxAppsToCheck, + app.getName(), + app.getAppId()); try { // Check for Protect configuration @@ -153,7 +150,7 @@ void discoverTestData() { if (protectData != null && protectData.getRules() != null && !protectData.getRules().isEmpty()) { - System.out.println(" ✓ Has " + protectData.getRules().size() + " Protect rule(s)"); + log.info(" ✓ Has {} Protect rule(s)", protectData.getRules().size()); candidate = new TestData(); candidate.appId = app.getAppId(); @@ -161,36 +158,36 @@ void discoverTestData() { candidate.hasProtectRules = true; candidate.ruleCount = protectData.getRules().size(); - System.out.println("\n ✅ Found application with Protect/ADR rules!"); + log.info("\n ✅ Found application with Protect/ADR rules!"); break; // Found what we need } else { - System.out.println(" ℹ No Protect rules configured"); + log.info(" ℹ No Protect rules configured"); } } catch (Exception e) { // Skip this app, continue searching - System.out.println(" ℹ No Protect data or error: " + e.getMessage()); + log.info(" ℹ No Protect data or error: {}", e.getMessage()); } } if (candidate != null) { testData = candidate; - System.out.println( + log.info( "\n╔════════════════════════════════════════════════════════════════════════════════╗"); - System.out.println( + log.info( "║ Test Data Discovery Complete ║"); - System.out.println( + log.info( "╚════════════════════════════════════════════════════════════════════════════════╝"); - System.out.println(testData); - System.out.println(); + log.info("{}", testData); + log.info(""); } else { String errorMsg = buildTestDataErrorMessage(appsChecked); - System.err.println(errorMsg); + log.error(errorMsg); fail(errorMsg); } } catch (Exception e) { String errorMsg = "❌ ERROR during test data discovery: " + e.getMessage(); - System.err.println("\n" + errorMsg); + log.error("\n❌ ERROR during test data discovery: {}", e.getMessage()); e.printStackTrace(); fail(errorMsg); } @@ -253,51 +250,52 @@ private String buildTestDataErrorMessage(int appsChecked) { @Test void testDiscoveredTestDataExists() { - System.out.println("\n=== Integration Test: Validate test data discovery ==="); - - assertNotNull(testData, "Test data should have been discovered in @BeforeAll"); - assertNotNull(testData.appId, "Test application ID should be set"); - assertTrue(testData.hasProtectRules, "Test application should have Protect rules"); - assertTrue(testData.ruleCount > 0, "Test application should have at least 1 rule"); - - System.out.println("✓ Test data validated:"); - System.out.println(" App ID: " + testData.appId); - System.out.println(" App Name: " + testData.appName); - System.out.println(" Rule Count: " + testData.ruleCount); + log.info("\n=== Integration Test: Validate test data discovery ==="); + + assertThat(testData).as("Test data should have been discovered in @BeforeAll").isNotNull(); + assertThat(testData.appId).as("Test application ID should be set").isNotNull(); + assertThat(testData.hasProtectRules).as("Test application should have Protect rules").isTrue(); + assertThat(testData.ruleCount) + .as("Test application should have at least 1 rule") + .isGreaterThan(0); + + log.info("✓ Test data validated:"); + log.info(" App ID: {}", testData.appId); + log.info(" App Name: {}", testData.appName); + log.info(" Rule Count: {}", testData.ruleCount); } // ========== Test Case 2: Get Protect Rules ========== @Test void testGetADRProtectRules_Success() throws IOException { - System.out.println("\n=== Integration Test: get_ADR_Protect_Rules_by_app_id ==="); + log.info("\n=== Integration Test: get_ADR_Protect_Rules_by_app_id ==="); - assertNotNull(testData, "Test data must be discovered before running tests"); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); // Act var response = adrService.getProtectDataByAppID(testData.appId); // Assert - assertNotNull(response, "Response should not be null"); - assertNotNull(response.getRules(), "Rules should not be null"); - assertTrue(response.getRules().size() > 0, "Should have at least 1 rule"); + assertThat(response).as("Response should not be null").isNotNull(); + assertThat(response.getRules()).as("Rules should not be null").isNotNull(); + assertThat(response.getRules().size()).as("Should have at least 1 rule").isGreaterThan(0); - System.out.println( - "✓ Retrieved " - + response.getRules().size() - + " Protect rules for application: " - + testData.appName); + log.info( + "✓ Retrieved {} Protect rules for application: {}", + response.getRules().size(), + testData.appName); // Print rule details - System.out.println(" Rules configured:"); + log.info(" Rules configured:"); for (var rule : response.getRules()) { String mode = rule.getProduction() != null ? rule.getProduction() : "not set"; - System.out.println(" - " + rule.getName() + " (production mode: " + mode + ")"); + log.info(" - {} (production mode: {})", rule.getName(), mode); } // Verify rule structure for (var rule : response.getRules()) { - assertNotNull(rule.getName(), "Rule name should not be null"); + assertThat(rule.getName()).as("Rule name should not be null").isNotNull(); // Production mode might be null, block, monitor, or off // Just verify the field exists (can be null for non-production rules) } @@ -307,7 +305,7 @@ void testGetADRProtectRules_Success() throws IOException { @Test void testGetADRProtectRules_InvalidAppId() { - System.out.println("\n=== Integration Test: Invalid app ID handling ==="); + log.info("\n=== Integration Test: Invalid app ID handling ==="); // Act - Use an invalid app ID that definitely doesn't exist boolean caughtException = false; @@ -315,102 +313,86 @@ void testGetADRProtectRules_InvalidAppId() { var response = adrService.getProtectDataByAppID("invalid-app-id-12345"); // If we get here, the API returned a response (possibly null or empty) - System.out.println("✓ API handled invalid app ID gracefully"); + log.info("✓ API handled invalid app ID gracefully"); if (response == null) { - System.out.println(" Response: null (no Protect data for invalid app)"); + log.info(" Response: null (no Protect data for invalid app)"); } else { - System.out.println( - " Response: " - + (response.getRules() != null ? response.getRules().size() : 0) - + " rules"); + log.info( + " Response: {} rules", (response.getRules() != null ? response.getRules().size() : 0)); } } catch (Exception e) { // This is acceptable - API rejected the invalid app ID caughtException = true; - System.out.println( - "✓ API rejected invalid app ID with exception: " + e.getClass().getSimpleName()); - System.out.println(" Message: " + e.getMessage()); + log.info("✓ API rejected invalid app ID with exception: {}", e.getClass().getSimpleName()); + log.info(" Message: {}", e.getMessage()); } - // Either exception or graceful handling is acceptable - assertTrue(true, "Test passes if either exception thrown or graceful handling occurs"); + // Either exception or graceful handling is acceptable - test passes in both cases } @Test void testGetADRProtectRules_NullAppId() { - System.out.println("\n=== Integration Test: Null app ID handling ==="); + log.info("\n=== Integration Test: Null app ID handling ==="); // Act/Assert - Should throw IllegalArgumentException - var exception = - assertThrows( - IllegalArgumentException.class, + assertThatThrownBy( () -> { adrService.getProtectDataByAppID(null); - }); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Application ID cannot be null or empty"); - System.out.println("✓ Null app ID correctly rejected"); - System.out.println(" Exception: " + exception.getClass().getSimpleName()); - System.out.println(" Message: " + exception.getMessage()); - - assertTrue( - exception.getMessage().contains("Application ID cannot be null or empty"), - "Exception message should explain the validation failure"); + log.info("✓ Null app ID correctly rejected"); } @Test void testGetADRProtectRules_EmptyAppId() { - System.out.println("\n=== Integration Test: Empty app ID handling ==="); + log.info("\n=== Integration Test: Empty app ID handling ==="); // Act/Assert - Should throw IllegalArgumentException - var exception = - assertThrows( - IllegalArgumentException.class, + assertThatThrownBy( () -> { adrService.getProtectDataByAppID(""); - }); - - System.out.println("✓ Empty app ID correctly rejected"); - System.out.println(" Exception: " + exception.getClass().getSimpleName()); - System.out.println(" Message: " + exception.getMessage()); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Application ID cannot be null or empty"); - assertTrue( - exception.getMessage().contains("Application ID cannot be null or empty"), - "Exception message should explain the validation failure"); + log.info("✓ Empty app ID correctly rejected"); } // ========== Test Case 4: Rule Details Verification ========== @Test void testGetADRProtectRules_VerifyRuleDetails() throws IOException { - System.out.println("\n=== Integration Test: Verify rule details structure ==="); + log.info("\n=== Integration Test: Verify rule details structure ==="); - assertNotNull(testData, "Test data must be discovered before running tests"); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); // Act var response = adrService.getProtectDataByAppID(testData.appId); // Assert - assertNotNull(response); - assertNotNull(response.getRules()); - assertFalse(response.getRules().isEmpty()); + assertThat(response).isNotNull(); + assertThat(response.getRules()).isNotNull(); + assertThat(response.getRules()).isNotEmpty(); - System.out.println("✓ Verifying rule details for " + response.getRules().size() + " rules:"); + log.info("✓ Verifying rule details for {} rules:", response.getRules().size()); // Detailed verification of each rule for (var rule : response.getRules()) { - System.out.println("\n Rule: " + rule.getName()); + log.info("\n Rule: {}", rule.getName()); // Verify required fields - assertNotNull(rule.getName(), "Rule name is required"); + assertThat(rule.getName()).as("Rule name is required").isNotNull(); - System.out.println(" ✓ Name: " + rule.getName()); + log.info(" ✓ Name: {}", rule.getName()); if (rule.getProduction() != null) { - System.out.println(" ✓ Production Mode: " + rule.getProduction()); + log.info(" ✓ Production Mode: {}", rule.getProduction()); } // Mode validation - production mode can be null, block, monitor, or off } - System.out.println("\n✓ All rules have valid structure and required fields"); + log.info("\n✓ All rules have valid structure and required fields"); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/ADRServiceTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/ADRServiceTest.java index 7f2b6e4..e613195 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/ADRServiceTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/ADRServiceTest.java @@ -15,7 +15,8 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -57,7 +58,7 @@ class ADRServiceTest { @BeforeEach void setUp() throws Exception { adrService = new ADRService(new PaginationHandler()); - mockContrastSDK = mock(ContrastSDK.class); + mockContrastSDK = mock(); // Mock static SDKHelper mockedSDKHelper = mockStatic(SDKHelper.class); @@ -108,13 +109,13 @@ void testGetAttacks_NoFilters_ReturnsAllAttacks() throws Exception { var result = adrService.getAttacks(null, null, null, null, null, null, null, null); // Then - assertEquals(3, result.items().size()); - assertEquals("attack-uuid-0", result.items().get(0).attackId()); - assertEquals("attack-uuid-1", result.items().get(1).attackId()); - assertEquals("attack-uuid-2", result.items().get(2).attackId()); - assertEquals(1, result.page()); - assertEquals(50, result.pageSize()); - assertFalse(result.hasMorePages()); + assertThat(result.items()).hasSize(3); + assertThat(result.items().get(0).attackId()).isEqualTo("attack-uuid-0"); + assertThat(result.items().get(1).attackId()).isEqualTo("attack-uuid-1"); + assertThat(result.items().get(2).attackId()).isEqualTo("attack-uuid-2"); + assertThat(result.page()).isEqualTo(1); + assertThat(result.pageSize()).isEqualTo(50); + assertThat(result.hasMorePages()).isFalse(); } // ========== Test: QuickFilter ========== @@ -141,7 +142,7 @@ void testGetAttacks_WithQuickFilter_PassesFilterToSDK() throws Exception { var captor = ArgumentCaptor.forClass(AttacksFilterBody.class); verify(extension).getAttacks(eq(TEST_ORG_ID), captor.capture(), eq(50), eq(0), isNull()); - assertEquals("PROBED", captor.getValue().getQuickFilter()); + assertThat(captor.getValue().getQuickFilter()).isEqualTo("PROBED"); } // ========== Test: Keyword Filter ========== @@ -168,7 +169,7 @@ void testGetAttacks_WithKeyword_PassesKeywordToSDK() throws Exception { var captor = ArgumentCaptor.forClass(AttacksFilterBody.class); verify(extension).getAttacks(eq(TEST_ORG_ID), captor.capture(), eq(50), eq(0), isNull()); - assertEquals("sql injection", captor.getValue().getKeyword()); + assertThat(captor.getValue().getKeyword()).isEqualTo("sql injection"); } // ========== Test: Boolean Filters ========== @@ -195,9 +196,9 @@ void testGetAttacks_WithBooleanFilters_PassesCorrectly() throws Exception { var captor = ArgumentCaptor.forClass(AttacksFilterBody.class); verify(extension).getAttacks(eq(TEST_ORG_ID), captor.capture(), eq(50), eq(0), isNull()); - assertEquals(true, captor.getValue().isIncludeSuppressed()); - assertEquals(false, captor.getValue().isIncludeBotBlockers()); - assertEquals(true, captor.getValue().isIncludeIpBlacklist()); + assertThat(captor.getValue().isIncludeSuppressed()).isEqualTo(true); + assertThat(captor.getValue().isIncludeBotBlockers()).isEqualTo(false); + assertThat(captor.getValue().isIncludeIpBlacklist()).isEqualTo(true); } // ========== Test: Pagination Parameters ========== @@ -259,11 +260,11 @@ void testGetAttacks_WithMultipleFilters_AllPassedCorrectly() throws Exception { verify(extension).getAttacks(eq(TEST_ORG_ID), captor.capture(), eq(25), eq(50), eq("severity")); var filter = captor.getValue(); - assertEquals("EXPLOITED", filter.getQuickFilter()); - assertEquals("xss", filter.getKeyword()); - assertTrue(filter.isIncludeSuppressed()); - assertTrue(filter.isIncludeBotBlockers()); - assertFalse(filter.isIncludeIpBlacklist()); + assertThat(filter.getQuickFilter()).isEqualTo("EXPLOITED"); + assertThat(filter.getKeyword()).isEqualTo("xss"); + assertThat(filter.isIncludeSuppressed()).isTrue(); + assertThat(filter.isIncludeBotBlockers()).isTrue(); + assertThat(filter.isIncludeIpBlacklist()).isFalse(); } // ========== Test: Empty Results ========== @@ -286,12 +287,13 @@ void testGetAttacks_EmptyResults_ReturnsEmptyList() throws Exception { var result = adrService.getAttacks(null, null, null, null, null, null, null, null); // Then - assertNotNull(result); - assertTrue(result.items().isEmpty()); - assertFalse(result.hasMorePages()); - assertNotNull(result.message(), "Empty results should have explanatory message"); - assertTrue( - result.message().contains("No items found"), "Message should explain empty results to AI"); + assertThat(result).isNotNull(); + assertThat(result.items()).isEmpty(); + assertThat(result.hasMorePages()).isFalse(); + assertThat(result.message()).as("Empty results should have explanatory message").isNotNull(); + assertThat(result.message()) + .as("Message should explain empty results to AI") + .contains("No items found"); } // ========== Test: Null Results ========== @@ -315,8 +317,8 @@ void testGetAttacks_NullResults_ReturnsEmptyList() throws Exception { var result = adrService.getAttacks(null, null, null, null, null, null, null, null); // Then - assertNotNull(result); - assertTrue(result.items().isEmpty()); + assertThat(result).isNotNull(); + assertThat(result.items()).isEmpty(); } // ========== Test: SDK Exception ========== @@ -334,17 +336,18 @@ void testGetAttacks_SDKThrowsException_PropagatesException() throws Exception { }); // When/Then - var exception = - assertThrows( - Exception.class, + assertThatThrownBy( () -> { adrService.getAttacks(null, null, null, null, null, null, null, null); - }); - - assertTrue( - exception.getMessage().contains("API connection failed") - || exception.getCause() != null - && exception.getCause().getMessage().contains("API connection failed")); + }) + .isInstanceOf(Exception.class) + .satisfies( + ex -> + assertThat( + ex.getMessage().contains("API connection failed") + || (ex.getCause() != null + && ex.getCause().getMessage().contains("API connection failed"))) + .isTrue()); } // ========== Test: Null Filters Don't Override Defaults ========== @@ -373,7 +376,7 @@ void testGetAttacks_NullFilters_DoesNotSetFilterBodyFields() throws Exception { var filter = captor.getValue(); // Verify null parameters didn't set fields (they should remain at constructor defaults) - assertNotNull(filter); // Filter body is created but fields remain unset + assertThat(filter).isNotNull(); // Filter body is created but fields remain unset } // ========== Pagination Tests ========== @@ -396,11 +399,11 @@ void testGetAttacks_WithTotalCount_ProvidesAccurateHasMorePages() throws Excepti var result = adrService.getAttacks(null, null, null, null, null, null, 1, 50); // Then - assertEquals(50, result.items().size()); - assertEquals(1, result.page()); - assertEquals(50, result.pageSize()); - assertEquals(150, result.totalItems()); - assertTrue(result.hasMorePages(), "Should have more pages (page 1 of 3)"); + assertThat(result.items()).hasSize(50); + assertThat(result.page()).isEqualTo(1); + assertThat(result.pageSize()).isEqualTo(50); + assertThat(result.totalItems()).isEqualTo(150); + assertThat(result.hasMorePages()).as("Should have more pages (page 1 of 3)").isTrue(); } @Test @@ -421,11 +424,11 @@ void testGetAttacks_LastPage_WithTotalCount_HasMorePagesFalse() throws Exception var result = adrService.getAttacks(null, null, null, null, null, null, 3, 50); // Then - assertEquals(50, result.items().size()); - assertEquals(3, result.page()); - assertEquals(50, result.pageSize()); - assertEquals(150, result.totalItems()); - assertFalse(result.hasMorePages(), "Last page should have hasMorePages=false"); + assertThat(result.items()).hasSize(50); + assertThat(result.page()).isEqualTo(3); + assertThat(result.pageSize()).isEqualTo(50); + assertThat(result.totalItems()).isEqualTo(150); + assertThat(result.hasMorePages()).as("Last page should have hasMorePages=false").isFalse(); } @Test @@ -447,10 +450,10 @@ void testGetAttacks_InvalidPageSize_ClampsAndWarns() throws Exception { var result = adrService.getAttacks(null, null, null, null, null, null, 1, 500); // Then - assertEquals(100, result.pageSize(), "PageSize should be clamped to 100"); - assertNotNull(result.message(), "Should have warning message"); - assertTrue(result.message().contains("500"), "Message should mention original value"); - assertTrue(result.message().contains("100"), "Message should mention clamped value"); + assertThat(result.pageSize()).as("PageSize should be clamped to 100").isEqualTo(100); + assertThat(result.message()).as("Should have warning message").isNotNull(); + assertThat(result.message()).as("Message should mention original value").contains("500"); + assertThat(result.message()).as("Message should mention clamped value").contains("100"); } @Test @@ -472,9 +475,11 @@ void testGetAttacks_InvalidPage_ClampsAndWarns() throws Exception { var result = adrService.getAttacks(null, null, null, null, null, null, 0, 50); // Then - assertEquals(1, result.page(), "Page should be clamped to 1"); - assertNotNull(result.message(), "Should have warning message"); - assertTrue(result.message().contains("Invalid page"), "Message should indicate invalid page"); + assertThat(result.page()).as("Page should be clamped to 1").isEqualTo(1); + assertThat(result.message()).as("Should have warning message").isNotNull(); + assertThat(result.message()) + .as("Message should indicate invalid page") + .contains("Invalid page"); } @Test @@ -495,8 +500,8 @@ void testGetAttacks_WithoutTotalCount_UsesHeuristic() throws Exception { var result = adrService.getAttacks(null, null, null, null, null, null, 1, 50); // Then - assertNull(result.totalItems(), "TotalItems should be null when not provided"); - assertTrue(result.hasMorePages(), "Heuristic: full page suggests more pages exist"); + assertThat(result.totalItems()).as("TotalItems should be null when not provided").isNull(); + assertThat(result.hasMorePages()).as("Heuristic: full page suggests more pages exist").isTrue(); } @Test @@ -517,9 +522,11 @@ void testGetAttacks_PartialPageWithoutCount_NoMorePages() throws Exception { var result = adrService.getAttacks(null, null, null, null, null, null, 1, 50); // Then - assertEquals(25, result.items().size()); - assertNull(result.totalItems(), "TotalItems should be null when not provided"); - assertFalse(result.hasMorePages(), "Heuristic: partial page suggests no more pages"); + assertThat(result.items().size()).isEqualTo(25); + assertThat(result.totalItems()).as("TotalItems should be null when not provided").isNull(); + assertThat(result.hasMorePages()) + .as("Heuristic: partial page suggests no more pages") + .isFalse(); } // ========== Test: Smart Defaults and Messages ========== @@ -542,13 +549,13 @@ void testGetAttacks_SmartDefaults_ReturnsMessages() throws Exception { var result = adrService.getAttacks(null, null, null, null, null, null, 1, 50); // Then: Should have messages about smart defaults - assertNotNull(result.message(), "Should have messages about smart defaults"); - assertTrue( - result.message().contains("No quickFilter applied"), - "Should have message about quickFilter default"); - assertTrue( - result.message().contains("Excluding suppressed attacks by default"), - "Should have message about includeSuppressed default"); + assertThat(result.message()).as("Should have messages about smart defaults").isNotNull(); + assertThat(result.message()) + .as("Should have message about quickFilter default") + .contains("No quickFilter applied"); + assertThat(result.message()) + .as("Should have message about includeSuppressed default") + .contains("Excluding suppressed attacks by default"); } @Test @@ -570,12 +577,12 @@ void testGetAttacks_ExplicitFilters_NoSmartDefaultMessages() throws Exception { // Then: Should NOT have smart default messages if (result.message() != null) { - assertFalse( - result.message().contains("No quickFilter applied"), - "Should not have quickFilter message when explicitly provided"); - assertFalse( - result.message().contains("Excluding suppressed attacks by default"), - "Should not have includeSuppressed message when explicitly provided"); + assertThat(result.message()) + .as("Should not have quickFilter message when explicitly provided") + .doesNotContain("No quickFilter applied"); + assertThat(result.message()) + .as("Should not have includeSuppressed message when explicitly provided") + .doesNotContain("Excluding suppressed attacks by default"); } } @@ -585,14 +592,14 @@ void testGetAttacks_InvalidQuickFilter_ReturnsError() throws Exception { var result = adrService.getAttacks("INVALID_FILTER", null, null, null, null, null, 1, 50); // Then: Should return error response with descriptive message - assertNotNull(result.message(), "Should have error message"); - assertTrue( - result.message().contains("Invalid quickFilter 'INVALID_FILTER'"), - "Should explain the invalid quickFilter"); - assertTrue( - result.message().contains("Valid: EXPLOITED, PROBED, BLOCKED, INEFFECTIVE, ALL"), - "Should list valid options"); - assertEquals(0, result.items().size(), "Should return empty items on error"); + assertThat(result.message()).as("Should have error message").isNotNull(); + assertThat(result.message()) + .as("Should explain the invalid quickFilter") + .contains("Invalid quickFilter 'INVALID_FILTER'"); + assertThat(result.message()) + .as("Should list valid options") + .contains("Valid: EXPLOITED, PROBED, BLOCKED, INEFFECTIVE, ALL"); + assertThat(result.items().size()).as("Should return empty items on error").isEqualTo(0); } @Test @@ -602,14 +609,14 @@ void testGetAttacks_InvalidSort_ReturnsError() throws Exception { adrService.getAttacks("EXPLOITED", null, false, null, null, "invalid sort!", 1, 50); // Then: Should return error response with descriptive message - assertNotNull(result.message(), "Should have error message"); - assertTrue( - result.message().contains("Invalid sort format 'invalid sort!'"), - "Should explain the invalid sort format"); - assertTrue( - result.message().contains("Must be a field name with optional '-' prefix"), - "Should explain the correct format"); - assertEquals(0, result.items().size(), "Should return empty items on error"); + assertThat(result.message()).as("Should have error message").isNotNull(); + assertThat(result.message()) + .as("Should explain the invalid sort format") + .contains("Invalid sort format 'invalid sort!'"); + assertThat(result.message()) + .as("Should explain the correct format") + .contains("Must be a field name with optional '-' prefix"); + assertThat(result.items().size()).as("Should return empty items on error").isEqualTo(0); } @Test @@ -618,11 +625,12 @@ void testGetAttacks_MultipleValidationErrors_CombinesErrors() throws Exception { var result = adrService.getAttacks("BAD_FILTER", null, null, null, null, "bad-format!", 1, 50); // Then: Should return combined error messages - assertNotNull(result.message(), "Should have error message"); - assertTrue( - result.message().contains("Invalid quickFilter"), "Should include quickFilter error"); - assertTrue(result.message().contains("Invalid sort format"), "Should include sort error"); - assertEquals(0, result.items().size(), "Should return empty items on error"); + assertThat(result.message()).as("Should have error message").isNotNull(); + assertThat(result.message()) + .as("Should include quickFilter error") + .contains("Invalid quickFilter"); + assertThat(result.message()).as("Should include sort error").contains("Invalid sort format"); + assertThat(result.items().size()).as("Should return empty items on error").isEqualTo(0); } // ========== Tests for get_ADR_Protect_Rules_by_app_id ========== @@ -644,9 +652,9 @@ void testGetProtectDataByAppID_Success() throws Exception { var result = adrService.getProtectDataByAppID(TEST_APP_ID); // Then - assertNotNull(result, "Result should not be null"); - assertNotNull(result.getRules(), "Rules should not be null"); - assertEquals(3, result.getRules().size(), "Should have 3 protect rules"); + assertThat(result).as("Result should not be null").isNotNull(); + assertThat(result.getRules()).as("Rules should not be null").isNotNull(); + assertThat(result.getRules().size()).as("Should have 3 protect rules").isEqualTo(3); } @Test @@ -666,44 +674,36 @@ void testGetProtectDataByAppID_WithRules() throws Exception { var result = adrService.getProtectDataByAppID(TEST_APP_ID); // Then - assertNotNull(result); - assertNotNull(result.getRules()); - assertFalse(result.getRules().isEmpty()); + assertThat(result).isNotNull(); + assertThat(result.getRules()).isNotNull(); + assertThat(result.getRules()).isNotEmpty(); // Verify rule details var firstRule = result.getRules().get(0); - assertNotNull(firstRule.getName(), "Rule should have a name"); - assertNotNull(firstRule.getProduction(), "Rule should have a production mode"); + assertThat(firstRule.getName()).as("Rule should have a name").isNotNull(); + assertThat(firstRule.getProduction()).as("Rule should have a production mode").isNotNull(); } @Test void testGetProtectDataByAppID_EmptyAppID() { // When/Then - var exception = - assertThrows( - IllegalArgumentException.class, + assertThatThrownBy( () -> { adrService.getProtectDataByAppID(""); - }); - - assertTrue( - exception.getMessage().contains("Application ID cannot be null or empty"), - "Should have descriptive error message"); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Application ID cannot be null or empty"); } @Test void testGetProtectDataByAppID_NullAppID() { // When/Then - var exception = - assertThrows( - IllegalArgumentException.class, + assertThatThrownBy( () -> { adrService.getProtectDataByAppID(null); - }); - - assertTrue( - exception.getMessage().contains("Application ID cannot be null or empty"), - "Should have descriptive error message"); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Application ID cannot be null or empty"); } @Test @@ -718,18 +718,21 @@ void testGetProtectDataByAppID_SDKFailure() throws Exception { }); // When/Then - var exception = - assertThrows( - Exception.class, + assertThatThrownBy( () -> { adrService.getProtectDataByAppID(TEST_APP_ID); - }); - - assertTrue( - exception.getMessage().contains("Failed to fetch protect config") - || (exception.getCause() != null - && exception.getCause().getMessage().contains("Failed to fetch protect config")), - "Should propagate SDK exception"); + }) + .isInstanceOf(Exception.class) + .satisfies( + ex -> + assertThat( + ex.getMessage().contains("Failed to fetch protect config") + || (ex.getCause() != null + && ex.getCause() + .getMessage() + .contains("Failed to fetch protect config"))) + .as("Should propagate SDK exception") + .isTrue()); } @Test @@ -746,7 +749,7 @@ void testGetProtectDataByAppID_NoProtectDataReturned() throws Exception { var result = adrService.getProtectDataByAppID(TEST_APP_ID); // Then - assertNull(result, "Should return null when no protect data available"); + assertThat(result).as("Should return null when no protect data available").isNull(); } @Test @@ -767,9 +770,9 @@ void testGetProtectDataByAppID_EmptyRulesList() throws Exception { var result = adrService.getProtectDataByAppID(TEST_APP_ID); // Then - assertNotNull(result); - assertNotNull(result.getRules()); - assertTrue(result.getRules().isEmpty(), "Should have empty rules list"); + assertThat(result).isNotNull(); + assertThat(result.getRules()).isNotNull(); + assertThat(result.getRules()).as("Should have empty rules list").isEmpty(); } // ========== Helper Methods ========== diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/ApplicationJsonParsingTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/ApplicationJsonParsingTest.java index 198c09f..a8dc322 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/ApplicationJsonParsingTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/ApplicationJsonParsingTest.java @@ -1,6 +1,6 @@ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application; import com.google.gson.Gson; @@ -61,37 +61,44 @@ public void testApplicationParsingWithMissingRequiredFieldsObjects() { var application = gson.fromJson(applicationJson, Application.class); // Verify basic fields - assertNotNull(application, "Application should not be null"); - assertEquals("test-app-123", application.getAppId(), "App ID should match"); - assertEquals("Test Application", application.getName(), "App name should match"); - assertEquals("online", application.getStatus(), "Status should match"); + assertThat(application).as("Application should not be null").isNotNull(); + assertThat(application.getAppId()).as("App ID should match").isEqualTo("test-app-123"); + assertThat(application.getName()).as("App name should match").isEqualTo("Test Application"); + assertThat(application.getStatus()).as("Status should match").isEqualTo("online"); // Verify missingRequiredFields parsing - assertNotNull( - application.getMissingRequiredFields(), "Missing required fields should not be null"); - assertEquals( - 2, application.getMissingRequiredFields().size(), "Should have 2 missing required fields"); + assertThat(application.getMissingRequiredFields()) + .as("Missing required fields should not be null") + .isNotNull(); + assertThat(application.getMissingRequiredFields()) + .as("Should have 2 missing required fields") + .hasSize(2); // Verify first missing required field var firstField = application.getMissingRequiredFields().get(0); - assertEquals("29", firstField.getFieldId(), "First field ID should match"); - assertEquals("STRING", firstField.getFieldType(), "First field type should match"); - assertEquals( - "Custom Name", firstField.getDisplayLabel(), "First field display label should match"); - assertEquals("customName", firstField.getAgentLabel(), "First field agent label should match"); - assertTrue(firstField.isRequired(), "First field should be required"); - assertFalse(firstField.isUnique(), "First field should not be unique"); + assertThat(firstField.getFieldId()).as("First field ID should match").isEqualTo("29"); + assertThat(firstField.getFieldType()).as("First field type should match").isEqualTo("STRING"); + assertThat(firstField.getDisplayLabel()) + .as("First field display label should match") + .isEqualTo("Custom Name"); + assertThat(firstField.getAgentLabel()) + .as("First field agent label should match") + .isEqualTo("customName"); + assertThat(firstField.isRequired()).as("First field should be required").isTrue(); + assertThat(firstField.isUnique()).as("First field should not be unique").isFalse(); // Verify second missing required field var secondField = application.getMissingRequiredFields().get(1); - assertEquals("30", secondField.getFieldId(), "Second field ID should match"); - assertEquals("SELECT", secondField.getFieldType(), "Second field type should match"); - assertEquals( - "Environment", secondField.getDisplayLabel(), "Second field display label should match"); - assertEquals( - "environment", secondField.getAgentLabel(), "Second field agent label should match"); - assertTrue(secondField.isRequired(), "Second field should be required"); - assertFalse(secondField.isUnique(), "Second field should not be unique"); + assertThat(secondField.getFieldId()).as("Second field ID should match").isEqualTo("30"); + assertThat(secondField.getFieldType()).as("Second field type should match").isEqualTo("SELECT"); + assertThat(secondField.getDisplayLabel()) + .as("Second field display label should match") + .isEqualTo("Environment"); + assertThat(secondField.getAgentLabel()) + .as("Second field agent label should match") + .isEqualTo("environment"); + assertThat(secondField.isRequired()).as("Second field should be required").isTrue(); + assertThat(secondField.isUnique()).as("Second field should not be unique").isFalse(); } @Test @@ -116,13 +123,14 @@ public void testApplicationParsingWithEmptyMissingRequiredFields() { var application = gson.fromJson(applicationJson, Application.class); - assertNotNull(application, "Application should not be null"); - assertEquals("test-app-456", application.getAppId(), "App ID should match"); - assertNotNull( - application.getMissingRequiredFields(), "Missing required fields should not be null"); - assertTrue( - application.getMissingRequiredFields().isEmpty(), - "Missing required fields should be empty"); + assertThat(application).as("Application should not be null").isNotNull(); + assertThat(application.getAppId()).as("App ID should match").isEqualTo("test-app-456"); + assertThat(application.getMissingRequiredFields()) + .as("Missing required fields should not be null") + .isNotNull(); + assertThat(application.getMissingRequiredFields()) + .as("Missing required fields should be empty") + .isEmpty(); } @Test @@ -147,11 +155,11 @@ public void testApplicationParsingWithNullMissingRequiredFields() { // Should handle missing missingRequiredFields field gracefully var application = gson.fromJson(applicationJson, Application.class); - assertNotNull(application, "Application should not be null"); - assertEquals("test-app-789", application.getAppId(), "App ID should match"); + assertThat(application).as("Application should not be null").isNotNull(); + assertThat(application.getAppId()).as("App ID should match").isEqualTo("test-app-789"); // Field should be null when not present in JSON - assertNull( - application.getMissingRequiredFields(), - "Missing required fields should be null when not in JSON"); + assertThat(application.getMissingRequiredFields()) + .as("Missing required fields should be null when not in JSON") + .isNull(); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceIntegrationTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceIntegrationTest.java index fd11dfb..2087514 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceIntegrationTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceIntegrationTest.java @@ -15,10 +15,11 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import com.contrast.labs.ai.mcp.contrast.data.VulnLight; import java.io.IOException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +39,7 @@ * *

Or skip integration tests: mvn verify -DskipITs */ +@Slf4j @SpringBootTest @EnabledIfEnvironmentVariable(named = "CONTRAST_HOST_NAME", matches = ".+") public class AssessServiceIntegrationTest { @@ -46,7 +48,7 @@ public class AssessServiceIntegrationTest { @Test void testEnvironmentsAndTagsArePopulated() throws IOException { - System.out.println("\n=== Integration Test: Environments and Tags ==="); + log.info("\n=== Integration Test: Environments and Tags ==="); // Get vulnerabilities from real TeamServer var response = @@ -63,54 +65,55 @@ void testEnvironmentsAndTagsArePopulated() throws IOException { null // vulnTags ); - assertNotNull(response, "Response should not be null"); - assertTrue(response.items().size() > 0, "Should have at least one vulnerability"); + assertThat(response).as("Response should not be null").isNotNull(); + assertThat(response.items()).as("Should have at least one vulnerability").isNotEmpty(); - System.out.println("Retrieved " + response.items().size() + " vulnerabilities"); + log.info("Retrieved {} vulnerabilities", response.items().size()); // Analyze first few vulnerabilities int withEnvironments = 0; int withTags = 0; for (VulnLight vuln : response.items()) { - assertNotNull(vuln.environments(), "Environments should never be null"); - assertNotNull(vuln.tags(), "Tags should never be null"); + assertThat(vuln.environments()).as("Environments should never be null").isNotNull(); + assertThat(vuln.tags()).as("Tags should never be null").isNotNull(); // Debug: Show all environment and tag data - System.out.println("Vuln " + vuln.vulnID() + ":"); - System.out.println( - " environments: " + vuln.environments() + " (size: " + vuln.environments().size() + ")"); - System.out.println(" tags: " + vuln.tags() + " (size: " + vuln.tags().size() + ")"); + log.info("Vuln {}:", vuln.vulnID()); + log.info(" environments: {} (size: {})", vuln.environments(), vuln.environments().size()); + log.info(" tags: {} (size: {})", vuln.tags(), vuln.tags().size()); if (!vuln.environments().isEmpty()) { withEnvironments++; - System.out.println(" ✓ Has environments: " + vuln.environments()); + log.info(" ✓ Has environments: {}", vuln.environments()); } if (!vuln.tags().isEmpty()) { withTags++; - System.out.println(" ✓ Has tags: " + vuln.tags()); + log.info(" ✓ Has tags: {}", vuln.tags()); } } - System.out.println("\nResults:"); - System.out.println( - " Vulnerabilities with environments: " + withEnvironments + "/" + response.items().size()); - System.out.println(" Vulnerabilities with tags: " + withTags + "/" + response.items().size()); + log.info("\nResults:"); + log.info( + " Vulnerabilities with environments: {}/{}", withEnvironments, response.items().size()); + log.info(" Vulnerabilities with tags: {}/{}", withTags, response.items().size()); // At least verify the fields are being returned (even if empty) // This ensures the API is returning the fields and they're being deserialized for (VulnLight vuln : response.items()) { - assertNotNull(vuln.environments(), "Environments field should exist (even if empty list)"); - assertNotNull(vuln.tags(), "Tags field should exist (even if empty list)"); + assertThat(vuln.environments()) + .as("Environments field should exist (even if empty list)") + .isNotNull(); + assertThat(vuln.tags()).as("Tags field should exist (even if empty list)").isNotNull(); } - System.out.println("✓ Integration test passed: environments and tags fields are present"); + log.info("✓ Integration test passed: environments and tags fields are present"); } @Test void testSessionMetadataIsPopulated() throws IOException { - System.out.println("\n=== Integration Test: Session Metadata ==="); + log.info("\n=== Integration Test: Session Metadata ==="); // Get vulnerabilities from real TeamServer with session metadata expanded var response = @@ -127,21 +130,21 @@ void testSessionMetadataIsPopulated() throws IOException { null // vulnTags ); - assertNotNull(response, "Response should not be null"); - assertTrue(response.items().size() > 0, "Should have at least one vulnerability"); + assertThat(response).as("Response should not be null").isNotNull(); + assertThat(response.items()).as("Should have at least one vulnerability").isNotEmpty(); - System.out.println("Retrieved " + response.items().size() + " vulnerabilities"); + log.info("Retrieved {} vulnerabilities", response.items().size()); // Analyze session metadata in vulnerabilities int withSessionMetadata = 0; int totalSessions = 0; for (VulnLight vuln : response.items()) { - assertNotNull(vuln.sessionMetadata(), "Session metadata should never be null"); + assertThat(vuln.sessionMetadata()).as("Session metadata should never be null").isNotNull(); // Debug: Show session metadata - System.out.println("Vuln " + vuln.vulnID() + ":"); - System.out.println(" sessionMetadata: " + vuln.sessionMetadata().size() + " session(s)"); + log.info("Vuln {}:", vuln.vulnID()); + log.info(" sessionMetadata: {} session(s)", vuln.sessionMetadata().size()); if (!vuln.sessionMetadata().isEmpty()) { withSessionMetadata++; @@ -149,61 +152,62 @@ void testSessionMetadataIsPopulated() throws IOException { // Show details of first session var firstSession = vuln.sessionMetadata().get(0); - System.out.println(" ✓ Has session metadata:"); - System.out.println(" - Session ID: " + firstSession.getSessionId()); + log.info(" ✓ Has session metadata:"); + log.info(" - Session ID: {}", firstSession.getSessionId()); if (firstSession.getMetadata() != null && !firstSession.getMetadata().isEmpty()) { - System.out.println(" - Metadata items: " + firstSession.getMetadata().size()); + log.info(" - Metadata items: {}", firstSession.getMetadata().size()); // Show first metadata item var firstItem = firstSession.getMetadata().get(0); - System.out.println( - " * " + firstItem.getDisplayLabel() + ": " + firstItem.getValue()); + log.info(" * {}: {}", firstItem.getDisplayLabel(), firstItem.getValue()); } } } - System.out.println("\nResults:"); - System.out.println( - " Vulnerabilities with session metadata: " - + withSessionMetadata - + "/" - + response.items().size()); - System.out.println(" Total sessions found: " + totalSessions); + log.info("\nResults:"); + log.info( + " Vulnerabilities with session metadata: {}/{}", + withSessionMetadata, + response.items().size()); + log.info(" Total sessions found: {}", totalSessions); // Verify the session metadata field exists (even if empty) - this confirms SDK expansion works for (VulnLight vuln : response.items()) { - assertNotNull( - vuln.sessionMetadata(), "Session metadata field should exist (even if empty list)"); + assertThat(vuln.sessionMetadata()) + .as("Session metadata field should exist (even if empty list)") + .isNotNull(); } - System.out.println( + log.info( "✓ Integration test passed: session metadata field is present and SDK expansion works"); } @Test void testVulnerabilitiesHaveBasicFields() throws IOException { - System.out.println("\n=== Integration Test: Basic Fields ==="); + log.info("\n=== Integration Test: Basic Fields ==="); var response = assessService.getAllVulnerabilities(1, 5, null, null, null, null, null, null, null, null); - assertNotNull(response); - assertFalse(response.items().isEmpty(), "Should have vulnerabilities"); + assertThat(response).isNotNull(); + assertThat(response.items()).as("Should have vulnerabilities").isNotEmpty(); // Verify each vulnerability has required fields for (VulnLight vuln : response.items()) { - assertNotNull(vuln.title(), "Title should not be null"); - assertNotNull(vuln.type(), "Type should not be null"); - assertNotNull(vuln.vulnID(), "VulnID should not be null"); - assertNotNull(vuln.severity(), "Severity should not be null"); - assertNotNull(vuln.status(), "Status should not be null"); - assertNotNull( - vuln.appID(), "appID should not be null (APPLICATION expand should be included)"); - assertNotNull( - vuln.appName(), "appName should not be null (APPLICATION expand should be included)"); - assertFalse(vuln.appID().isEmpty(), "appID should not be empty"); - assertFalse(vuln.appName().isEmpty(), "appName should not be empty"); - - System.out.println( + assertThat(vuln.title()).as("Title should not be null").isNotNull(); + assertThat(vuln.type()).as("Type should not be null").isNotNull(); + assertThat(vuln.vulnID()).as("VulnID should not be null").isNotNull(); + assertThat(vuln.severity()).as("Severity should not be null").isNotNull(); + assertThat(vuln.status()).as("Status should not be null").isNotNull(); + assertThat(vuln.appID()) + .as("appID should not be null (APPLICATION expand should be included)") + .isNotNull(); + assertThat(vuln.appName()) + .as("appName should not be null (APPLICATION expand should be included)") + .isNotNull(); + assertThat(vuln.appID()).as("appID should not be empty").isNotEmpty(); + assertThat(vuln.appName()).as("appName should not be empty").isNotEmpty(); + + log.info( "✓ " + vuln.vulnID() + ": " @@ -217,13 +221,13 @@ void testVulnerabilitiesHaveBasicFields() throws IOException { + ")"); } - System.out.println("✓ All vulnerabilities have required fields including appID and appName"); + log.info("✓ All vulnerabilities have required fields including appID and appName"); } @Test void testVulnTagsWithSpacesHandledBySDK() throws IOException { - System.out.println("\n=== Integration Test: VulnTags with Spaces ==="); - System.out.println("Testing that SDK properly handles URL encoding of tags with spaces"); + log.info("\n=== Integration Test: VulnTags with Spaces ==="); + log.info("Testing that SDK properly handles URL encoding of tags with spaces"); // Query with a tag that contains spaces - this should work now that AIML-193 is complete // The SDK should handle URL encoding internally @@ -241,154 +245,150 @@ void testVulnTagsWithSpacesHandledBySDK() throws IOException { "SmartFix Remediated" // vulnTags with space - SDK should handle encoding ); - assertNotNull(response, "Response should not be null"); - System.out.println( - "Query completed successfully (returned " + response.items().size() + " vulnerabilities)"); + assertThat(response).as("Response should not be null").isNotNull(); + log.info("Query completed successfully (returned {} vulnerabilities)", response.items().size()); // The query should complete without error - whether we get results depends on the org's data // The important thing is that the SDK properly encoded the tag with spaces if (response.items().size() > 0) { - System.out.println("✓ Found vulnerabilities with 'SmartFix Remediated' tag:"); + log.info("✓ Found vulnerabilities with 'SmartFix Remediated' tag:"); for (VulnLight vuln : response.items()) { - System.out.println(" - " + vuln.vulnID() + ": " + vuln.title()); - System.out.println(" Tags: " + vuln.tags()); + log.info(" - {}: {}", vuln.vulnID(), vuln.title()); + log.info(" Tags: {}", vuln.tags()); } } else { - System.out.println("ℹ No vulnerabilities found with 'SmartFix Remediated' tag (this is OK)"); + log.info("ℹ No vulnerabilities found with 'SmartFix Remediated' tag (this is OK)"); } // Try with multiple tags including spaces and special characters - System.out.println("\nTesting multiple tags with spaces:"); + log.info("\nTesting multiple tags with spaces:"); response = assessService.getAllVulnerabilities( 1, 10, null, null, null, null, null, null, null, "Tag With Spaces,another-tag"); - assertNotNull(response, "Response should not be null"); - System.out.println("✓ Query with multiple tags completed successfully"); - System.out.println(" (returned " + response.items().size() + " vulnerabilities)"); + assertThat(response).as("Response should not be null").isNotNull(); + log.info("✓ Query with multiple tags completed successfully"); + log.info(" (returned {} vulnerabilities)", response.items().size()); - System.out.println("\n✓ Integration test passed: SDK properly handles vulnTags with spaces"); + log.info("\n✓ Integration test passed: SDK properly handles vulnTags with spaces"); } @Test void testListVulnsByAppIdWithSessionMetadata() throws IOException { - System.out.println("\n=== Integration Test: listVulnsByAppId() with Session Metadata ==="); + log.info("\n=== Integration Test: listVulnsByAppId() with Session Metadata ==="); // Step 1: Get some vulnerabilities to find an appId (single API call) - System.out.println("Step 1: Getting vulnerabilities to discover an appId..."); + log.info("Step 1: Getting vulnerabilities to discover an appId..."); var allVulns = assessService.getAllVulnerabilities(1, 10, null, null, null, null, null, null, null, null); - assertNotNull(allVulns, "Response should not be null"); - assertFalse(allVulns.items().isEmpty(), "Should have at least one vulnerability"); + assertThat(allVulns).as("Response should not be null").isNotNull(); + assertThat(allVulns.items()).as("Should have at least one vulnerability").isNotEmpty(); - System.out.println(" ✓ Found " + allVulns.items().size() + " vulnerability(ies)"); + log.info(" ✓ Found {} vulnerability(ies)", allVulns.items().size()); // Step 2: Get applications list (single API call) - System.out.println("Step 2: Getting first application with vulnerabilities..."); + log.info("Step 2: Getting first application with vulnerabilities..."); var applications = assessService.getAllApplications(); - assertNotNull(applications, "Applications list should not be null"); - assertFalse(applications.isEmpty(), "Should have at least one application"); + assertThat(applications).as("Applications list should not be null").isNotNull(); + assertThat(applications).as("Should have at least one application").isNotEmpty(); // Just use the first application - no iteration needed var testAppId = applications.get(0).appID(); var testAppName = applications.get(0).name(); - System.out.println(" ✓ Using application: " + testAppName + " (ID: " + testAppId + ")"); + log.info(" ✓ Using application: {} (ID: {})", testAppName, testAppId); // Step 3: Call listVulnsByAppId() with the discovered appId - System.out.println("Step 3: Calling listVulnsByAppId() for app: " + testAppName); + log.info("Step 3: Calling listVulnsByAppId() for app: {}", testAppName); var vulnerabilities = assessService.listVulnsByAppId(testAppId); - assertNotNull(vulnerabilities, "Vulnerabilities list should not be null"); - System.out.println(" ✓ Retrieved " + vulnerabilities.size() + " vulnerability(ies)"); + assertThat(vulnerabilities).as("Vulnerabilities list should not be null").isNotNull(); + log.info(" ✓ Retrieved {} vulnerability(ies)", vulnerabilities.size()); if (vulnerabilities.isEmpty()) { - System.out.println(" ℹ No vulnerabilities for this app (this is OK for the test)"); + log.info(" ℹ No vulnerabilities for this app (this is OK for the test)"); return; } // Step 4: Verify session metadata is populated - System.out.println("Step 4: Verifying session metadata is populated..."); + log.info("Step 4: Verifying session metadata is populated..."); int withSessionMetadata = 0; for (VulnLight vuln : vulnerabilities) { - assertNotNull(vuln.sessionMetadata(), "Session metadata should never be null"); + assertThat(vuln.sessionMetadata()).as("Session metadata should never be null").isNotNull(); if (!vuln.sessionMetadata().isEmpty()) { withSessionMetadata++; - System.out.println( + log.info( " ✓ Vuln " + vuln.vulnID() + " has " + vuln.sessionMetadata().size() + " session(s)"); } } - System.out.println("\nResults:"); - System.out.println( + log.info("\nResults:"); + log.info( " Vulnerabilities with session metadata: " + withSessionMetadata + "/" + vulnerabilities.size()); - System.out.println( + log.info( "✓ Integration test passed: listVulnsByAppId() returns vulnerabilities with session" + " metadata"); } @Test void testListVulnsInAppByNameForLatestSessionWithDynamicSessionId() throws IOException { - System.out.println( + log.info( "\n" + "=== Integration Test: listVulnsByAppIdForLatestSession() with Dynamic Session" + " Discovery ==="); // Step 1: Get applications list (single API call) - System.out.println("Step 1: Getting first application..."); + log.info("Step 1: Getting first application..."); var applications = assessService.getAllApplications(); - assertNotNull(applications, "Applications list should not be null"); - assertFalse(applications.isEmpty(), "Should have at least one application"); + assertThat(applications).as("Applications list should not be null").isNotNull(); + assertThat(applications).as("Should have at least one application").isNotEmpty(); // Just use the first application - no iteration needed var testAppID = applications.get(0).appID(); var testAppName = applications.get(0).name(); - System.out.println(" ✓ Using application: " + testAppName + " (ID: " + testAppID + ")"); + log.info(" ✓ Using application: {} (ID: {})", testAppName, testAppID); // Step 2: Call listVulnsByAppIdForLatestSession() with the discovered app ID - System.out.println( - "Step 2: Calling listVulnsByAppIdForLatestSession() for appID: " + testAppID); + log.info("Step 2: Calling listVulnsByAppIdForLatestSession() for appID: {}", testAppID); var latestSessionVulns = assessService.listVulnsByAppIdForLatestSession(testAppID); - assertNotNull(latestSessionVulns, "Vulnerabilities list should not be null"); - System.out.println( - " ✓ Retrieved " + latestSessionVulns.size() + " vulnerability(ies) for latest session"); + assertThat(latestSessionVulns).as("Vulnerabilities list should not be null").isNotNull(); + log.info(" ✓ Retrieved {} vulnerability(ies) for latest session", latestSessionVulns.size()); if (latestSessionVulns.isEmpty()) { - System.out.println( + log.info( " ℹ No vulnerabilities in latest session (this is valid if latest session has no" + " vulns)"); return; } // Step 3: Verify session metadata is populated in results - System.out.println("Step 3: Verifying session metadata is populated..."); + log.info("Step 3: Verifying session metadata is populated..."); int withSessionMetadata = 0; for (VulnLight vuln : latestSessionVulns) { - assertNotNull(vuln.sessionMetadata(), "Session metadata should never be null"); + assertThat(vuln.sessionMetadata()).as("Session metadata should never be null").isNotNull(); if (!vuln.sessionMetadata().isEmpty()) { withSessionMetadata++; String sessionId = vuln.sessionMetadata().get(0).getSessionId(); - System.out.println(" ✓ Vuln " + vuln.vulnID() + " has session ID: " + sessionId); + log.info(" ✓ Vuln {} has session ID: {}", vuln.vulnID(), sessionId); } } - System.out.println("\nResults:"); - System.out.println(" Vulnerabilities returned: " + latestSessionVulns.size()); - System.out.println( - " Vulnerabilities with session metadata: " - + withSessionMetadata - + "/" - + latestSessionVulns.size()); - System.out.println( + log.info("\nResults:"); + log.info(" Vulnerabilities returned: {}", latestSessionVulns.size()); + log.info( + " Vulnerabilities with session metadata: {}/{}", + withSessionMetadata, + latestSessionVulns.size()); + log.info( "✓ Integration test passed: listVulnsByAppIdForLatestSession() returns vulnerabilities with" + " session metadata"); } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceTest.java index 9c5110f..1988a8f 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceTest.java @@ -15,7 +15,9 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -146,8 +148,8 @@ void testGetAllVulnerabilities_PassesCorrectParametersToSDK() throws Exception { verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertEquals(75, form.getOffset()); // (page 2 - 1) * 75 - assertEquals(75, form.getLimit()); + assertThat(form.getOffset()).isEqualTo(75); // (page 2 - 1) * 75 + assertThat(form.getLimit()).isEqualTo(75); } @Test @@ -165,13 +167,13 @@ void testGetAllVulnerabilities_SetsExpandParametersCorrectly() throws Exception verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getExpand()); - assertTrue( - form.getExpand().contains(TraceFilterForm.TraceExpandValue.SESSION_METADATA), - "Expand should include SESSION_METADATA"); - assertTrue( - form.getExpand().contains(TraceFilterForm.TraceExpandValue.SERVER_ENVIRONMENTS), - "Expand should include SERVER_ENVIRONMENTS"); + assertThat(form.getExpand()).isNotNull(); + assertThat(form.getExpand()) + .as("Expand should include SESSION_METADATA") + .contains(TraceFilterForm.TraceExpandValue.SESSION_METADATA); + assertThat(form.getExpand()) + .as("Expand should include SERVER_ENVIRONMENTS") + .contains(TraceFilterForm.TraceExpandValue.SERVER_ENVIRONMENTS); } @Test @@ -265,10 +267,12 @@ void testGetAllVulnerabilities_EmptyResults_PassesEmptyListToPaginationHandler() ); // Verify result contains helpful message for AI - assertNotNull(result); - assertTrue(result.items().isEmpty()); - assertNotNull(result.message(), "Empty results should have explanatory message"); - assertEquals("No items found.", result.message(), "Message should explain empty results to AI"); + assertThat(result).isNotNull(); + assertThat(result.items()).isEmpty(); + assertThat(result.message()).as("Empty results should have explanatory message").isNotNull(); + assertThat(result.message()) + .as("Message should explain empty results to AI") + .isEqualTo("No items found."); } // ========== Helper Methods ========== @@ -284,7 +288,7 @@ private Traces createMockTraces(int traceCount, Integer totalCount) { var traces = new ArrayList(); for (int i = 0; i < traceCount; i++) { - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test Vulnerability " + i); when(trace.getRule()).thenReturn("test-rule-" + i); when(trace.getUuid()).thenReturn("uuid-" + i); @@ -326,15 +330,15 @@ void testListVulnerabilityTypes_Success() throws Exception { var result = assessService.listVulnerabilityTypes(); // Assert - assertNotNull(result); - assertEquals(5, result.size()); + assertThat(result).isNotNull(); + assertThat(result.size()).isEqualTo(5); // Verify sorted alphabetically - assertEquals("cmd-injection", result.get(0)); - assertEquals("crypto-bad-mac", result.get(1)); - assertEquals("path-traversal", result.get(2)); - assertEquals("sql-injection", result.get(3)); - assertEquals("xss-reflected", result.get(4)); + assertThat(result.get(0)).isEqualTo("cmd-injection"); + assertThat(result.get(1)).isEqualTo("crypto-bad-mac"); + assertThat(result.get(2)).isEqualTo("path-traversal"); + assertThat(result.get(3)).isEqualTo("sql-injection"); + assertThat(result.get(4)).isEqualTo("xss-reflected"); verify(mockContrastSDK).getRules(TEST_ORG_ID); } @@ -349,8 +353,8 @@ void testListVulnerabilityTypes_EmptyRules() throws Exception { var result = assessService.listVulnerabilityTypes(); // Assert - assertNotNull(result); - assertTrue(result.isEmpty(), "Should return empty list when no rules available"); + assertThat(result).isNotNull(); + assertThat(result).as("Should return empty list when no rules available").isEmpty(); verify(mockContrastSDK).getRules(TEST_ORG_ID); } @@ -363,8 +367,8 @@ void testListVulnerabilityTypes_NullRulesObject() throws Exception { var result = assessService.listVulnerabilityTypes(); // Assert - assertNotNull(result); - assertTrue(result.isEmpty(), "Should return empty list when Rules object is null"); + assertThat(result).isNotNull(); + assertThat(result).as("Should return empty list when Rules object is null").isEmpty(); verify(mockContrastSDK).getRules(TEST_ORG_ID); } @@ -386,19 +390,19 @@ void testListVulnerabilityTypes_FiltersNullAndEmptyNames() throws Exception { var result = assessService.listVulnerabilityTypes(); // Assert - assertNotNull(result); + assertThat(result).isNotNull(); // Should only have the 4 valid names (whitespace-only gets trimmed to empty and filtered) - assertEquals(4, result.size()); - assertTrue(result.contains("sql-injection")); - assertTrue(result.contains("xss-reflected")); - assertTrue(result.contains("path-traversal")); - assertTrue(result.contains("cmd-injection")); + assertThat(result.size()).isEqualTo(4); + assertThat(result).contains("sql-injection"); + assertThat(result).contains("xss-reflected"); + assertThat(result).contains("path-traversal"); + assertThat(result).contains("cmd-injection"); // Verify sorted - assertEquals("cmd-injection", result.get(0)); - assertEquals("path-traversal", result.get(1)); - assertEquals("sql-injection", result.get(2)); - assertEquals("xss-reflected", result.get(3)); + assertThat(result.get(0)).isEqualTo("cmd-injection"); + assertThat(result.get(1)).isEqualTo("path-traversal"); + assertThat(result.get(2)).isEqualTo("sql-injection"); + assertThat(result.get(3)).isEqualTo("xss-reflected"); } @Test @@ -408,14 +412,12 @@ void testListVulnerabilityTypes_SDKThrowsException() throws Exception { .thenThrow(new RuntimeException("API connection failed")); // Act & Assert - var exception = - assertThrows( - Exception.class, + assertThatThrownBy( () -> { assessService.listVulnerabilityTypes(); - }); - - assertTrue(exception.getMessage().contains("Failed to retrieve vulnerability types")); + }) + .isInstanceOf(Exception.class) + .hasMessageContaining("Failed to retrieve vulnerability types"); verify(mockContrastSDK).getRules(TEST_ORG_ID); } @@ -433,13 +435,14 @@ void testListVulnerabilityTypes_LargeRuleSet() throws Exception { var result = assessService.listVulnerabilityTypes(); // Assert - assertNotNull(result); - assertEquals(100, result.size()); + assertThat(result).isNotNull(); + assertThat(result.size()).isEqualTo(100); // Verify still sorted for (int i = 0; i < result.size() - 1; i++) { - assertTrue( - result.get(i).compareTo(result.get(i + 1)) < 0, "Rules should be sorted alphabetically"); + assertThat(result.get(i).compareTo(result.get(i + 1))) + .as("Rules should be sorted alphabetically") + .isLessThan(0); } } @@ -518,13 +521,13 @@ void testGetAllVulnerabilities_SeverityFilter() throws Exception { 1, 50, "CRITICAL,HIGH", null, null, null, null, null, null, null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getSeverities()); - assertEquals(2, form.getSeverities().size()); + assertThat(form.getSeverities()).isNotNull(); + assertThat(form.getSeverities().size()).isEqualTo(2); } @Test @@ -535,15 +538,15 @@ void testGetAllVulnerabilities_InvalidSeverity_HardFailure() throws Exception { 1, 50, "CRITICAL,SUPER_HIGH", null, null, null, null, null, null, null); // Assert - Hard failure returns error response with empty items - assertNotNull(response); - assertTrue(response.items().isEmpty(), "Hard failure should return empty items"); - assertEquals(1, response.page()); - assertEquals(50, response.pageSize()); - assertEquals(0, response.totalItems()); + assertThat(response).isNotNull(); + assertThat(response.items()).as("Hard failure should return empty items").isEmpty(); + assertThat(response.page()).isEqualTo(1); + assertThat(response.pageSize()).isEqualTo(50); + assertThat(response.totalItems()).isEqualTo(0); - assertNotNull(response.message()); - assertTrue(response.message().contains("Invalid severity 'SUPER_HIGH'")); - assertTrue(response.message().contains("Valid: CRITICAL, HIGH, MEDIUM, LOW, NOTE")); + assertThat(response.message()).isNotNull(); + assertThat(response.message()).contains("Invalid severity 'SUPER_HIGH'"); + assertThat(response.message()).contains("Valid: CRITICAL, HIGH, MEDIUM, LOW, NOTE"); // Verify SDK was NOT called (hard failure stops execution) verify(mockContrastSDK, never()).getTracesInOrg(any(), any()); @@ -561,16 +564,16 @@ void testGetAllVulnerabilities_StatusSmartDefaults() throws Exception { assessService.getAllVulnerabilities(1, 50, null, null, null, null, null, null, null, null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getStatus()); - assertEquals(3, form.getStatus().size()); - assertTrue(form.getStatus().contains("Reported")); - assertTrue(form.getStatus().contains("Suspicious")); - assertTrue(form.getStatus().contains("Confirmed")); + assertThat(form.getStatus()).isNotNull(); + assertThat(form.getStatus().size()).isEqualTo(3); + assertThat(form.getStatus()).contains("Reported"); + assertThat(form.getStatus()).contains("Suspicious"); + assertThat(form.getStatus()).contains("Confirmed"); // Message content is tested in VulnerabilityFilterParamsTest } @@ -588,16 +591,16 @@ void testGetAllVulnerabilities_StatusExplicitOverride() throws Exception { 1, 50, null, "Reported,Fixed,Remediated", null, null, null, null, null, null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getStatus()); - assertEquals(3, form.getStatus().size()); - assertTrue(form.getStatus().contains("Reported")); - assertTrue(form.getStatus().contains("Fixed")); - assertTrue(form.getStatus().contains("Remediated")); + assertThat(form.getStatus()).isNotNull(); + assertThat(form.getStatus().size()).isEqualTo(3); + assertThat(form.getStatus()).contains("Reported"); + assertThat(form.getStatus()).contains("Fixed"); + assertThat(form.getStatus()).contains("Remediated"); } @Test @@ -613,15 +616,15 @@ void testGetAllVulnerabilities_VulnTypesFilter() throws Exception { 1, 50, null, null, null, "sql-injection,xss-reflected", null, null, null, null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getVulnTypes()); - assertEquals(2, form.getVulnTypes().size()); - assertTrue(form.getVulnTypes().contains("sql-injection")); - assertTrue(form.getVulnTypes().contains("xss-reflected")); + assertThat(form.getVulnTypes()).isNotNull(); + assertThat(form.getVulnTypes().size()).isEqualTo(2); + assertThat(form.getVulnTypes()).contains("sql-injection"); + assertThat(form.getVulnTypes()).contains("xss-reflected"); } @Test @@ -637,13 +640,13 @@ void testGetAllVulnerabilities_EnvironmentFilter() throws Exception { 1, 50, null, null, null, null, "PRODUCTION,QA", null, null, null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getEnvironments()); - assertEquals(2, form.getEnvironments().size()); + assertThat(form.getEnvironments()).isNotNull(); + assertThat(form.getEnvironments().size()).isEqualTo(2); } @Test @@ -659,13 +662,13 @@ void testGetAllVulnerabilities_DateFilterValid() throws Exception { 1, 50, null, null, null, null, null, "2025-01-01", "2025-12-31", null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getStartDate()); - assertNotNull(form.getEndDate()); + assertThat(form.getStartDate()).isNotNull(); + assertThat(form.getEndDate()).isNotNull(); // Message content is tested in VulnerabilityFilterParamsTest } @@ -683,16 +686,16 @@ void testGetAllVulnerabilities_VulnTagsFilter() throws Exception { 1, 50, null, null, null, null, null, null, null, "SmartFix Remediated,reviewed"); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getFilterTags()); - assertEquals(2, form.getFilterTags().size()); + assertThat(form.getFilterTags()).isNotNull(); + assertThat(form.getFilterTags().size()).isEqualTo(2); // SDK now handles URL encoding (AIML-193 complete) - tags passed through as-is - assertTrue(form.getFilterTags().contains("SmartFix Remediated")); - assertTrue(form.getFilterTags().contains("reviewed")); + assertThat(form.getFilterTags()).contains("SmartFix Remediated"); + assertThat(form.getFilterTags()).contains("reviewed"); } @Test @@ -717,16 +720,16 @@ void testGetAllVulnerabilities_MultipleFilters() throws Exception { null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getSeverities()); - assertNotNull(form.getStatus()); - assertNotNull(form.getVulnTypes()); - assertNotNull(form.getEnvironments()); - assertNotNull(form.getStartDate()); + assertThat(form.getSeverities()).isNotNull(); + assertThat(form.getStatus()).isNotNull(); + assertThat(form.getVulnTypes()).isNotNull(); + assertThat(form.getEnvironments()).isNotNull(); + assertThat(form.getStartDate()).isNotNull(); } @Test @@ -743,7 +746,7 @@ void testGetAllVulnerabilities_AppIdRouting() throws Exception { 1, 50, null, null, testAppId, null, null, null, null, null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); // Verify it used app-specific API, not org-level verify(mockContrastSDK).getTraces(eq(TEST_ORG_ID), eq(testAppId), any(TraceFilterForm.class)); verify(mockContrastSDK, never()).getTracesInOrg(eq(TEST_ORG_ID), any(TraceFilterForm.class)); @@ -762,13 +765,13 @@ void testGetAllVulnerabilities_WhitespaceInFilters() throws Exception { 1, 50, "CRITICAL , HIGH , ", null, null, null, null, null, null, null); // Assert - assertNotNull(response); + assertThat(response).isNotNull(); var captor = ArgumentCaptor.forClass(TraceFilterForm.class); verify(mockContrastSDK).getTracesInOrg(eq(TEST_ORG_ID), captor.capture()); var form = captor.getValue(); - assertNotNull(form.getSeverities()); - assertEquals(2, form.getSeverities().size()); + assertThat(form.getSeverities()).isNotNull(); + assertThat(form.getSeverities().size()).isEqualTo(2); } @Test @@ -778,7 +781,7 @@ void testGetAllVulnerabilities_EnvironmentsInResponse() throws Exception { var traces = new ArrayList(); // Trace 1: Multiple servers with different environments - var trace1 = mock(Trace.class); + Trace trace1 = mock(); when(trace1.getTitle()).thenReturn("SQL Injection"); when(trace1.getRule()).thenReturn("sql-injection"); when(trace1.getUuid()).thenReturn("uuid-1"); @@ -796,7 +799,7 @@ void testGetAllVulnerabilities_EnvironmentsInResponse() throws Exception { traces.add(trace1); // Trace 2: No servers - var trace2 = mock(Trace.class); + Trace trace2 = mock(); when(trace2.getTitle()).thenReturn("XSS"); when(trace2.getRule()).thenReturn("xss-reflected"); when(trace2.getUuid()).thenReturn("uuid-2"); @@ -810,7 +813,7 @@ void testGetAllVulnerabilities_EnvironmentsInResponse() throws Exception { traces.add(trace2); // Trace 3: Single server with one environment - var trace3 = mock(Trace.class); + Trace trace3 = mock(); when(trace3.getTitle()).thenReturn("Path Traversal"); when(trace3.getRule()).thenReturn("path-traversal"); when(trace3.getUuid()).thenReturn("uuid-3"); @@ -844,32 +847,32 @@ void testGetAllVulnerabilities_EnvironmentsInResponse() throws Exception { assessService.getAllVulnerabilities(1, 50, null, null, null, null, null, null, null, null); // Assert - assertNotNull(response); - assertEquals(3, response.items().size()); + assertThat(response).isNotNull(); + assertThat(response.items()).hasSize(3); // Verify trace 1: Multiple environments, deduplicated and sorted var vuln1 = response.items().get(0); - assertEquals("SQL Injection", vuln1.title()); - assertNotNull(vuln1.environments()); - assertEquals(2, vuln1.environments().size()); - assertTrue(vuln1.environments().contains("PRODUCTION")); - assertTrue(vuln1.environments().contains("QA")); + assertThat(vuln1.title()).isEqualTo("SQL Injection"); + assertThat(vuln1.environments()).isNotNull(); + assertThat(vuln1.environments()).hasSize(2); + assertThat(vuln1.environments()).contains("PRODUCTION"); + assertThat(vuln1.environments()).contains("QA"); // Verify sorted order - assertEquals("PRODUCTION", vuln1.environments().get(0)); - assertEquals("QA", vuln1.environments().get(1)); + assertThat(vuln1.environments().get(0)).isEqualTo("PRODUCTION"); + assertThat(vuln1.environments().get(1)).isEqualTo("QA"); // Verify trace 2: No servers = empty environments var vuln2 = response.items().get(1); - assertEquals("XSS", vuln2.title()); - assertNotNull(vuln2.environments()); - assertEquals(0, vuln2.environments().size()); + assertThat(vuln2.title()).isEqualTo("XSS"); + assertThat(vuln2.environments()).isNotNull(); + assertThat(vuln2.environments()).hasSize(0); // Verify trace 3: Single environment var vuln3 = response.items().get(2); - assertEquals("Path Traversal", vuln3.title()); - assertNotNull(vuln3.environments()); - assertEquals(1, vuln3.environments().size()); - assertEquals("DEVELOPMENT", vuln3.environments().get(0)); + assertThat(vuln3.title()).isEqualTo("Path Traversal"); + assertThat(vuln3.environments()).isNotNull(); + assertThat(vuln3.environments()).hasSize(1); + assertThat(vuln3.environments().get(0)).isEqualTo("DEVELOPMENT"); } @Test @@ -879,7 +882,7 @@ void testVulnLight_TimestampFields_ISO8601Format() throws Exception { var firstSeen = JAN_1_2024_00_00_UTC; var closed = FEB_19_2025_13_20_UTC; - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test Vulnerability"); when(trace.getRule()).thenReturn("test-rule"); when(trace.getUuid()).thenReturn("test-uuid-123"); @@ -891,7 +894,7 @@ void testVulnLight_TimestampFields_ISO8601Format() throws Exception { when(trace.getServerEnvironments()).thenReturn(new ArrayList<>()); when(trace.getTags()).thenReturn(new ArrayList<>()); - var mockTraces = mock(Traces.class); + Traces mockTraces = mock(); when(mockTraces.getTraces()).thenReturn(List.of(trace)); when(mockTraces.getCount()).thenReturn(1); @@ -903,44 +906,44 @@ void testVulnLight_TimestampFields_ISO8601Format() throws Exception { assessService.getAllVulnerabilities(1, 50, null, null, null, null, null, null, null, null); // Assert - assertNotNull(response); - assertEquals(1, response.items().size()); + assertThat(response).isNotNull(); + assertThat(response.items().size()).isEqualTo(1); var vuln = response.items().get(0); // Verify field names use *At convention - assertNotNull(vuln.lastSeenAt(), "lastSeenAt field should exist"); - assertNotNull(vuln.firstSeenAt(), "firstSeenAt field should exist"); - assertNotNull(vuln.closedAt(), "closedAt field should exist"); + assertThat(vuln.lastSeenAt()).as("lastSeenAt field should exist").isNotNull(); + assertThat(vuln.firstSeenAt()).as("firstSeenAt field should exist").isNotNull(); + assertThat(vuln.closedAt()).as("closedAt field should exist").isNotNull(); // Verify ISO 8601 format with timezone offset (YYYY-MM-DDTHH:MM:SS+/-HH:MM) var iso8601Pattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"; - assertTrue( - vuln.lastSeenAt().matches(iso8601Pattern), - "lastSeenAt should be ISO 8601 with timezone: " + vuln.lastSeenAt()); - assertTrue( - vuln.firstSeenAt().matches(iso8601Pattern), - "firstSeenAt should be ISO 8601 with timezone: " + vuln.firstSeenAt()); - assertTrue( - vuln.closedAt().matches(iso8601Pattern), - "closedAt should be ISO 8601 with timezone: " + vuln.closedAt()); + assertThat(vuln.lastSeenAt()) + .as("lastSeenAt should be ISO 8601 with timezone: " + vuln.lastSeenAt()) + .matches(iso8601Pattern); + assertThat(vuln.firstSeenAt()) + .as("firstSeenAt should be ISO 8601 with timezone: " + vuln.firstSeenAt()) + .matches(iso8601Pattern); + assertThat(vuln.closedAt()) + .as("closedAt should be ISO 8601 with timezone: " + vuln.closedAt()) + .matches(iso8601Pattern); // Verify timestamps include timezone offset - assertTrue( - vuln.lastSeenAt().contains("+") || vuln.lastSeenAt().contains("-"), - "lastSeenAt should include timezone offset"); - assertTrue( - vuln.firstSeenAt().contains("+") || vuln.firstSeenAt().contains("-"), - "firstSeenAt should include timezone offset"); - assertTrue( - vuln.closedAt().contains("+") || vuln.closedAt().contains("-"), - "closedAt should include timezone offset"); + assertThat(vuln.lastSeenAt().contains("+") || vuln.lastSeenAt().contains("-")) + .as("lastSeenAt should include timezone offset") + .isTrue(); + assertThat(vuln.firstSeenAt().contains("+") || vuln.firstSeenAt().contains("-")) + .as("firstSeenAt should include timezone offset") + .isTrue(); + assertThat(vuln.closedAt().contains("+") || vuln.closedAt().contains("-")) + .as("closedAt should include timezone offset") + .isTrue(); } @Test void testVulnLight_TimestampFields_NullHandling() throws Exception { // Arrange - Create trace with null timestamps - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test Vulnerability"); when(trace.getRule()).thenReturn("test-rule"); when(trace.getUuid()).thenReturn("test-uuid-123"); @@ -952,7 +955,7 @@ void testVulnLight_TimestampFields_NullHandling() throws Exception { when(trace.getServerEnvironments()).thenReturn(new ArrayList<>()); when(trace.getTags()).thenReturn(new ArrayList<>()); - var mockTraces = mock(Traces.class); + Traces mockTraces = mock(); when(mockTraces.getTraces()).thenReturn(List.of(trace)); when(mockTraces.getCount()).thenReturn(1); @@ -964,14 +967,14 @@ void testVulnLight_TimestampFields_NullHandling() throws Exception { assessService.getAllVulnerabilities(1, 50, null, null, null, null, null, null, null, null); // Assert - assertNotNull(response); - assertEquals(1, response.items().size()); + assertThat(response).isNotNull(); + assertThat(response.items().size()).isEqualTo(1); var vuln = response.items().get(0); // Verify null timestamps are handled correctly - assertNotNull(vuln.lastSeenAt(), "lastSeenAt should always be present"); - assertNull(vuln.firstSeenAt(), "firstSeenAt should be null when not set"); - assertNull(vuln.closedAt(), "closedAt should be null when not set"); + assertThat(vuln.lastSeenAt()).as("lastSeenAt should always be present").isNotNull(); + assertThat(vuln.firstSeenAt()).as("firstSeenAt should be null when not set").isNull(); + assertThat(vuln.closedAt()).as("closedAt should be null when not set").isNull(); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/AttackFilterParamsTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/AttackFilterParamsTest.java index 69bc274..5df6cea 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/AttackFilterParamsTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/AttackFilterParamsTest.java @@ -15,7 +15,8 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; @@ -25,71 +26,74 @@ class AttackFilterParamsTest { void testValidFiltersAllProvided() { var params = AttackFilterParams.of("EXPLOITED", "xss", true, true, false, "severity"); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); var filterBody = params.toAttacksFilterBody(); - assertEquals("EXPLOITED", filterBody.getQuickFilter()); - assertEquals("xss", filterBody.getKeyword()); - assertTrue(filterBody.isIncludeSuppressed()); - assertTrue(filterBody.isIncludeBotBlockers()); - assertFalse(filterBody.isIncludeIpBlacklist()); + assertThat(filterBody.getQuickFilter()).isEqualTo("EXPLOITED"); + assertThat(filterBody.getKeyword()).isEqualTo("xss"); + assertThat(filterBody.isIncludeSuppressed()).isTrue(); + assertThat(filterBody.isIncludeBotBlockers()).isTrue(); + assertThat(filterBody.isIncludeIpBlacklist()).isFalse(); } @Test void testNoFiltersProvided() { var params = AttackFilterParams.of(null, null, null, null, null, null); - assertTrue(params.isValid()); - assertFalse(params.messages().isEmpty()); // Should have smart defaults messages - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.messages()).isNotEmpty(); // Should have smart defaults messages + assertThat(params.errors()).isEmpty(); // Smart defaults should be applied var filterBody = params.toAttacksFilterBody(); - assertEquals("ALL", filterBody.getQuickFilter()); - assertFalse(filterBody.isIncludeSuppressed()); // Smart default: exclude suppressed + assertThat(filterBody.getQuickFilter()).isEqualTo("ALL"); + assertThat(filterBody.isIncludeSuppressed()).isFalse(); // Smart default: exclude suppressed } @Test void testSmartDefaultForIncludeSuppressed() { var params = AttackFilterParams.of(null, null, null, null, null, null); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); // Should have two messages: quickFilter default and includeSuppressed default - assertEquals(2, params.messages().size()); - assertTrue(params.messages().stream().anyMatch(m -> m.contains("No quickFilter applied"))); - assertTrue( - params.messages().stream() - .anyMatch(m -> m.contains("Excluding suppressed attacks by default"))); + assertThat(params.messages()).hasSize(2); + assertThat(params.messages().stream().anyMatch(m -> m.contains("No quickFilter applied"))) + .isTrue(); + assertThat( + params.messages().stream() + .anyMatch(m -> m.contains("Excluding suppressed attacks by default"))) + .isTrue(); var filterBody = params.toAttacksFilterBody(); - assertFalse(filterBody.isIncludeSuppressed()); + assertThat(filterBody.isIncludeSuppressed()).isFalse(); } @Test void testExplicitIncludeSuppressedNoMessage() { var params = AttackFilterParams.of("EXPLOITED", null, true, null, null, null); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); // Should not have includeSuppressed message when explicitly provided - assertFalse(params.messages().stream().anyMatch(m -> m.contains("Excluding suppressed"))); + assertThat(params.messages().stream().anyMatch(m -> m.contains("Excluding suppressed"))) + .isFalse(); var filterBody = params.toAttacksFilterBody(); - assertTrue(filterBody.isIncludeSuppressed()); + assertThat(filterBody.isIncludeSuppressed()).isTrue(); } @Test void testInvalidQuickFilterHardFailure() { var params = AttackFilterParams.of("INVALID", null, null, null, null, null); - assertFalse(params.isValid()); - assertEquals(1, params.errors().size()); - assertTrue(params.errors().get(0).contains("Invalid quickFilter 'INVALID'")); - assertTrue( - params.errors().get(0).contains("Valid: EXPLOITED, PROBED, BLOCKED, INEFFECTIVE, ALL")); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(1); + assertThat(params.errors().get(0)).contains("Invalid quickFilter 'INVALID'"); + assertThat(params.errors().get(0)) + .contains("Valid: EXPLOITED, PROBED, BLOCKED, INEFFECTIVE, ALL"); } @Test @@ -98,8 +102,8 @@ void testValidQuickFilterValues() { for (String filter : validFilters) { var params = AttackFilterParams.of(filter, null, false, null, null, null); - assertTrue(params.isValid(), "Filter " + filter + " should be valid"); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).as("Filter " + filter + " should be valid").isTrue(); + assertThat(params.errors()).isEmpty(); } } @@ -107,74 +111,74 @@ void testValidQuickFilterValues() { void testQuickFilterCaseInsensitive() { // Test lowercase and mixed case var params1 = AttackFilterParams.of("exploited", null, false, null, null, null); - assertTrue(params1.isValid()); - assertEquals("EXPLOITED", params1.toAttacksFilterBody().getQuickFilter()); + assertThat(params1.isValid()).isTrue(); + assertThat(params1.toAttacksFilterBody().getQuickFilter()).isEqualTo("EXPLOITED"); var params2 = AttackFilterParams.of("PrObEd", null, false, null, null, null); - assertTrue(params2.isValid()); - assertEquals("PROBED", params2.toAttacksFilterBody().getQuickFilter()); + assertThat(params2.isValid()).isTrue(); + assertThat(params2.toAttacksFilterBody().getQuickFilter()).isEqualTo("PROBED"); } @Test void testQuickFilterWithWhitespace() { var params = AttackFilterParams.of(" EXPLOITED ", null, false, null, null, null); - assertTrue(params.isValid()); - assertEquals("EXPLOITED", params.toAttacksFilterBody().getQuickFilter()); + assertThat(params.isValid()).isTrue(); + assertThat(params.toAttacksFilterBody().getQuickFilter()).isEqualTo("EXPLOITED"); } @Test void testKeywordPassThrough() { var params = AttackFilterParams.of("EXPLOITED", "sql injection test", false, null, null, null); - assertTrue(params.isValid()); - assertEquals("sql injection test", params.toAttacksFilterBody().getKeyword()); + assertThat(params.isValid()).isTrue(); + assertThat(params.toAttacksFilterBody().getKeyword()).isEqualTo("sql injection test"); } @Test void testValidSortFormat() { var params = AttackFilterParams.of("EXPLOITED", null, false, null, null, "severity"); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); } @Test void testValidDescendingSortFormat() { var params = AttackFilterParams.of("EXPLOITED", null, false, null, null, "-severity"); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); } @Test void testInvalidSortFormatHardFailure() { var params = AttackFilterParams.of("EXPLOITED", null, false, null, null, "invalid sort!"); - assertFalse(params.isValid()); - assertEquals(1, params.errors().size()); - assertTrue(params.errors().get(0).contains("Invalid sort format 'invalid sort!'")); - assertTrue(params.errors().get(0).contains("Must be a field name with optional '-' prefix")); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(1); + assertThat(params.errors().get(0)).contains("Invalid sort format 'invalid sort!'"); + assertThat(params.errors().get(0)).contains("Must be a field name with optional '-' prefix"); } @Test void testValidSortWithUnderscores() { var params = AttackFilterParams.of("EXPLOITED", null, false, null, null, "field_name"); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); } @Test void testAllBooleanFlagsExplicitlySet() { var params = AttackFilterParams.of("BLOCKED", "keyword", true, true, true, null); - assertTrue(params.isValid()); + assertThat(params.isValid()).isTrue(); var filterBody = params.toAttacksFilterBody(); - assertTrue(filterBody.isIncludeSuppressed()); - assertTrue(filterBody.isIncludeBotBlockers()); - assertTrue(filterBody.isIncludeIpBlacklist()); + assertThat(filterBody.isIncludeSuppressed()).isTrue(); + assertThat(filterBody.isIncludeBotBlockers()).isTrue(); + assertThat(filterBody.isIncludeIpBlacklist()).isTrue(); } @Test @@ -182,65 +186,68 @@ void testMultipleErrorsAccumulate() { var params = AttackFilterParams.of("INVALID_FILTER", null, null, null, null, "bad-sort-format!"); - assertFalse(params.isValid()); - assertEquals(2, params.errors().size()); // quickFilter and sort errors + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(2); // quickFilter and sort errors } @Test void testMessagesAreImmutable() { var params = AttackFilterParams.of(null, null, null, null, null, null); - assertThrows( - UnsupportedOperationException.class, - () -> { - params.messages().add("Should fail"); - }); + assertThatThrownBy( + () -> { + params.messages().add("Should fail"); + }) + .isInstanceOf(UnsupportedOperationException.class); } @Test void testErrorsAreImmutable() { var params = AttackFilterParams.of("INVALID", null, null, null, null, null); - assertThrows( - UnsupportedOperationException.class, - () -> { - params.errors().add("Should fail"); - }); + assertThatThrownBy( + () -> { + params.errors().add("Should fail"); + }) + .isInstanceOf(UnsupportedOperationException.class); } @Test void testQuickFilterDefaultMessage() { var params = AttackFilterParams.of(null, null, false, null, null, null); - assertTrue(params.isValid()); - assertTrue( - params.messages().stream() - .anyMatch(m -> m.contains("No quickFilter applied - showing all attack types"))); + assertThat(params.isValid()).isTrue(); + assertThat( + params.messages().stream() + .anyMatch(m -> m.contains("No quickFilter applied - showing all attack types"))) + .isTrue(); } @Test void testNoQuickFilterMessageWhenProvided() { var params = AttackFilterParams.of("EXPLOITED", null, false, null, null, null); - assertTrue(params.isValid()); - assertFalse(params.messages().stream().anyMatch(m -> m.contains("No quickFilter applied"))); + assertThat(params.isValid()).isTrue(); + assertThat(params.messages().stream().anyMatch(m -> m.contains("No quickFilter applied"))) + .isFalse(); } @Test void testEmptyStringQuickFilterTreatedAsNull() { var params = AttackFilterParams.of(" ", null, false, null, null, null); - assertTrue(params.isValid()); + assertThat(params.isValid()).isTrue(); // Empty/whitespace should be treated as null and use default - assertEquals("ALL", params.toAttacksFilterBody().getQuickFilter()); - assertTrue(params.messages().stream().anyMatch(m -> m.contains("No quickFilter applied"))); + assertThat(params.toAttacksFilterBody().getQuickFilter()).isEqualTo("ALL"); + assertThat(params.messages().stream().anyMatch(m -> m.contains("No quickFilter applied"))) + .isTrue(); } @Test void testEmptyStringKeywordHandled() { var params = AttackFilterParams.of("EXPLOITED", " ", false, null, null, null); - assertTrue(params.isValid()); + assertThat(params.isValid()).isTrue(); // Empty keyword shouldn't cause issues } @@ -248,7 +255,7 @@ void testEmptyStringKeywordHandled() { void testEmptyStringSortTreatedAsNull() { var params = AttackFilterParams.of("EXPLOITED", null, false, null, null, " "); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/FilterHelperTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/FilterHelperTest.java index 60ba7af..7bc818c 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/FilterHelperTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/FilterHelperTest.java @@ -15,7 +15,7 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import java.time.LocalDateTime; import java.time.ZoneId; @@ -55,17 +55,16 @@ void testFormatTimestamp_ValidTimestamp() { var result = FilterHelper.formatTimestamp(epochMillis); // Then: Should return ISO 8601 format with timezone offset - assertNotNull(result); - assertTrue( - result.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "Timestamp should match ISO 8601 format with timezone offset: " + result); + assertThat(result).isNotNull(); + assertThat(result) + .as("Timestamp should match ISO 8601 format with timezone offset: " + result) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); // Verify it can be parsed back var parsed = ZonedDateTime.parse(result, DateTimeFormatter.ISO_OFFSET_DATE_TIME); - assertEquals( - epochMillis, - parsed.toInstant().toEpochMilli(), - "Parsed timestamp should match original epoch milliseconds"); + assertThat(parsed.toInstant().toEpochMilli()) + .as("Parsed timestamp should match original epoch milliseconds") + .isEqualTo(epochMillis); } @Test @@ -77,7 +76,7 @@ void testFormatTimestamp_NullInput() { var result = FilterHelper.formatTimestamp(nullValue); // Then: Should return null - assertNull(result, "Formatting null timestamp should return null"); + assertThat(result).as("Formatting null timestamp should return null").isNull(); } @Test @@ -89,13 +88,15 @@ void testFormatTimestamp_EpochZero() { var result = FilterHelper.formatTimestamp(epochMillis); // Then: Should return valid ISO 8601 timestamp - assertNotNull(result); - assertTrue( - result.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "Timestamp should match ISO 8601 format: " + result); - assertTrue( - result.startsWith("1970-01-01") || result.startsWith("1969-12-31"), - "Epoch zero should be Jan 1, 1970 UTC (or Dec 31, 1969 in negative timezone): " + result); + assertThat(result).isNotNull(); + assertThat(result) + .as("Timestamp should match ISO 8601 format: " + result) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(result.startsWith("1970-01-01") || result.startsWith("1969-12-31")) + .as( + "Epoch zero should be Jan 1, 1970 UTC (or Dec 31, 1969 in negative timezone): " + + result) + .isTrue(); } @Test @@ -107,13 +108,13 @@ void testFormatTimestamp_FutureDate() { var result = FilterHelper.formatTimestamp(epochMillis); // Then: Should return valid ISO 8601 timestamp - assertNotNull(result); - assertTrue( - result.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "Timestamp should match ISO 8601 format: " + result); - assertTrue( - result.startsWith("2029-12-31") || result.startsWith("2030-01-01"), - "Should represent Jan 1, 2030 in local timezone: " + result); + assertThat(result).isNotNull(); + assertThat(result) + .as("Timestamp should match ISO 8601 format: " + result) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(result.startsWith("2029-12-31") || result.startsWith("2030-01-01")) + .as("Should represent Jan 1, 2030 in local timezone: " + result) + .isTrue(); } @Test @@ -133,7 +134,9 @@ void testFormatTimestamp_UsesSystemDefaultTimezone() { var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx"); var expectedFormat = expected.format(formatter); - assertEquals(expectedFormat, result, "Should format timestamp using system default timezone"); + assertThat(result) + .as("Should format timestamp using system default timezone") + .isEqualTo(expectedFormat); } @Test @@ -145,15 +148,15 @@ void testFormatTimestamp_PreservesTimezone() { var result = FilterHelper.formatTimestamp(epochMillis); // Then: Should include timezone offset in the output - assertTrue( - result.contains("+") || result.contains("-"), - "Timestamp should include timezone offset (+ or -): " + result); + assertThat(result.contains("+") || result.contains("-")) + .as("Timestamp should include timezone offset (+ or -): " + result) + .isTrue(); // Extract and verify timezone offset format var timezoneOffset = result.substring(result.length() - 6); - assertTrue( - timezoneOffset.matches("[+-]\\d{2}:\\d{2}"), - "Timezone offset should be in format +/-HH:MM: " + timezoneOffset); + assertThat(timezoneOffset) + .as("Timezone offset should be in format +/-HH:MM: " + timezoneOffset) + .matches("[+-]\\d{2}:\\d{2}"); } @Test @@ -166,10 +169,10 @@ void testFormatTimestamp_ConsistentFormat() { var result = FilterHelper.formatTimestamp(timestamp); // Then: All should match ISO 8601 format with timezone - assertNotNull(result); - assertTrue( - result.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "All timestamps should match ISO 8601 format: " + result); + assertThat(result).isNotNull(); + assertThat(result) + .as("All timestamps should match ISO 8601 format: " + result) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); } } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/HintGeneratorTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/HintGeneratorTest.java index b609106..cb9ee19 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/HintGeneratorTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/HintGeneratorTest.java @@ -1,6 +1,6 @@ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import com.contrast.labs.ai.mcp.contrast.hints.HintGenerator; import org.junit.jupiter.api.DisplayName; @@ -16,35 +16,35 @@ public class HintGeneratorTest { @DisplayName("Test with a valid SQL injection rule") public void testGenerateVulnerabilityFixHintForSqlInjection() { var sqlInjectionHints = HintGenerator.generateVulnerabilityFixHint("sql-injection"); - assertTrue( - sqlInjectionHints.contains("allow list"), - "SQL injection hints should contain 'allow list' guidance"); + assertThat(sqlInjectionHints) + .as("SQL injection hints should contain 'allow list' guidance") + .contains("allow list"); } @Test @DisplayName("Test with null rule") public void testGenerateVulnerabilityFixHintForNullRule() { var nullRuleHints = HintGenerator.generateVulnerabilityFixHint(null); - assertTrue( - nullRuleHints.contains("Where a vulnerable library exists"), - "Null rule should return the default hint"); + assertThat(nullRuleHints) + .as("Null rule should return the default hint") + .contains("Where a vulnerable library exists"); } @Test @DisplayName("Test with empty rule") public void testGenerateVulnerabilityFixHintForEmptyRule() { var emptyRuleHints = HintGenerator.generateVulnerabilityFixHint(""); - assertTrue( - emptyRuleHints.contains("Where a vulnerable library exists"), - "Empty rule should return the default hint"); + assertThat(emptyRuleHints) + .as("Empty rule should return the default hint") + .contains("Where a vulnerable library exists"); } @Test @DisplayName("Test with non-existent rule") public void testGenerateVulnerabilityFixHintForNonExistentRule() { var nonExistentRuleHints = HintGenerator.generateVulnerabilityFixHint("non-existent-rule"); - assertTrue( - nonExistentRuleHints.contains("Where a vulnerable library exists"), - "Non-existent rule should return the default hint"); + assertThat(nonExistentRuleHints) + .as("Non-existent rule should return the default hint") + .contains("Where a vulnerable library exists"); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/PaginationParamsTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/PaginationParamsTest.java index 04729fa..e4649df 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/PaginationParamsTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/PaginationParamsTest.java @@ -15,7 +15,8 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; @@ -25,138 +26,138 @@ class PaginationParamsTest { void testValidPaginationDefaults() { var params = PaginationParams.of(null, null); - assertEquals(1, params.page()); - assertEquals(50, params.pageSize()); - assertEquals(0, params.offset()); - assertEquals(50, params.limit()); - assertTrue(params.warnings().isEmpty()); - assertTrue(params.isValid()); + assertThat(params.page()).isEqualTo(1); + assertThat(params.pageSize()).isEqualTo(50); + assertThat(params.offset()).isEqualTo(0); + assertThat(params.limit()).isEqualTo(50); + assertThat(params.warnings()).isEmpty(); + assertThat(params.isValid()).isTrue(); } @Test void testValidPaginationCustom() { var params = PaginationParams.of(3, 25); - assertEquals(3, params.page()); - assertEquals(25, params.pageSize()); - assertEquals(50, params.offset()); // (3-1) * 25 - assertEquals(25, params.limit()); - assertTrue(params.warnings().isEmpty()); - assertTrue(params.isValid()); + assertThat(params.page()).isEqualTo(3); + assertThat(params.pageSize()).isEqualTo(25); + assertThat(params.offset()).isEqualTo(50); // (3-1) * 25 + assertThat(params.limit()).isEqualTo(25); + assertThat(params.warnings()).isEmpty(); + assertThat(params.isValid()).isTrue(); } @Test void testInvalidPageNegative() { var params = PaginationParams.of(-5, 50); - assertEquals(1, params.page()); // Clamped to 1 - assertEquals(50, params.pageSize()); - assertEquals(0, params.offset()); - assertTrue(params.isValid()); // Always valid with soft failures - assertEquals(1, params.warnings().size()); - assertTrue(params.warnings().get(0).contains("Invalid page number -5")); + assertThat(params.page()).isEqualTo(1); // Clamped to 1 + assertThat(params.pageSize()).isEqualTo(50); + assertThat(params.offset()).isEqualTo(0); + assertThat(params.isValid()).isTrue(); // Always valid with soft failures + assertThat(params.warnings().size()).isEqualTo(1); + assertThat(params.warnings().get(0)).contains("Invalid page number -5"); } @Test void testInvalidPageZero() { var params = PaginationParams.of(0, 50); - assertEquals(1, params.page()); // Clamped to 1 - assertEquals(50, params.pageSize()); - assertEquals(0, params.offset()); - assertTrue(params.isValid()); - assertEquals(1, params.warnings().size()); - assertTrue(params.warnings().get(0).contains("Invalid page number 0")); + assertThat(params.page()).isEqualTo(1); // Clamped to 1 + assertThat(params.pageSize()).isEqualTo(50); + assertThat(params.offset()).isEqualTo(0); + assertThat(params.isValid()).isTrue(); + assertThat(params.warnings().size()).isEqualTo(1); + assertThat(params.warnings().get(0)).contains("Invalid page number 0"); } @Test void testInvalidPageSizeNegative() { var params = PaginationParams.of(1, -10); - assertEquals(1, params.page()); - assertEquals(50, params.pageSize()); // Clamped to default 50 - assertEquals(0, params.offset()); - assertTrue(params.isValid()); - assertEquals(1, params.warnings().size()); - assertTrue(params.warnings().get(0).contains("Invalid pageSize -10")); + assertThat(params.page()).isEqualTo(1); + assertThat(params.pageSize()).isEqualTo(50); // Clamped to default 50 + assertThat(params.offset()).isEqualTo(0); + assertThat(params.isValid()).isTrue(); + assertThat(params.warnings().size()).isEqualTo(1); + assertThat(params.warnings().get(0)).contains("Invalid pageSize -10"); } @Test void testInvalidPageSizeZero() { var params = PaginationParams.of(1, 0); - assertEquals(1, params.page()); - assertEquals(50, params.pageSize()); // Clamped to default 50 - assertEquals(0, params.offset()); - assertTrue(params.isValid()); - assertEquals(1, params.warnings().size()); - assertTrue(params.warnings().get(0).contains("Invalid pageSize 0")); + assertThat(params.page()).isEqualTo(1); + assertThat(params.pageSize()).isEqualTo(50); // Clamped to default 50 + assertThat(params.offset()).isEqualTo(0); + assertThat(params.isValid()).isTrue(); + assertThat(params.warnings().size()).isEqualTo(1); + assertThat(params.warnings().get(0)).contains("Invalid pageSize 0"); } @Test void testPageSizeExceedsMaximum() { var params = PaginationParams.of(1, 200); - assertEquals(1, params.page()); - assertEquals(100, params.pageSize()); // Capped to 100 - assertEquals(0, params.offset()); - assertTrue(params.isValid()); - assertEquals(1, params.warnings().size()); - assertTrue(params.warnings().get(0).contains("Requested pageSize 200 exceeds maximum 100")); + assertThat(params.page()).isEqualTo(1); + assertThat(params.pageSize()).isEqualTo(100); // Capped to 100 + assertThat(params.offset()).isEqualTo(0); + assertThat(params.isValid()).isTrue(); + assertThat(params.warnings().size()).isEqualTo(1); + assertThat(params.warnings().get(0)).contains("Requested pageSize 200 exceeds maximum 100"); } @Test void testMultipleValidationWarnings() { var params = PaginationParams.of(-5, 200); - assertEquals(1, params.page()); // Clamped to 1 - assertEquals(100, params.pageSize()); // Capped to 100 - assertEquals(0, params.offset()); - assertTrue(params.isValid()); - assertEquals(2, params.warnings().size()); - assertTrue(params.warnings().get(0).contains("Invalid page number -5")); - assertTrue(params.warnings().get(1).contains("Requested pageSize 200 exceeds maximum 100")); + assertThat(params.page()).isEqualTo(1); // Clamped to 1 + assertThat(params.pageSize()).isEqualTo(100); // Capped to 100 + assertThat(params.offset()).isEqualTo(0); + assertThat(params.isValid()).isTrue(); + assertThat(params.warnings().size()).isEqualTo(2); + assertThat(params.warnings().get(0)).contains("Invalid page number -5"); + assertThat(params.warnings().get(1)).contains("Requested pageSize 200 exceeds maximum 100"); } @Test void testOffsetCalculation() { // Page 1 var p1 = PaginationParams.of(1, 50); - assertEquals(0, p1.offset()); + assertThat(p1.offset()).isEqualTo(0); // Page 2 var p2 = PaginationParams.of(2, 50); - assertEquals(50, p2.offset()); + assertThat(p2.offset()).isEqualTo(50); // Page 5 with custom page size var p3 = PaginationParams.of(5, 25); - assertEquals(100, p3.offset()); // (5-1) * 25 + assertThat(p3.offset()).isEqualTo(100); // (5-1) * 25 } @Test void testLimitMatchesPageSize() { var params = PaginationParams.of(1, 75); - assertEquals(75, params.pageSize()); - assertEquals(75, params.limit()); + assertThat(params.pageSize()).isEqualTo(75); + assertThat(params.limit()).isEqualTo(75); } @Test void testPageSizeBoundaryValues() { // Min valid var pMin = PaginationParams.of(1, 1); - assertEquals(1, pMin.pageSize()); - assertTrue(pMin.warnings().isEmpty()); + assertThat(pMin.pageSize()).isEqualTo(1); + assertThat(pMin.warnings()).isEmpty(); // Max valid var pMax = PaginationParams.of(1, 100); - assertEquals(100, pMax.pageSize()); - assertTrue(pMax.warnings().isEmpty()); + assertThat(pMax.pageSize()).isEqualTo(100); + assertThat(pMax.warnings()).isEmpty(); // Just over max var pOver = PaginationParams.of(1, 101); - assertEquals(100, pOver.pageSize()); - assertEquals(1, pOver.warnings().size()); + assertThat(pOver.pageSize()).isEqualTo(100); + assertThat(pOver.warnings().size()).isEqualTo(1); } @Test @@ -164,10 +165,10 @@ void testWarningsAreImmutable() { var params = PaginationParams.of(-1, 200); // Should throw UnsupportedOperationException - assertThrows( - UnsupportedOperationException.class, - () -> { - params.warnings().add("This should fail"); - }); + assertThatThrownBy( + () -> { + params.warnings().add("This should fail"); + }) + .isInstanceOf(UnsupportedOperationException.class); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/RouteCoverageServiceIntegrationTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/RouteCoverageServiceIntegrationTest.java index fd076d4..0c0891b 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/RouteCoverageServiceIntegrationTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/RouteCoverageServiceIntegrationTest.java @@ -15,13 +15,15 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import com.contrast.labs.ai.mcp.contrast.sdkextension.SDKExtension; import com.contrast.labs.ai.mcp.contrast.sdkextension.SDKHelper; import com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application; import com.contrast.labs.ai.mcp.contrast.sdkextension.data.routecoverage.Route; import java.io.IOException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -46,6 +48,7 @@ * *

Or skip integration tests: mvn verify -DskipITs */ +@Slf4j @SpringBootTest @EnabledIfEnvironmentVariable(named = "CONTRAST_HOST_NAME", matches = ".+") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -104,12 +107,10 @@ public String toString() { @BeforeAll void discoverTestData() { - System.out.println( + log.info( "\n╔════════════════════════════════════════════════════════════════════════════════╗"); - System.out.println( - "║ Route Coverage Integration Test - Discovering Test Data ║"); - System.out.println( - "╚════════════════════════════════════════════════════════════════════════════════╝"); + log.info("║ Route Coverage Integration Test - Discovering Test Data ║"); + log.info("╚════════════════════════════════════════════════════════════════════════════════╝"); try { var sdk = @@ -117,26 +118,26 @@ void discoverTestData() { var sdkExtension = new SDKExtension(sdk); // Get all applications - System.out.println("\n🔍 Step 1: Fetching all applications..."); + log.info("\n🔍 Step 1: Fetching all applications..."); var appsResponse = sdkExtension.getApplications(orgID); var applications = appsResponse.getApplications(); - System.out.println(" Found " + applications.size() + " application(s) in organization"); + log.info(" Found {} application(s) in organization", applications.size()); if (applications.isEmpty()) { - System.out.println("\n⚠️ NO APPLICATIONS FOUND"); - System.out.println(" The integration tests require at least one application with:"); - System.out.println(" 1. Route coverage data (routes discovered or exercised)"); - System.out.println(" 2. Session metadata (optional but recommended)"); - System.out.println("\n To create test data:"); - System.out.println(" - Deploy an application with Contrast agent"); - System.out.println(" - Exercise some routes (make HTTP requests)"); - System.out.println(" - Optionally: Configure session metadata in agent"); + log.info("\n⚠️ NO APPLICATIONS FOUND"); + log.info(" The integration tests require at least one application with:"); + log.info(" 1. Route coverage data (routes discovered or exercised)"); + log.info(" 2. Session metadata (optional but recommended)"); + log.info("\n To create test data:"); + log.info(" - Deploy an application with Contrast agent"); + log.info(" - Exercise some routes (make HTTP requests)"); + log.info(" - Optionally: Configure session metadata in agent"); return; } // Search for suitable test application - prioritize apps with BOTH routes AND session // metadata - System.out.println( + log.info( "\n🔍 Step 2: Searching for application with route coverage AND session metadata..."); TestData bestCandidate = null; TestData fallbackCandidate = null; // App with routes but no session metadata @@ -145,22 +146,17 @@ void discoverTestData() { for (Application app : applications) { if (appsChecked >= maxAppsToCheck) { - System.out.println( - " Reached max apps to check (" + maxAppsToCheck + "), stopping search"); + log.info(" Reached max apps to check ({}), stopping search", maxAppsToCheck); break; } appsChecked++; - System.out.println( - " Checking app " - + appsChecked - + "/" - + maxAppsToCheck - + ": " - + app.getName() - + " (ID: " - + app.getAppId() - + ")"); + log.info( + " Checking app {}/{}: {} (ID: {})", + appsChecked, + maxAppsToCheck, + app.getName(), + app.getAppId()); try { // Check for route coverage @@ -168,7 +164,7 @@ void discoverTestData() { if (routeResponse != null && routeResponse.getRoutes() != null && !routeResponse.getRoutes().isEmpty()) { - System.out.println(" ✓ Has " + routeResponse.getRoutes().size() + " route(s)"); + log.info(" ✓ Has {} route(s)", routeResponse.getRoutes().size()); var candidate = new TestData(); candidate.appId = app.getAppId(); @@ -192,15 +188,14 @@ void discoverTestData() { candidate.sessionMetadataName = firstMetadata.getMetadataField().getAgentLabel(); candidate.sessionMetadataValue = firstMetadata.getValue(); - System.out.println(" ✓ Has session metadata"); - System.out.println( - " ✓ Session metadata field: " - + candidate.sessionMetadataName - + "=" - + candidate.sessionMetadataValue); + log.info(" ✓ Has session metadata"); + log.info( + " ✓ Session metadata field: {}={}", + candidate.sessionMetadataName, + candidate.sessionMetadataValue); // Found perfect candidate with both routes and session metadata! - System.out.println( + log.info( "\n ✅ Found PERFECT test application with routes AND session metadata!"); bestCandidate = candidate; break; // Stop searching - we found what we need @@ -208,19 +203,18 @@ void discoverTestData() { } } } catch (Exception e) { - System.out.println(" ℹ No session metadata: " + e.getMessage()); + log.info(" ℹ No session metadata: {}", e.getMessage()); } // Save as fallback if we haven't found a perfect candidate yet if (!candidate.hasSessionMetadata && fallbackCandidate == null) { - System.out.println( - " ℹ Saving as fallback candidate (has routes but no session metadata)"); + log.info(" ℹ Saving as fallback candidate (has routes but no session metadata)"); fallbackCandidate = candidate; } } } catch (Exception e) { // Skip this app, continue searching - System.out.println(" ℹ No route coverage or error: " + e.getMessage()); + log.info(" ℹ No route coverage or error: {}", e.getMessage()); } } @@ -229,34 +223,33 @@ void discoverTestData() { if (candidate != null) { testData = candidate; - System.out.println( + log.info( "\n╔════════════════════════════════════════════════════════════════════════════════╗"); - System.out.println( + log.info( "║ Test Data Discovery Complete ║"); - System.out.println( + log.info( "╚════════════════════════════════════════════════════════════════════════════════╝"); - System.out.println(testData); - System.out.println(); + log.info("{}", testData); + log.info(""); // Validate that we have session metadata for complete testing if (!candidate.hasSessionMetadata) { - System.err.println( - "\n⚠️ WARNING: Application has route coverage but NO SESSION METADATA"); - System.err.println(" Some tests will fail. To fix this:"); - System.err.println(" 1. Configure session metadata in your Contrast agent"); - System.err.println(" 2. Restart the application"); - System.err.println(" 3. Make some HTTP requests to exercise routes"); - System.err.println(" 4. Re-run the integration tests"); + log.error("\n⚠️ WARNING: Application has route coverage but NO SESSION METADATA"); + log.error(" Some tests will fail. To fix this:"); + log.error(" 1. Configure session metadata in your Contrast agent"); + log.error(" 2. Restart the application"); + log.error(" 3. Make some HTTP requests to exercise routes"); + log.error(" 4. Re-run the integration tests"); } } else { String errorMsg = buildTestDataErrorMessage(appsChecked); - System.err.println(errorMsg); + log.error(errorMsg); fail(errorMsg); } } catch (Exception e) { String errorMsg = "❌ ERROR during test data discovery: " + e.getMessage(); - System.err.println("\n" + errorMsg); + log.error("\n{}", errorMsg); e.printStackTrace(); fail(errorMsg); } @@ -319,38 +312,42 @@ private String buildTestDataErrorMessage(int appsChecked) { @Test void testDiscoveredTestDataExists() { - System.out.println("\n=== Integration Test: Validate test data discovery ==="); - - assertNotNull(testData, "Test data should have been discovered in @BeforeAll"); - assertNotNull(testData.appId, "Test application ID should be set"); - assertTrue(testData.hasRouteCoverage, "Test application should have route coverage"); - assertTrue(testData.routeCount > 0, "Test application should have at least 1 route"); - - System.out.println("✓ Test data validated:"); - System.out.println(" App ID: " + testData.appId); - System.out.println(" App Name: " + testData.appName); - System.out.println(" Route Count: " + testData.routeCount); - System.out.println(" Has Session Metadata: " + testData.hasSessionMetadata); + log.info("\n=== Integration Test: Validate test data discovery ==="); + + assertThat(testData).as("Test data should have been discovered in @BeforeAll").isNotNull(); + assertThat(testData.appId).as("Test application ID should be set").isNotNull(); + assertThat(testData.hasRouteCoverage) + .as("Test application should have route coverage") + .isTrue(); + assertThat(testData.routeCount > 0) + .as("Test application should have at least 1 route") + .isTrue(); + + log.info("✓ Test data validated:"); + log.info(" App ID: {}", testData.appId); + log.info(" App Name: {}", testData.appName); + log.info(" Route Count: {}", testData.routeCount); + log.info(" Has Session Metadata: {}", testData.hasSessionMetadata); } // ========== Test Case 2: Unfiltered Query ========== @Test void testGetRouteCoverage_Unfiltered_Success() throws IOException { - System.out.println("\n=== Integration Test: get_route_coverage (unfiltered) ==="); + log.info("\n=== Integration Test: get_route_coverage (unfiltered) ==="); - assertNotNull(testData, "Test data must be discovered before running tests"); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); // Act var response = routeCoverageService.getRouteCoverage(testData.appId, null, null, null); // Assert - assertNotNull(response, "Response should not be null"); - assertTrue(response.isSuccess(), "Response should indicate success"); - assertNotNull(response.getRoutes(), "Routes should not be null"); - assertTrue(response.getRoutes().size() > 0, "Should have at least 1 route"); + assertThat(response).as("Response should not be null").isNotNull(); + assertThat(response.isSuccess()).as("Response should indicate success").isTrue(); + assertThat(response.getRoutes()).as("Routes should not be null").isNotNull(); + assertThat(response.getRoutes().size() > 0).as("Should have at least 1 route").isTrue(); - System.out.println( + log.info( "✓ Retrieved " + response.getRoutes().size() + " routes for application: " @@ -361,14 +358,16 @@ void testGetRouteCoverage_Unfiltered_Success() throws IOException { response.getRoutes().stream().filter(route -> route.getExercised() > 0).count(); long discoveredCount = response.getRoutes().size() - exercisedCount; - System.out.println(" Exercised routes: " + exercisedCount); - System.out.println(" Discovered routes: " + discoveredCount); + log.info(" Exercised routes: {}", exercisedCount); + log.info(" Discovered routes: {}", discoveredCount); // Verify all routes have details for (Route route : response.getRoutes()) { - assertNotNull(route.getSignature(), "Route signature should not be null"); - assertNotNull(route.getRouteHash(), "Route hash should not be null"); - assertNotNull(route.getRouteDetailsResponse(), "Route details should be populated"); + assertThat(route.getSignature()).as("Route signature should not be null").isNotNull(); + assertThat(route.getRouteHash()).as("Route hash should not be null").isNotNull(); + assertThat(route.getRouteDetailsResponse()) + .as("Route details should be populated") + .isNotNull(); } } @@ -376,15 +375,16 @@ void testGetRouteCoverage_Unfiltered_Success() throws IOException { @Test void testGetRouteCoverage_SessionMetadataFilter_Success() throws IOException { - System.out.println("\n=== Integration Test: get_route_coverage (session metadata filter) ==="); + log.info("\n=== Integration Test: get_route_coverage (session metadata filter) ==="); - assertNotNull(testData, "Test data must be discovered before running tests"); - assertTrue( - testData.hasSessionMetadata, - "Test application must have session metadata. Found app with route coverage but no session" - + " metadata. Please configure session metadata in your Contrast agent."); - assertNotNull(testData.sessionMetadataName, "Session metadata name must be set"); - assertNotNull(testData.sessionMetadataValue, "Session metadata value must be set"); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); + assertThat(testData.hasSessionMetadata) + .as( + "Test application must have session metadata. Found app with route coverage but no" + + " session metadata. Please configure session metadata in your Contrast agent.") + .isTrue(); + assertThat(testData.sessionMetadataName).as("Session metadata name must be set").isNotNull(); + assertThat(testData.sessionMetadataValue).as("Session metadata value must be set").isNotNull(); // Act var response = @@ -392,16 +392,16 @@ void testGetRouteCoverage_SessionMetadataFilter_Success() throws IOException { testData.appId, testData.sessionMetadataName, testData.sessionMetadataValue, null); // Assert - assertNotNull(response, "Response should not be null"); - assertTrue(response.isSuccess(), "Response should indicate success"); - assertNotNull(response.getRoutes(), "Routes should not be null"); + assertThat(response).as("Response should not be null").isNotNull(); + assertThat(response.isSuccess()).as("Response should indicate success").isTrue(); + assertThat(response.getRoutes()).as("Routes should not be null").isNotNull(); - System.out.println( + log.info( "✓ Retrieved " + response.getRoutes().size() + " routes for application: " + testData.appName); - System.out.println( + log.info( " Filtered by session metadata: " + testData.sessionMetadataName + "=" @@ -409,15 +409,16 @@ void testGetRouteCoverage_SessionMetadataFilter_Success() throws IOException { // Verify route details are populated for (Route route : response.getRoutes()) { - assertNotNull( - route.getRouteDetailsResponse(), "Route details should be populated for filtered routes"); + assertThat(route.getRouteDetailsResponse()) + .as("Route details should be populated for filtered routes") + .isNotNull(); } if (response.getRoutes().size() > 0) { - System.out.println(" Sample routes:"); + log.info(" Sample routes:"); response.getRoutes().stream() .limit(3) - .forEach(route -> System.out.println(" - " + route.getSignature())); + .forEach(route -> log.info(" - {}", route.getSignature())); } } @@ -425,39 +426,42 @@ void testGetRouteCoverage_SessionMetadataFilter_Success() throws IOException { @Test void testGetRouteCoverage_LatestSession_Success() throws IOException { - System.out.println("\n=== Integration Test: get_route_coverage (latest session) ==="); + log.info("\n=== Integration Test: get_route_coverage (latest session) ==="); - assertNotNull(testData, "Test data must be discovered before running tests"); - assertTrue( - testData.hasSessionMetadata, - "Test application must have session metadata for latest session test. " - + "Please configure session metadata in your Contrast agent."); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); + assertThat(testData.hasSessionMetadata) + .as( + "Test application must have session metadata for latest session test. " + + "Please configure session metadata in your Contrast agent.") + .isTrue(); // Act var response = routeCoverageService.getRouteCoverage(testData.appId, null, null, true); // Assert - assertNotNull(response, "Response should not be null"); - assertTrue( - response.isSuccess(), - "Response should indicate success. Application should have session metadata."); - assertNotNull(response.getRoutes(), "Routes should not be null when success is true"); + assertThat(response).as("Response should not be null").isNotNull(); + assertThat(response.isSuccess()) + .as("Response should indicate success. Application should have session metadata.") + .isTrue(); + assertThat(response.getRoutes()) + .as("Routes should not be null when success is true") + .isNotNull(); - System.out.println( - "✓ Retrieved " + response.getRoutes().size() + " routes from latest session"); - System.out.println(" Application: " + testData.appName); + log.info("✓ Retrieved {} routes from latest session", response.getRoutes().size()); + log.info(" Application: {}", testData.appName); // Count exercised vs discovered long exercisedCount = response.getRoutes().stream().filter(route -> route.getExercised() > 0).count(); - System.out.println(" Exercised: " + exercisedCount); - System.out.println(" Discovered: " + (response.getRoutes().size() - exercisedCount)); + log.info(" Exercised: {}", exercisedCount); + log.info(" Discovered: {}", (response.getRoutes().size() - exercisedCount)); // Verify all routes have details for (Route route : response.getRoutes()) { - assertNotNull( - route.getRouteDetailsResponse(), "Route details should be populated for latest session"); + assertThat(route.getRouteDetailsResponse()) + .as("Route details should be populated for latest session") + .isNotNull(); } } @@ -465,13 +469,14 @@ void testGetRouteCoverage_LatestSession_Success() throws IOException { @Test void testGetRouteCoverage_CompareFilters() throws IOException { - System.out.println("\n=== Integration Test: Compare different filter types ==="); + log.info("\n=== Integration Test: Compare different filter types ==="); - assertNotNull(testData, "Test data must be discovered before running tests"); - assertTrue( - testData.hasSessionMetadata, - "Test application must have session metadata for comparison test. " - + "Please configure session metadata in your Contrast agent."); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); + assertThat(testData.hasSessionMetadata) + .as( + "Test application must have session metadata for comparison test. " + + "Please configure session metadata in your Contrast agent.") + .isTrue(); // Get route coverage using different filters var unfilteredResponse = @@ -485,35 +490,41 @@ void testGetRouteCoverage_CompareFilters() throws IOException { routeCoverageService.getRouteCoverage(testData.appId, null, null, true); // Assert all methods returned data - assertNotNull(unfilteredResponse, "Unfiltered response should not be null"); - assertNotNull(sessionMetadataResponse, "Session metadata response should not be null"); - assertNotNull(latestSessionResponse, "Latest session response should not be null"); - - assertTrue(unfilteredResponse.isSuccess(), "Unfiltered query should succeed"); - assertTrue(sessionMetadataResponse.isSuccess(), "Session metadata query should succeed"); - assertTrue(latestSessionResponse.isSuccess(), "Latest session query should succeed"); - - System.out.println("✓ All filter types work correctly:"); - System.out.println(" Unfiltered routes: " + unfilteredResponse.getRoutes().size()); - System.out.println(" Session metadata routes: " + sessionMetadataResponse.getRoutes().size()); - System.out.println(" Latest session routes: " + latestSessionResponse.getRoutes().size()); + assertThat(unfilteredResponse).as("Unfiltered response should not be null").isNotNull(); + assertThat(sessionMetadataResponse) + .as("Session metadata response should not be null") + .isNotNull(); + assertThat(latestSessionResponse).as("Latest session response should not be null").isNotNull(); + + assertThat(unfilteredResponse.isSuccess()).as("Unfiltered query should succeed").isTrue(); + assertThat(sessionMetadataResponse.isSuccess()) + .as("Session metadata query should succeed") + .isTrue(); + assertThat(latestSessionResponse.isSuccess()) + .as("Latest session query should succeed") + .isTrue(); + + log.info("✓ All filter types work correctly:"); + log.info(" Unfiltered routes: {}", unfilteredResponse.getRoutes().size()); + log.info(" Session metadata routes: {}", sessionMetadataResponse.getRoutes().size()); + log.info(" Latest session routes: {}", latestSessionResponse.getRoutes().size()); // Verify unfiltered should have >= filtered results (more data when not filtered) - assertTrue( - unfilteredResponse.getRoutes().size() >= sessionMetadataResponse.getRoutes().size(), - "Unfiltered query should return same or more routes than filtered query"); + assertThat(unfilteredResponse.getRoutes().size() >= sessionMetadataResponse.getRoutes().size()) + .as("Unfiltered query should return same or more routes than filtered query") + .isTrue(); // Latest session should have routes (since we validated session metadata exists) - assertTrue( - latestSessionResponse.getRoutes().size() > 0, - "Latest session query should return at least some routes"); + assertThat(latestSessionResponse.getRoutes().size() > 0) + .as("Latest session query should return at least some routes") + .isTrue(); } // ========== Error Handling Test ========== @Test void testGetRouteCoverage_InvalidAppId() { - System.out.println("\n=== Integration Test: Invalid app ID handling ==="); + log.info("\n=== Integration Test: Invalid app ID handling ==="); // Act - Use an invalid app ID that definitely doesn't exist boolean caughtException = false; @@ -522,28 +533,27 @@ void testGetRouteCoverage_InvalidAppId() { routeCoverageService.getRouteCoverage("invalid-app-id-12345", null, null, null); // If we get here, the API returned a response (possibly empty) - System.out.println("✓ API handled invalid app ID gracefully"); - System.out.println(" Routes returned: " + response.getRoutes().size()); + log.info("✓ API handled invalid app ID gracefully"); + log.info(" Routes returned: {}", response.getRoutes().size()); } catch (IOException e) { // This is also acceptable - API rejected the invalid app ID caughtException = true; - System.out.println("✓ API rejected invalid app ID with IOException: " + e.getMessage()); + log.info("✓ API rejected invalid app ID with IOException: {}", e.getMessage()); } catch (Exception e) { // Catch other exceptions like UnauthorizedException caughtException = true; - System.out.println( - "✓ API rejected invalid app ID with error: " + e.getClass().getSimpleName()); + log.info("✓ API rejected invalid app ID with error: {}", e.getClass().getSimpleName()); } - assertTrue( - caughtException || true, - "Either exception thrown or graceful handling - both are acceptable"); + assertThat(caughtException || true) + .as("Either exception thrown or graceful handling - both are acceptable") + .isTrue(); } @Test void testGetRouteCoverage_EmptyStrings_TreatedAsNull() throws Exception { - System.out.println("\n=== Integration Test: Empty string parameters (MCP-OU8 bug fix) ==="); + log.info("\n=== Integration Test: Empty string parameters (MCP-OU8 bug fix) ==="); // This test validates the fix for MCP-OU8: empty strings should be treated as null // and trigger the GET endpoint (unfiltered query) instead of the POST endpoint with empty @@ -553,38 +563,43 @@ void testGetRouteCoverage_EmptyStrings_TreatedAsNull() throws Exception { var response = routeCoverageService.getRouteCoverage(testData.appId, "", "", false); // Assert - assertNotNull(response, "Response should not be null"); - assertTrue(response.isSuccess(), "Response should be successful"); + assertThat(response).as("Response should not be null").isNotNull(); + assertThat(response.isSuccess()).as("Response should be successful").isTrue(); - System.out.println("✓ Response successful: " + response.isSuccess()); - System.out.println("✓ Routes returned: " + response.getRoutes().size()); + log.info("✓ Response successful: {}", response.isSuccess()); + log.info("✓ Routes returned: {}", response.getRoutes().size()); // The key assertion: empty strings should NOT return "No sessions found" message // This message indicates the POST endpoint was called incorrectly if (response.getMessages() != null && !response.getMessages().isEmpty()) { String combinedMessages = String.join(", ", response.getMessages()); - assertFalse( - combinedMessages.contains("No sessions found with the provided filters"), - "Empty strings should not trigger POST endpoint - messages should not contain 'No" - + " sessions found'"); - System.out.println("✓ Messages: " + combinedMessages); + assertThat(combinedMessages.contains("No sessions found with the provided filters")) + .as( + "Empty strings should not trigger POST endpoint - messages should not contain 'No" + + " sessions found'") + .isFalse(); + log.info("✓ Messages: {}", combinedMessages); } // Should return routes (assuming the app has route coverage) if (testData.hasRouteCoverage) { - assertTrue( - response.getRoutes().size() > 0, - "Empty strings should return all routes (unfiltered query) when app has route coverage"); - System.out.println("✓ Routes found via unfiltered query (empty strings treated as null)"); + assertThat(response.getRoutes().size() > 0) + .as( + "Empty strings should return all routes (unfiltered query) when app has route" + + " coverage") + .isTrue(); + log.info("✓ Routes found via unfiltered query (empty strings treated as null)"); // Verify route details are populated for (Route route : response.getRoutes()) { - assertNotNull(route.getRouteDetailsResponse(), "Each route should have details populated"); - assertTrue( - route.getRouteDetailsResponse().isSuccess(), - "Route details should be successfully loaded"); + assertThat(route.getRouteDetailsResponse()) + .as("Each route should have details populated") + .isNotNull(); + assertThat(route.getRouteDetailsResponse().isSuccess()) + .as("Route details should be successfully loaded") + .isTrue(); } - System.out.println("✓ All routes have valid route details"); + log.info("✓ All routes have valid route details"); } } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/RouteCoverageServiceTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/RouteCoverageServiceTest.java index 973131d..3c71aa6 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/RouteCoverageServiceTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/RouteCoverageServiceTest.java @@ -15,7 +15,8 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -71,8 +72,8 @@ class RouteCoverageServiceTest { @BeforeEach void setUp() throws Exception { routeCoverageService = new RouteCoverageService(); - mockContrastSDK = mock(ContrastSDK.class); - mockSDKExtension = mock(SDKExtension.class); + mockContrastSDK = mock(); + mockSDKExtension = mock(); // Mock the static SDKHelper.getSDK() method mockedSDKHelper = mockStatic(SDKHelper.class); @@ -180,9 +181,9 @@ void testGetRouteCoverage_UnfilteredQuery_Success() throws Exception { var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, null); // Assert - assertNotNull(result); - assertTrue(result.isSuccess()); - assertEquals(3, result.getRoutes().size()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getRoutes()).hasSize(3); // Verify SDK was called with null metadata (unfiltered query) verify(mockSDKExtension).getRouteCoverage(eq(TEST_ORG_ID), eq(TEST_APP_ID), isNull()); @@ -201,9 +202,9 @@ void testGetRouteCoverage_UnfilteredQuery_EmptyRoutes() throws Exception { var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, null); // Assert - assertNotNull(result); - assertTrue(result.isSuccess()); - assertEquals(0, result.getRoutes().size()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getRoutes()).hasSize(0); // Verify no route details calls made when no routes verify(mockSDKExtension, never()).getRouteDetails(anyString(), anyString(), anyString()); @@ -230,23 +231,23 @@ void testGetRouteCoverage_SessionMetadataFilter_Success() throws Exception { TEST_APP_ID, TEST_METADATA_NAME, TEST_METADATA_VALUE, null); // Assert - assertNotNull(result); - assertTrue(result.isSuccess()); - assertEquals(4, result.getRoutes().size()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getRoutes()).hasSize(4); // Verify metadata filter structure var captor = ArgumentCaptor.forClass(RouteCoverageBySessionIDAndMetadataRequestExtended.class); verify(mockSDKExtension).getRouteCoverage(eq(TEST_ORG_ID), eq(TEST_APP_ID), captor.capture()); var request = captor.getValue(); - assertNotNull(request); - assertNotNull(request.getValues()); - assertEquals(1, request.getValues().size()); + assertThat(request).isNotNull(); + assertThat(request.getValues()).isNotNull(); + assertThat(request.getValues()).hasSize(1); var metadata = request.getValues().get(0); - assertEquals(TEST_METADATA_NAME, metadata.getLabel()); - assertEquals(1, metadata.getValues().size()); - assertEquals(TEST_METADATA_VALUE, metadata.getValues().get(0)); + assertThat(metadata.getLabel()).isEqualTo(TEST_METADATA_NAME); + assertThat(metadata.getValues()).hasSize(1); + assertThat(metadata.getValues().get(0)).isEqualTo(TEST_METADATA_VALUE); // Verify route details fetched for each route verify(mockSDKExtension, times(4)) @@ -269,7 +270,7 @@ void testGetRouteCoverage_SessionMetadataFilter_MultipleRoutes() throws Exceptio TEST_APP_ID, TEST_METADATA_NAME, TEST_METADATA_VALUE, null); // Assert - assertEquals(5, result.getRoutes().size()); + assertThat(result.getRoutes()).hasSize(5); // Verify route details fetched for each route verify(mockSDKExtension, times(5)) @@ -277,21 +278,19 @@ void testGetRouteCoverage_SessionMetadataFilter_MultipleRoutes() throws Exceptio // Verify each route has details attached for (var route : result.getRoutes()) { - assertNotNull(route.getRouteDetailsResponse()); + assertThat(route.getRouteDetailsResponse()).isNotNull(); } } @Test void testGetRouteCoverage_SessionMetadataFilter_MissingValue() throws Exception { // Act & Assert - var exception = - assertThrows( - IllegalArgumentException.class, + assertThatThrownBy( () -> { routeCoverageService.getRouteCoverage(TEST_APP_ID, TEST_METADATA_NAME, null, null); - }); - - assertTrue(exception.getMessage().contains("sessionMetadataValue is required")); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("sessionMetadataValue is required"); // Verify SDK was never called verify(mockSDKExtension, never()).getRouteCoverage(anyString(), anyString(), any()); @@ -301,14 +300,12 @@ void testGetRouteCoverage_SessionMetadataFilter_MissingValue() throws Exception void testGetRouteCoverage_SessionMetadataFilter_EmptyValue() throws Exception { // Test validation with empty string for sessionMetadataValue (MCP-3EG) // Act & Assert - var exception = - assertThrows( - IllegalArgumentException.class, + assertThatThrownBy( () -> { routeCoverageService.getRouteCoverage(TEST_APP_ID, TEST_METADATA_NAME, "", null); - }); - - assertTrue(exception.getMessage().contains("sessionMetadataValue is required")); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("sessionMetadataValue is required"); // Verify SDK was never called verify(mockSDKExtension, never()).getRouteCoverage(anyString(), anyString(), any()); @@ -337,9 +334,9 @@ void testGetRouteCoverage_LatestSessionFilter_Success() throws Exception { var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, true); // Assert - assertNotNull(result); - assertTrue(result.isSuccess()); - assertEquals(2, result.getRoutes().size()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getRoutes()).hasSize(2); // Verify latest session was fetched verify(mockSDKExtension).getLatestSessionMetadata(eq(TEST_ORG_ID), eq(TEST_APP_ID)); @@ -349,7 +346,7 @@ void testGetRouteCoverage_LatestSessionFilter_Success() throws Exception { verify(mockSDKExtension).getRouteCoverage(eq(TEST_ORG_ID), eq(TEST_APP_ID), captor.capture()); var request = captor.getValue(); - assertNotNull(request); + assertThat(request).isNotNull(); // Note: Can't verify sessionId directly as it's protected in base class // But we can verify the method was called } @@ -364,8 +361,8 @@ void testGetRouteCoverage_LatestSessionFilter_NoSessionMetadata() throws Excepti var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, true); // Assert - Should return empty response with success=false - assertNotNull(result); - assertFalse(result.isSuccess()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isFalse(); // Verify route coverage was NOT called verify(mockSDKExtension, never()).getRouteCoverage(anyString(), anyString(), any()); @@ -383,8 +380,8 @@ void testGetRouteCoverage_LatestSessionFilter_NullAgentSession() throws Exceptio var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, true); // Assert - Should return empty response with success=false - assertNotNull(result); - assertFalse(result.isSuccess()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isFalse(); // Verify route coverage was NOT called verify(mockSDKExtension, never()).getRouteCoverage(anyString(), anyString(), any()); @@ -399,14 +396,12 @@ void testGetRouteCoverage_SDKThrowsIOException() throws Exception { .thenThrow(new IOException("API connection failed")); // Act & Assert - var exception = - assertThrows( - IOException.class, + assertThatThrownBy( () -> { routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, null); - }); - - assertEquals("API connection failed", exception.getMessage()); + }) + .isInstanceOf(IOException.class) + .hasMessage("API connection failed"); } @Test @@ -422,15 +417,13 @@ void testGetRouteCoverage_RouteDetailsFails() throws Exception { .thenThrow(new IOException("Failed to fetch route details")); // Act & Assert - var exception = - assertThrows( - IOException.class, + assertThatThrownBy( () -> { routeCoverageService.getRouteCoverage( TEST_APP_ID, TEST_METADATA_NAME, TEST_METADATA_VALUE, null); - }); - - assertTrue(exception.getMessage().contains("Failed to fetch route details")); + }) + .isInstanceOf(IOException.class) + .hasMessageContaining("Failed to fetch route details"); } @Test @@ -440,14 +433,12 @@ void testGetRouteCoverage_LatestSessionFetchFails() throws Exception { .thenThrow(new IOException("Session metadata API failed")); // Act & Assert - var exception = - assertThrows( - IOException.class, + assertThatThrownBy( () -> { routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, true); - }); - - assertTrue(exception.getMessage().contains("Session metadata API failed")); + }) + .isInstanceOf(IOException.class) + .hasMessageContaining("Session metadata API failed"); } // ========== SDK Configuration Tests ========== @@ -489,8 +480,8 @@ void testGetRouteCoverage_AllParametersNull() throws Exception { var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, null); // Assert - assertNotNull(result); - assertTrue(result.isSuccess()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isTrue(); verify(mockSDKExtension).getRouteCoverage(eq(TEST_ORG_ID), eq(TEST_APP_ID), isNull()); } @@ -507,8 +498,8 @@ void testGetRouteCoverage_UseLatestSessionFalse() throws Exception { var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, null, null, false); // Assert - assertNotNull(result); - assertTrue(result.isSuccess()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isTrue(); verify(mockSDKExtension).getRouteCoverage(eq(TEST_ORG_ID), eq(TEST_APP_ID), isNull()); verify(mockSDKExtension, never()).getLatestSessionMetadata(anyString(), anyString()); } @@ -527,9 +518,9 @@ void testGetRouteCoverage_EmptyStringParameters_TreatedAsNull() throws Exception var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, "", "", false); // Assert - Should call SDK with null (unfiltered query), not with empty metadata filter - assertNotNull(result); - assertTrue(result.isSuccess()); - assertEquals(2, result.getRoutes().size()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getRoutes()).hasSize(2); // Verify SDK was called with null metadata (unfiltered query) - GET endpoint verify(mockSDKExtension).getRouteCoverage(eq(TEST_ORG_ID), eq(TEST_APP_ID), isNull()); @@ -559,8 +550,8 @@ void testGetRouteCoverage_EmptySessionMetadataNameOnly_TreatedAsNull() throws Ex var result = routeCoverageService.getRouteCoverage(TEST_APP_ID, "", null, null); // Assert - assertNotNull(result); - assertTrue(result.isSuccess()); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isTrue(); verify(mockSDKExtension).getRouteCoverage(eq(TEST_ORG_ID), eq(TEST_APP_ID), isNull()); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/SCAServiceIntegrationTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/SCAServiceIntegrationTest.java index bfcdb04..59aa198 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/SCAServiceIntegrationTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/SCAServiceIntegrationTest.java @@ -15,13 +15,16 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; import com.contrast.labs.ai.mcp.contrast.sdkextension.SDKExtension; import com.contrast.labs.ai.mcp.contrast.sdkextension.SDKHelper; import com.contrast.labs.ai.mcp.contrast.sdkextension.data.LibraryExtended; import com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application; import java.io.IOException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -45,6 +48,7 @@ * *

Or skip integration tests: mvn verify -DskipITs */ +@Slf4j @SpringBootTest @EnabledIfEnvironmentVariable(named = "CONTRAST_HOST_NAME", matches = ".+") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -96,12 +100,10 @@ public String toString() { @BeforeAll void discoverTestData() { - System.out.println( + log.info( "\n╔════════════════════════════════════════════════════════════════════════════════╗"); - System.out.println( - "║ SCA Service Integration Test - Discovering Test Data ║"); - System.out.println( - "╚════════════════════════════════════════════════════════════════════════════════╝"); + log.info("║ SCA Service Integration Test - Discovering Test Data ║"); + log.info("╚════════════════════════════════════════════════════════════════════════════════╝"); try { var sdk = @@ -109,21 +111,21 @@ void discoverTestData() { var sdkExtension = new SDKExtension(sdk); // Get all applications - System.out.println("\n🔍 Step 1: Fetching all applications..."); + log.info("\n🔍 Step 1: Fetching all applications..."); var appsResponse = sdkExtension.getApplications(orgID); var applications = appsResponse.getApplications(); - System.out.println(" Found " + applications.size() + " application(s) in organization"); + log.info(" Found {} application(s) in organization", applications.size()); if (applications.isEmpty()) { - System.out.println("\n⚠️ NO APPLICATIONS FOUND"); - System.out.println(" The integration tests require at least one application with:"); - System.out.println(" 1. Third-party libraries"); - System.out.println(" 2. Optionally: Libraries with known CVEs"); + log.info("\n⚠️ NO APPLICATIONS FOUND"); + log.info(" The integration tests require at least one application with:"); + log.info(" 1. Third-party libraries"); + log.info(" 2. Optionally: Libraries with known CVEs"); return; } // Search for application with libraries - System.out.println("\n🔍 Step 2: Searching for application with libraries..."); + log.info("\n🔍 Step 2: Searching for application with libraries..."); TestData bestCandidate = null; TestData fallbackCandidate = null; int appsChecked = 0; @@ -131,28 +133,23 @@ void discoverTestData() { for (Application app : applications) { if (appsChecked >= maxAppsToCheck) { - System.out.println( - " Reached max apps to check (" + maxAppsToCheck + "), stopping search"); + log.info(" Reached max apps to check ({}), stopping search", maxAppsToCheck); break; } appsChecked++; - System.out.println( - " Checking app " - + appsChecked - + "/" - + maxAppsToCheck - + ": " - + app.getName() - + " (ID: " - + app.getAppId() - + ")"); + log.info( + " Checking app {}/{}: {} (ID: {})", + appsChecked, + maxAppsToCheck, + app.getName(), + app.getAppId()); try { // Check for libraries var libraries = SDKHelper.getLibsForID(app.getAppId(), orgID, sdkExtension); if (libraries != null && !libraries.isEmpty()) { - System.out.println(" ✓ Has " + libraries.size() + " library/libraries"); + log.info(" ✓ Has {} library/libraries", libraries.size()); var candidate = new TestData(); candidate.appId = app.getAppId(); @@ -168,8 +165,8 @@ void discoverTestData() { var firstVuln = lib.getVulnerabilities().get(0); if (firstVuln.getName() != null && firstVuln.getName().startsWith("CVE-")) { candidate.vulnerableCveId = firstVuln.getName(); - System.out.println( - " ✓ Has vulnerable library with CVE: " + candidate.vulnerableCveId); + log.info( + " ✓ Has vulnerable library with CVE: {}", candidate.vulnerableCveId); break; } } @@ -177,21 +174,21 @@ void discoverTestData() { // Perfect candidate: has libraries AND vulnerable libraries with CVEs if (candidate.hasVulnerableLibrary && candidate.vulnerableCveId != null) { - System.out.println("\n ✅ Found PERFECT application with libraries AND CVEs!"); + log.info("\n ✅ Found PERFECT application with libraries AND CVEs!"); bestCandidate = candidate; break; } // Fallback: has libraries but no CVEs if (fallbackCandidate == null) { - System.out.println(" ℹ Saving as fallback (has libraries but no CVEs found)"); + log.info(" ℹ Saving as fallback (has libraries but no CVEs found)"); fallbackCandidate = candidate; } } else { - System.out.println(" ℹ No libraries found"); + log.info(" ℹ No libraries found"); } } catch (Exception e) { - System.out.println(" ℹ Error checking libraries: " + e.getMessage()); + log.info(" ℹ Error checking libraries: {}", e.getMessage()); } } @@ -200,32 +197,30 @@ void discoverTestData() { if (candidate != null) { testData = candidate; - System.out.println( + log.info( "\n╔════════════════════════════════════════════════════════════════════════════════╗"); - System.out.println( + log.info( "║ Test Data Discovery Complete ║"); - System.out.println( + log.info( "╚════════════════════════════════════════════════════════════════════════════════╝"); - System.out.println(testData); - System.out.println(); + log.info("{}", testData); + log.info(""); // Warn if no CVEs found if (!candidate.hasVulnerableLibrary) { - System.err.println( - "\n⚠️ WARNING: Application has libraries but NO VULNERABLE LIBRARIES"); - System.err.println(" CVE-related tests will be skipped."); - System.err.println( - " To enable full testing, use an application with vulnerable dependencies."); + log.error("\n⚠️ WARNING: Application has libraries but NO VULNERABLE LIBRARIES"); + log.error(" CVE-related tests will be skipped."); + log.error(" To enable full testing, use an application with vulnerable dependencies."); } } else { String errorMsg = buildTestDataErrorMessage(appsChecked); - System.err.println(errorMsg); + log.error(errorMsg); fail(errorMsg); } } catch (Exception e) { String errorMsg = "❌ ERROR during test data discovery: " + e.getMessage(); - System.err.println("\n" + errorMsg); + log.error("\n{}", errorMsg); e.printStackTrace(); fail(errorMsg); } @@ -292,20 +287,22 @@ private String buildTestDataErrorMessage(int appsChecked) { @Test void testDiscoveredTestDataExists() { - System.out.println("\n=== Integration Test: Validate test data discovery ==="); - - assertNotNull(testData, "Test data should have been discovered in @BeforeAll"); - assertNotNull(testData.appId, "Test application ID should be set"); - assertTrue(testData.hasLibraries, "Test application should have libraries"); - assertTrue(testData.libraryCount > 0, "Test application should have at least 1 library"); - - System.out.println("✓ Test data validated:"); - System.out.println(" App ID: " + testData.appId); - System.out.println(" App Name: " + testData.appName); - System.out.println(" Library Count: " + testData.libraryCount); - System.out.println(" Has Vulnerable Libraries: " + testData.hasVulnerableLibrary); + log.info("\n=== Integration Test: Validate test data discovery ==="); + + assertThat(testData).as("Test data should have been discovered in @BeforeAll").isNotNull(); + assertThat(testData.appId).as("Test application ID should be set").isNotNull(); + assertThat(testData.hasLibraries).as("Test application should have libraries").isTrue(); + assertThat(testData.libraryCount > 0) + .as("Test application should have at least 1 library") + .isTrue(); + + log.info("✓ Test data validated:"); + log.info(" App ID: {}", testData.appId); + log.info(" App Name: {}", testData.appName); + log.info(" Library Count: {}", testData.libraryCount); + log.info(" Has Vulnerable Libraries: {}", testData.hasVulnerableLibrary); if (testData.vulnerableCveId != null) { - System.out.println(" Sample CVE: " + testData.vulnerableCveId); + log.info(" Sample CVE: {}", testData.vulnerableCveId); } } @@ -313,132 +310,128 @@ void testDiscoveredTestDataExists() { @Test void testListApplicationLibraries_Success() throws IOException { - System.out.println("\n=== Integration Test: list_application_libraries_by_app_id ==="); + log.info("\n=== Integration Test: list_application_libraries_by_app_id ==="); - assertNotNull(testData, "Test data must be discovered before running tests"); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); // Act var libraries = scaService.getApplicationLibrariesByID(testData.appId); // Assert - assertNotNull(libraries, "Libraries list should not be null"); - assertTrue(libraries.size() > 0, "Should have at least 1 library"); + assertThat(libraries).as("Libraries list should not be null").isNotNull(); + assertThat(libraries).as("Should have at least 1 library").isNotEmpty(); - System.out.println( - "✓ Retrieved " + libraries.size() + " libraries for application: " + testData.appName); + log.info("✓ Retrieved {} libraries for application: {}", libraries.size(), testData.appName); // Print sample libraries - System.out.println(" Sample libraries:"); + log.info(" Sample libraries:"); libraries.stream() .limit(5) .forEach( lib -> { - System.out.println( - " - " - + lib.getFilename() - + " (version: " - + lib.getVersion() - + ", classes used: " - + lib.getClassedUsed() - + "/" - + lib.getClassCount() - + ")"); + log.info( + " - {} (version: {}, classes used: {}/{})", + lib.getFilename(), + lib.getVersion(), + lib.getClassedUsed(), + lib.getClassCount()); }); // Verify library structure for (LibraryExtended lib : libraries) { - assertNotNull(lib.getFilename(), "Library filename should not be null"); - assertNotNull(lib.getHash(), "Library hash should not be null"); - assertTrue(lib.getClassCount() >= 0, "Class count should be non-negative"); - assertTrue(lib.getClassedUsed() >= 0, "Classes used should be non-negative"); + assertThat(lib.getFilename()).as("Library filename should not be null").isNotNull(); + assertThat(lib.getHash()).as("Library hash should not be null").isNotNull(); + assertThat(lib.getClassCount()).as("Class count should be non-negative").isNotNegative(); + assertThat(lib.getClassedUsed()).as("Classes used should be non-negative").isNotNegative(); } } @Test void testListApplicationLibraries_ClassUsageIndicatesUsage() throws IOException { - System.out.println("\n=== Integration Test: Class usage statistics ==="); + log.info("\n=== Integration Test: Class usage statistics ==="); - assertNotNull(testData, "Test data must be discovered before running tests"); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); // Act var libraries = scaService.getApplicationLibrariesByID(testData.appId); // Assert - assertNotNull(libraries); - assertFalse(libraries.isEmpty()); + assertThat(libraries).isNotNull(); + assertThat(libraries).isNotEmpty(); - System.out.println("✓ Analyzing class usage for " + libraries.size() + " libraries:"); + log.info("✓ Analyzing class usage for {} libraries:", libraries.size()); // Count libraries by usage long activeLibs = libraries.stream().filter(lib -> lib.getClassedUsed() > 0).count(); long unusedLibs = libraries.stream().filter(lib -> lib.getClassedUsed() == 0).count(); - System.out.println(" Active libraries (classes used > 0): " + activeLibs); - System.out.println(" Likely unused libraries (classes used = 0): " + unusedLibs); + log.info(" Active libraries (classes used > 0): {}", activeLibs); + log.info(" Likely unused libraries (classes used = 0): {}", unusedLibs); // Verify class usage makes sense for (LibraryExtended lib : libraries) { - assertTrue( - lib.getClassedUsed() <= lib.getClassCount(), - "Classes used should not exceed total class count for " + lib.getFilename()); + assertThat(lib.getClassedUsed() <= lib.getClassCount()) + .as("Classes used should not exceed total class count for " + lib.getFilename()) + .isTrue(); } - System.out.println("✓ Class usage statistics are valid"); + log.info("✓ Class usage statistics are valid"); } // ========== Test Case 3: CVE Lookup (if CVE available) ========== @Test void testListApplicationsVulnerableToCVE_Success() throws IOException { - System.out.println("\n=== Integration Test: list_applications_vulnerable_to_cve ==="); + log.info("\n=== Integration Test: list_applications_vulnerable_to_cve ==="); if (testData.vulnerableCveId == null) { - System.out.println("⚠️ SKIPPED: No vulnerable libraries with CVEs found in test data"); - System.out.println(" To enable this test, use an application with vulnerable dependencies"); + log.info("⚠️ SKIPPED: No vulnerable libraries with CVEs found in test data"); + log.info(" To enable this test, use an application with vulnerable dependencies"); return; } - assertNotNull(testData, "Test data must be discovered before running tests"); + assertThat(testData).as("Test data must be discovered before running tests").isNotNull(); // Act var cveData = scaService.listCVESForApplication(testData.vulnerableCveId); // Assert - assertNotNull(cveData, "CVE data should not be null"); - assertNotNull(cveData.getApps(), "Apps list should not be null"); - assertNotNull(cveData.getLibraries(), "Libraries list should not be null"); + assertThat(cveData).as("CVE data should not be null").isNotNull(); + assertThat(cveData.getApps()).as("Apps list should not be null").isNotNull(); + assertThat(cveData.getLibraries()).as("Libraries list should not be null").isNotNull(); - System.out.println("✓ Retrieved CVE data for: " + testData.vulnerableCveId); - System.out.println(" Affected applications: " + cveData.getApps().size()); - System.out.println(" Vulnerable libraries: " + cveData.getLibraries().size()); + log.info("✓ Retrieved CVE data for: {}", testData.vulnerableCveId); + log.info(" Affected applications: {}", cveData.getApps().size()); + log.info(" Vulnerable libraries: {}", cveData.getLibraries().size()); // Verify our test app is in the list boolean foundTestApp = cveData.getApps().stream().anyMatch(app -> app.getApp_id().equals(testData.appId)); if (foundTestApp) { - System.out.println(" ✓ Test application '" + testData.appName + "' is in the affected list"); + log.info(" ✓ Test application '{}' is in the affected list", testData.appName); } // Verify library data - assertFalse(cveData.getLibraries().isEmpty(), "Should have at least one vulnerable library"); + assertThat(cveData.getLibraries().isEmpty()) + .as("Should have at least one vulnerable library") + .isFalse(); - System.out.println(" Sample vulnerable libraries:"); + log.info(" Sample vulnerable libraries:"); cveData.getLibraries().stream() .limit(3) .forEach( lib -> { - System.out.println( - " - " + lib.getFile_name() + " (version: " + lib.getVersion() + ")"); + log.info(" - {} (version: {})", lib.getFile_name(), lib.getVersion()); }); } @Test void testListApplicationsVulnerableToCVE_ClassUsagePopulated() throws IOException { - System.out.println("\n=== Integration Test: CVE class usage population ==="); + log.info("\n=== Integration Test: CVE class usage population ==="); if (testData.vulnerableCveId == null) { - System.out.println("⚠️ SKIPPED: No vulnerable libraries with CVEs found in test data"); + log.info("⚠️ SKIPPED: No vulnerable libraries with CVEs found in test data"); return; } @@ -446,15 +439,14 @@ void testListApplicationsVulnerableToCVE_ClassUsagePopulated() throws IOExceptio var cveData = scaService.listCVESForApplication(testData.vulnerableCveId); // Assert - assertNotNull(cveData); - assertNotNull(cveData.getApps()); + assertThat(cveData).isNotNull(); + assertThat(cveData.getApps()).isNotNull(); - System.out.println( - "✓ Checking class usage data for " + cveData.getApps().size() + " affected applications:"); + log.info("✓ Checking class usage data for {} affected applications:", cveData.getApps().size()); // Verify class usage is populated for apps (implementation populates this) for (var app : cveData.getApps()) { - System.out.println( + log.info( " App: " + app.getName() + " (class count: " @@ -464,18 +456,19 @@ void testListApplicationsVulnerableToCVE_ClassUsagePopulated() throws IOExceptio + ")"); // Class count should be >= 0 - assertTrue( - app.getClassCount() >= 0, "Class count should be non-negative for app: " + app.getName()); + assertThat(app.getClassCount() >= 0) + .as("Class count should be non-negative for app: " + app.getName()) + .isTrue(); } - System.out.println("✓ Class usage data is populated correctly"); + log.info("✓ Class usage data is populated correctly"); } // ========== Test Case 4: Error Handling ========== @Test void testListApplicationLibraries_InvalidAppId() { - System.out.println("\n=== Integration Test: Invalid app ID handling ==="); + log.info("\n=== Integration Test: Invalid app ID handling ==="); // Act - Use an invalid app ID boolean caughtException = false; @@ -483,36 +476,30 @@ void testListApplicationLibraries_InvalidAppId() { var libraries = scaService.getApplicationLibrariesByID("invalid-app-id-12345"); // If we get here, API handled it gracefully - System.out.println("✓ API handled invalid app ID gracefully"); - System.out.println( - " Libraries returned: " + (libraries != null ? libraries.size() : "null")); + log.info("✓ API handled invalid app ID gracefully"); + log.info(" Libraries returned: {}", (libraries != null ? libraries.size() : "null")); } catch (Exception e) { caughtException = true; - System.out.println( - "✓ API rejected invalid app ID with exception: " + e.getClass().getSimpleName()); + log.info("✓ API rejected invalid app ID with exception: {}", e.getClass().getSimpleName()); } - assertTrue(true, "Test passes if either exception or graceful handling occurs"); + assertThat(true).as("Test passes if either exception or graceful handling occurs").isTrue(); } @Test void testListApplicationsVulnerableToCVE_InvalidCVE() { - System.out.println("\n=== Integration Test: Invalid CVE ID handling ==="); + log.info("\n=== Integration Test: Invalid CVE ID handling ==="); // Act & Assert - Non-existent CVE should throw IOException - var exception = - assertThrows( - IOException.class, + assertThatThrownBy( () -> { scaService.listCVESForApplication("CVE-9999-NONEXISTENT"); - }, - "Non-existent CVE should throw IOException"); - - System.out.println("✓ Non-existent CVE correctly rejected with IOException"); - System.out.println(" Exception message: " + exception.getMessage()); - assertTrue( - exception.getMessage().contains("Failed to retrieve CVE data"), - "Exception message should indicate CVE retrieval failure"); + }) + .as("Non-existent CVE should throw IOException") + .isInstanceOf(IOException.class) + .hasMessageContaining("Failed to retrieve CVE data"); + + log.info("✓ Non-existent CVE correctly rejected with IOException"); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/SCAServiceTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/SCAServiceTest.java index 1308a5b..4259437 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/SCAServiceTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/SCAServiceTest.java @@ -15,7 +15,8 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -120,8 +121,8 @@ void testGetApplicationLibrariesByID_Success() throws IOException { var result = scaService.getApplicationLibrariesByID(TEST_APP_ID); // Then - assertNotNull(result, "Result should not be null"); - assertEquals(3, result.size(), "Should return 3 libraries"); + assertThat(result).as("Result should not be null").isNotNull(); + assertThat(result.size()).as("Should return 3 libraries").isEqualTo(3); // Verify SDKHelper was called correctly mockedSDKHelper.verify( @@ -150,30 +151,30 @@ void testGetApplicationLibrariesByID_EmptyList() throws IOException { var result = scaService.getApplicationLibrariesByID(TEST_APP_ID); // Then - assertNotNull(result, "Result should not be null"); - assertTrue(result.isEmpty(), "Result should be empty list"); + assertThat(result).as("Result should not be null").isNotNull(); + assertThat(result.isEmpty()).as("Result should be empty list").isTrue(); } @Test void testGetApplicationLibrariesByID_NullAppID() { // When/Then - Should handle null gracefully or throw descriptive exception - assertThrows( - Exception.class, - () -> { - scaService.getApplicationLibrariesByID(null); - }, - "Should throw exception for null app ID"); + assertThatThrownBy( + () -> { + scaService.getApplicationLibrariesByID(null); + }) + .as("Should throw exception for null app ID") + .isInstanceOf(Exception.class); } @Test void testGetApplicationLibrariesByID_EmptyAppID() { // When/Then - Should handle empty string appropriately - assertThrows( - Exception.class, - () -> { - scaService.getApplicationLibrariesByID(""); - }, - "Should throw exception for empty app ID"); + assertThatThrownBy( + () -> { + scaService.getApplicationLibrariesByID(""); + }) + .as("Should throw exception for empty app ID") + .isInstanceOf(Exception.class); } @Test @@ -184,16 +185,12 @@ void testGetApplicationLibrariesByID_SDKFailure() { .thenThrow(new RuntimeException("SDK connection failed")); // When/Then - var exception = - assertThrows( - RuntimeException.class, + assertThatThrownBy( () -> { scaService.getApplicationLibrariesByID(TEST_APP_ID); - }); - - assertTrue( - exception.getMessage().contains("SDK connection failed"), - "Exception message should indicate SDK failure"); + }) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("SDK connection failed"); } @Test @@ -209,13 +206,17 @@ void testGetApplicationLibrariesByID_VerifiesClassUsage() throws IOException { var result = scaService.getApplicationLibrariesByID(TEST_APP_ID); // Then - assertEquals(2, result.size()); + assertThat(result.size()).isEqualTo(2); // Verify first library has class usage > 0 (actively used) - assertTrue(result.get(0).getClassedUsed() > 0, "First library should have classes used"); + assertThat(result.get(0).getClassedUsed()) + .as("First library should have classes used") + .isGreaterThan(0); // Verify second library has class usage = 0 (likely unused) - assertEquals(0, result.get(1).getClassedUsed(), "Second library should have zero classes used"); + assertThat(result.get(1).getClassedUsed()) + .as("Second library should have zero classes used") + .isEqualTo(0); } // ========== Tests for list_applications_vulnerable_to_cve ========== @@ -227,7 +228,7 @@ void testListCVESForApplication_Success() throws IOException { var mockLibraries = createMockLibrariesWithClassUsage(); // Mock SDKExtension.getAppsForCVE - var mockExtension = mock(SDKExtension.class); + SDKExtension mockExtension = mock(); when(mockExtension.getAppsForCVE(eq(TEST_ORG_ID), eq(TEST_CVE_ID))).thenReturn(mockCveData); // Replace mockedSDKExtension to return our configured mock @@ -250,10 +251,10 @@ void testListCVESForApplication_Success() throws IOException { var result = scaService.listCVESForApplication(TEST_CVE_ID); // Then - assertNotNull(result, "Result should not be null"); - assertNotNull(result.getApps(), "Apps list should not be null"); - assertNotNull(result.getLibraries(), "Libraries list should not be null"); - assertFalse(result.getApps().isEmpty(), "Should have at least one app"); + assertThat(result).as("Result should not be null").isNotNull(); + assertThat(result.getApps()).as("Apps list should not be null").isNotNull(); + assertThat(result.getLibraries()).as("Libraries list should not be null").isNotNull(); + assertThat(result.getApps()).as("Should have at least one app").isNotEmpty(); } @Test @@ -277,8 +278,8 @@ void testListCVESForApplication_NoCVEFound() throws IOException { var result = scaService.listCVESForApplication("CVE-9999-NONEXISTENT"); // Then - assertNotNull(result, "Result should not be null"); - assertTrue(result.getApps().isEmpty(), "Should have no vulnerable apps"); + assertThat(result).as("Result should not be null").isNotNull(); + assertThat(result.getApps()).as("Should have no vulnerable apps").isEmpty(); } @Test @@ -306,13 +307,15 @@ void testListCVESForApplication_ClassUsagePopulation() throws IOException { var result = scaService.listCVESForApplication(TEST_CVE_ID); // Then - assertNotNull(result, "Result should not be null"); - assertTrue(result.getApps().size() > 0, "Should have apps"); + assertThat(result).as("Result should not be null").isNotNull(); + assertThat(result.getApps()).as("Should have apps").isNotEmpty(); // Verify class usage was populated for apps // (Implementation in SCAService populates classCount and classUsage fields) var firstApp = result.getApps().get(0); - assertTrue(firstApp.getClassCount() >= 0, "Class count should be populated"); + assertThat(firstApp.getClassCount()) + .as("Class count should be populated") + .isGreaterThanOrEqualTo(0); } // ========== Helper Methods ========== @@ -320,7 +323,7 @@ void testListCVESForApplication_ClassUsagePopulation() throws IOException { private List createMockLibraries(int count) { var libraries = new ArrayList(); for (int i = 0; i < count; i++) { - var lib = mock(LibraryExtended.class); + LibraryExtended lib = mock(); when(lib.getFilename()).thenReturn("library-" + i + ".jar"); when(lib.getHash()).thenReturn("hash-" + i); when(lib.getVersion()).thenReturn("1.0." + i); @@ -335,7 +338,7 @@ private List createMockLibrariesWithClassUsage() { var libraries = new ArrayList(); // Library 1: Actively used (classesUsed > 0) - var lib1 = mock(LibraryExtended.class); + LibraryExtended lib1 = mock(); when(lib1.getFilename()).thenReturn("actively-used-lib.jar"); when(lib1.getHash()).thenReturn("hash-active-123"); when(lib1.getVersion()).thenReturn("2.1.0"); @@ -344,7 +347,7 @@ private List createMockLibrariesWithClassUsage() { libraries.add(lib1); // Library 2: Likely unused (classesUsed = 0) - var lib2 = mock(LibraryExtended.class); + LibraryExtended lib2 = mock(); when(lib2.getFilename()).thenReturn("unused-lib.jar"); when(lib2.getHash()).thenReturn("hash-unused-456"); when(lib2.getVersion()).thenReturn("1.5.2"); @@ -358,7 +361,7 @@ private List createMockLibrariesWithClassUsage() { private List createMockLibrariesWithMatchingHash() { var libraries = new ArrayList(); - var lib = mock(LibraryExtended.class); + LibraryExtended lib = mock(); when(lib.getFilename()).thenReturn("vulnerable-lib.jar"); when(lib.getHash()).thenReturn("matching-hash-789"); when(lib.getVersion()).thenReturn("1.0.0"); diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/VulnerabilityFilterParamsTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/VulnerabilityFilterParamsTest.java index b70f6e1..4aefbbf 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/VulnerabilityFilterParamsTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/VulnerabilityFilterParamsTest.java @@ -15,7 +15,8 @@ */ package com.contrast.labs.ai.mcp.contrast; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.contrastsecurity.http.RuleSeverity; import com.contrastsecurity.http.TraceFilterForm; @@ -36,29 +37,29 @@ void testValidFiltersAllProvided() { "2025-12-31", "reviewed"); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); - assertEquals("app123", params.appId()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); + assertThat(params.appId()).isEqualTo("app123"); var form = params.toTraceFilterForm(); - assertNotNull(form.getSeverities()); - assertEquals(2, form.getSeverities().size()); - assertTrue(form.getSeverities().contains(RuleSeverity.CRITICAL)); - assertTrue(form.getSeverities().contains(RuleSeverity.HIGH)); + assertThat(form.getSeverities()).isNotNull(); + assertThat(form.getSeverities()).hasSize(2); + assertThat(form.getSeverities()).contains(RuleSeverity.CRITICAL); + assertThat(form.getSeverities()).contains(RuleSeverity.HIGH); } @Test void testNoFiltersProvided() { var params = VulnerabilityFilterParams.of(null, null, null, null, null, null, null, null); - assertTrue(params.isValid()); - assertFalse(params.warnings().isEmpty()); // Should have smart defaults warning - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.warnings()).isNotEmpty(); // Should have smart defaults warning + assertThat(params.errors()).isEmpty(); // Smart defaults should be applied to status var form = params.toTraceFilterForm(); - assertNotNull(form.getStatus()); - assertEquals(3, form.getStatus().size()); + assertThat(form.getStatus()).isNotNull(); + assertThat(form.getStatus()).hasSize(3); } @Test @@ -67,10 +68,10 @@ void testInvalidSeverityHardFailure() { VulnerabilityFilterParams.of( "CRITICAL,SUPER_HIGH", null, null, null, null, null, null, null); - assertFalse(params.isValid()); - assertEquals(1, params.errors().size()); - assertTrue(params.errors().get(0).contains("Invalid severity 'SUPER_HIGH'")); - assertTrue(params.errors().get(0).contains("Valid: CRITICAL, HIGH, MEDIUM, LOW, NOTE")); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(1); + assertThat(params.errors().get(0)).contains("Invalid severity 'SUPER_HIGH'"); + assertThat(params.errors().get(0)).contains("Valid: CRITICAL, HIGH, MEDIUM, LOW, NOTE"); } @Test @@ -79,8 +80,8 @@ void testAllInvalidSeveritiesHardFailure() { VulnerabilityFilterParams.of( "SUPER_HIGH,ULTRA_CRITICAL", null, null, null, null, null, null, null); - assertFalse(params.isValid()); - assertEquals(2, params.errors().size()); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(2); } @Test @@ -88,14 +89,11 @@ void testInvalidStatusHardFailure() { var params = VulnerabilityFilterParams.of(null, "Reported,Invalid", null, null, null, null, null, null); - assertFalse(params.isValid()); - assertEquals(1, params.errors().size()); - assertTrue(params.errors().get(0).contains("Invalid status 'Invalid'")); - assertTrue( - params - .errors() - .get(0) - .contains("Valid: Reported, Suspicious, Confirmed, Remediated, Fixed")); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(1); + assertThat(params.errors().get(0)).contains("Invalid status 'Invalid'"); + assertThat(params.errors().get(0)) + .contains("Valid: Reported, Suspicious, Confirmed, Remediated, Fixed"); } @Test @@ -104,8 +102,8 @@ void testMultipleInvalidStatusesHardFailure() { VulnerabilityFilterParams.of( null, "Reported,BadStatus,AnotherBad", null, null, null, null, null, null); - assertFalse(params.isValid()); - assertEquals(2, params.errors().size()); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(2); } @Test @@ -114,10 +112,10 @@ void testInvalidEnvironmentHardFailure() { VulnerabilityFilterParams.of( null, null, null, null, "PRODUCTION,STAGING", null, null, null); - assertFalse(params.isValid()); - assertEquals(1, params.errors().size()); - assertTrue(params.errors().get(0).contains("Invalid environment 'STAGING'")); - assertTrue(params.errors().get(0).contains("Valid: DEVELOPMENT, QA, PRODUCTION")); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(1); + assertThat(params.errors().get(0)).contains("Invalid environment 'STAGING'"); + assertThat(params.errors().get(0)).contains("Valid: DEVELOPMENT, QA, PRODUCTION"); } @Test @@ -125,10 +123,10 @@ void testUnparseableDateHardFailure() { var params = VulnerabilityFilterParams.of(null, null, null, null, null, "not-a-date", null, null); - assertFalse(params.isValid()); - assertEquals(1, params.errors().size()); - assertTrue(params.errors().get(0).contains("Invalid lastSeenAfter date 'not-a-date'")); - assertTrue(params.errors().get(0).contains("Expected ISO format")); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(1); + assertThat(params.errors().get(0)).contains("Invalid lastSeenAfter date 'not-a-date'"); + assertThat(params.errors().get(0)).contains("Expected ISO format"); } @Test @@ -137,10 +135,10 @@ void testDateRangeContradictionHardFailure() { VulnerabilityFilterParams.of( null, null, null, null, null, "2025-12-31", "2025-01-01", null); - assertFalse(params.isValid()); - assertEquals(1, params.errors().size()); - assertTrue(params.errors().get(0).contains("Invalid date range")); - assertTrue(params.errors().get(0).contains("lastSeenAfter must be before lastSeenBefore")); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(1); + assertThat(params.errors().get(0)).contains("Invalid date range"); + assertThat(params.errors().get(0)).contains("lastSeenAfter must be before lastSeenBefore"); } @Test @@ -149,14 +147,14 @@ void testValidDateRange() { VulnerabilityFilterParams.of( null, null, null, null, null, "2025-01-01", "2025-12-31", null); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); - assertFalse( - params.warnings().isEmpty()); // Should have time filter warning and smart defaults warning + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); + assertThat(params.warnings()) + .isNotEmpty(); // Should have time filter warning and smart defaults warning var form = params.toTraceFilterForm(); - assertNotNull(form.getStartDate()); - assertNotNull(form.getEndDate()); + assertThat(form.getStartDate()).isNotNull(); + assertThat(form.getEndDate()).isNotNull(); } @Test @@ -172,12 +170,12 @@ void testEpochTimestampDates() { "1735689600000", // 2025-01-01 in epoch null); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); TraceFilterForm form = params.toTraceFilterForm(); - assertNotNull(form.getStartDate()); - assertNotNull(form.getEndDate()); + assertThat(form.getStartDate()).isNotNull(); + assertThat(form.getEndDate()).isNotNull(); } @Test @@ -185,11 +183,11 @@ void testSmartDefaultsWarning() { VulnerabilityFilterParams params = VulnerabilityFilterParams.of(null, null, null, null, null, null, null, null); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); - assertEquals(1, params.warnings().size()); - assertTrue(params.warnings().get(0).contains("Showing actionable vulnerabilities only")); - assertTrue(params.warnings().get(0).contains("excluding Fixed and Remediated")); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); + assertThat(params.warnings()).hasSize(1); + assertThat(params.warnings().get(0)).contains("Showing actionable vulnerabilities only"); + assertThat(params.warnings().get(0)).contains("excluding Fixed and Remediated"); } @Test @@ -198,9 +196,9 @@ void testExplicitStatusesNoSmartDefaultsWarning() { VulnerabilityFilterParams.of( null, "Reported,Confirmed", null, null, null, null, null, null); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); - assertTrue(params.warnings().isEmpty()); // No smart defaults warning + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); + assertThat(params.warnings()).isEmpty(); // No smart defaults warning } @Test @@ -208,13 +206,14 @@ void testTimeFilterWarningAdded() { VulnerabilityFilterParams params = VulnerabilityFilterParams.of(null, null, null, null, null, "2025-01-01", null, null); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); // Should have both smart defaults warning and time filter warning - assertEquals(2, params.warnings().size()); - assertTrue( - params.warnings().stream() - .anyMatch(w -> w.contains("Time filters apply to LAST ACTIVITY DATE"))); + assertThat(params.warnings()).hasSize(2); + assertThat( + params.warnings().stream() + .anyMatch(w -> w.contains("Time filters apply to LAST ACTIVITY DATE"))) + .isTrue(); } @Test @@ -222,10 +221,10 @@ void testNoTimeFilterWarningWhenDateInvalid() { VulnerabilityFilterParams params = VulnerabilityFilterParams.of(null, null, null, null, null, "invalid-date", null, null); - assertFalse(params.isValid()); - assertFalse(params.errors().isEmpty()); + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).isNotEmpty(); // Should NOT have time filter warning since date parsing failed - assertFalse(params.warnings().stream().anyMatch(w -> w.contains("Time filters apply"))); + assertThat(params.warnings()).noneMatch(w -> w.contains("Time filters apply")); } @Test @@ -234,14 +233,14 @@ void testVulnTypesPassThrough() { VulnerabilityFilterParams.of( null, null, null, "sql-injection,xss-reflected,made-up-type", null, null, null, null); - assertTrue(params.isValid()); - assertTrue(params.errors().isEmpty()); + assertThat(params.isValid()).isTrue(); + assertThat(params.errors()).isEmpty(); TraceFilterForm form = params.toTraceFilterForm(); - assertNotNull(form.getVulnTypes()); - assertEquals(3, form.getVulnTypes().size()); - assertTrue(form.getVulnTypes().contains("sql-injection")); - assertTrue(form.getVulnTypes().contains("made-up-type")); // Not validated + assertThat(form.getVulnTypes()).isNotNull(); + assertThat(form.getVulnTypes()).hasSize(3); + assertThat(form.getVulnTypes()).contains("sql-injection"); + assertThat(form.getVulnTypes()).contains("made-up-type"); // Not validated } @Test @@ -250,15 +249,15 @@ void testVulnTagsCaseSensitive() { VulnerabilityFilterParams.of( null, null, null, null, null, null, null, "SmartFix Remediated,Reviewed,reviewed"); - assertTrue(params.isValid()); + assertThat(params.isValid()).isTrue(); TraceFilterForm form = params.toTraceFilterForm(); - assertNotNull(form.getFilterTags()); - assertEquals(3, form.getFilterTags().size()); + assertThat(form.getFilterTags()).isNotNull(); + assertThat(form.getFilterTags()).hasSize(3); // SDK now handles URL encoding (AIML-193 complete) - tags passed through as-is - assertTrue(form.getFilterTags().contains("SmartFix Remediated")); - assertTrue(form.getFilterTags().contains("Reviewed")); - assertTrue(form.getFilterTags().contains("reviewed")); // Case preserved + assertThat(form.getFilterTags()).contains("SmartFix Remediated"); + assertThat(form.getFilterTags()).contains("Reviewed"); + assertThat(form.getFilterTags()).contains("reviewed"); // Case preserved } @Test @@ -268,13 +267,13 @@ void testVulnTagsWithSpacesAndSpecialChars() { VulnerabilityFilterParams.of( null, null, null, null, null, null, null, "Tag With Spaces,tag-hyphen,tag&special"); - assertTrue(params.isValid()); + assertThat(params.isValid()).isTrue(); TraceFilterForm form = params.toTraceFilterForm(); - assertNotNull(form.getFilterTags()); - assertEquals(3, form.getFilterTags().size()); - assertEquals("Tag With Spaces", form.getFilterTags().get(0)); - assertEquals("tag-hyphen", form.getFilterTags().get(1)); - assertEquals("tag&special", form.getFilterTags().get(2)); + assertThat(form.getFilterTags()).isNotNull(); + assertThat(form.getFilterTags()).hasSize(3); + assertThat(form.getFilterTags().get(0)).isEqualTo("Tag With Spaces"); + assertThat(form.getFilterTags().get(1)).isEqualTo("tag-hyphen"); + assertThat(form.getFilterTags().get(2)).isEqualTo("tag&special"); } @Test @@ -283,8 +282,8 @@ void testMultipleErrorsAccumulate() { VulnerabilityFilterParams.of( "SUPER_HIGH", "BadStatus", null, null, "STAGING", "bad-date", null, null); - assertFalse(params.isValid()); - assertEquals(4, params.errors().size()); // 4 different validation errors + assertThat(params.isValid()).isFalse(); + assertThat(params.errors()).hasSize(4); // 4 different validation errors } @Test @@ -292,8 +291,8 @@ void testAppIdPassedThrough() { VulnerabilityFilterParams params = VulnerabilityFilterParams.of(null, null, "my-app-123", null, null, null, null, null); - assertTrue(params.isValid()); - assertEquals("my-app-123", params.appId()); + assertThat(params.isValid()).isTrue(); + assertThat(params.appId()).isEqualTo("my-app-123"); } @Test @@ -301,11 +300,8 @@ void testWarningsAreImmutable() { VulnerabilityFilterParams params = VulnerabilityFilterParams.of(null, null, null, null, null, null, null, null); - assertThrows( - UnsupportedOperationException.class, - () -> { - params.warnings().add("Should fail"); - }); + assertThatThrownBy(() -> params.warnings().add("Should fail")) + .isInstanceOf(UnsupportedOperationException.class); } @Test @@ -313,10 +309,7 @@ void testErrorsAreImmutable() { VulnerabilityFilterParams params = VulnerabilityFilterParams.of("INVALID", null, null, null, null, null, null, null); - assertThrows( - UnsupportedOperationException.class, - () -> { - params.errors().add("Should fail"); - }); + assertThatThrownBy(() -> params.errors().add("Should fail")) + .isInstanceOf(UnsupportedOperationException.class); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/data/AttackSummaryTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/data/AttackSummaryTest.java index d42e0be..c978d70 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/data/AttackSummaryTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/data/AttackSummaryTest.java @@ -15,7 +15,7 @@ */ package com.contrast.labs.ai.mcp.contrast.data; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import com.contrast.labs.ai.mcp.contrast.FilterHelper; import com.contrast.labs.ai.mcp.contrast.sdkextension.data.adr.Application; @@ -41,46 +41,38 @@ void testFromAttack_FormatsTimestampsWithFilterHelper() { var summary = AttackSummary.fromAttack(attack); // Then: All timestamp strings should be in ISO 8601 format - assertNotNull(summary.startTime(), "startTime should not be null"); - assertNotNull(summary.endTime(), "endTime should not be null"); - assertNotNull(summary.firstEventTime(), "firstEventTime should not be null"); - assertNotNull(summary.lastEventTime(), "lastEventTime should not be null"); + assertThat(summary.startTime()).as("startTime should not be null").isNotNull(); + assertThat(summary.endTime()).as("endTime should not be null").isNotNull(); + assertThat(summary.firstEventTime()).as("firstEventTime should not be null").isNotNull(); + assertThat(summary.lastEventTime()).as("lastEventTime should not be null").isNotNull(); // Verify ISO 8601 format with timezone offset - assertTrue( - summary.startTime().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "startTime should match ISO 8601 format: " + summary.startTime()); - assertTrue( - summary.endTime().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "endTime should match ISO 8601 format: " + summary.endTime()); - assertTrue( - summary - .firstEventTime() - .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "firstEventTime should match ISO 8601 format: " + summary.firstEventTime()); - assertTrue( - summary - .lastEventTime() - .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "lastEventTime should match ISO 8601 format: " + summary.lastEventTime()); + assertThat(summary.startTime()) + .as("startTime should match ISO 8601 format: " + summary.startTime()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(summary.endTime()) + .as("endTime should match ISO 8601 format: " + summary.endTime()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(summary.firstEventTime()) + .as("firstEventTime should match ISO 8601 format: " + summary.firstEventTime()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(summary.lastEventTime()) + .as("lastEventTime should match ISO 8601 format: " + summary.lastEventTime()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); // Verify timestamps match expected FilterHelper output - assertEquals( - FilterHelper.formatTimestamp(attack.getStart_time()), - summary.startTime(), - "startTime should match FilterHelper.formatTimestamp output"); - assertEquals( - FilterHelper.formatTimestamp(attack.getEnd_time()), - summary.endTime(), - "endTime should match FilterHelper.formatTimestamp output"); - assertEquals( - FilterHelper.formatTimestamp(attack.getFirst_event_time()), - summary.firstEventTime(), - "firstEventTime should match FilterHelper.formatTimestamp output"); - assertEquals( - FilterHelper.formatTimestamp(attack.getLast_event_time()), - summary.lastEventTime(), - "lastEventTime should match FilterHelper.formatTimestamp output"); + assertThat(summary.startTime()) + .as("startTime should match FilterHelper.formatTimestamp output") + .isEqualTo(FilterHelper.formatTimestamp(attack.getStart_time())); + assertThat(summary.endTime()) + .as("endTime should match FilterHelper.formatTimestamp output") + .isEqualTo(FilterHelper.formatTimestamp(attack.getEnd_time())); + assertThat(summary.firstEventTime()) + .as("firstEventTime should match FilterHelper.formatTimestamp output") + .isEqualTo(FilterHelper.formatTimestamp(attack.getFirst_event_time())); + assertThat(summary.lastEventTime()) + .as("lastEventTime should match FilterHelper.formatTimestamp output") + .isEqualTo(FilterHelper.formatTimestamp(attack.getLast_event_time())); } @Test @@ -92,22 +84,18 @@ void testFromAttack_PreservesMillisecondTimestamps() { var summary = AttackSummary.fromAttack(attack); // Then: Millisecond timestamps should be preserved exactly - assertEquals( - attack.getStart_time(), - summary.startTimeMs(), - "startTimeMs should preserve original millisecond value"); - assertEquals( - attack.getEnd_time(), - summary.endTimeMs(), - "endTimeMs should preserve original millisecond value"); - assertEquals( - attack.getFirst_event_time(), - summary.firstEventTimeMs(), - "firstEventTimeMs should preserve original millisecond value"); - assertEquals( - attack.getLast_event_time(), - summary.lastEventTimeMs(), - "lastEventTimeMs should preserve original millisecond value"); + assertThat(summary.startTimeMs()) + .as("startTimeMs should preserve original millisecond value") + .isEqualTo(attack.getStart_time()); + assertThat(summary.endTimeMs()) + .as("endTimeMs should preserve original millisecond value") + .isEqualTo(attack.getEnd_time()); + assertThat(summary.firstEventTimeMs()) + .as("firstEventTimeMs should preserve original millisecond value") + .isEqualTo(attack.getFirst_event_time()); + assertThat(summary.lastEventTimeMs()) + .as("lastEventTimeMs should preserve original millisecond value") + .isEqualTo(attack.getLast_event_time()); } @Test @@ -119,26 +107,24 @@ void testApplicationAttackInfo_FormatsTimestampsWithFilterHelper() { var appInfo = AttackSummary.ApplicationAttackInfo.fromAttackApplication(attackApp); // Then: Timestamp strings should be in ISO 8601 format - assertNotNull(appInfo.startTime(), "startTime should not be null"); - assertNotNull(appInfo.endTime(), "endTime should not be null"); + assertThat(appInfo.startTime()).as("startTime should not be null").isNotNull(); + assertThat(appInfo.endTime()).as("endTime should not be null").isNotNull(); // Verify ISO 8601 format with timezone offset - assertTrue( - appInfo.startTime().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "startTime should match ISO 8601 format: " + appInfo.startTime()); - assertTrue( - appInfo.endTime().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "endTime should match ISO 8601 format: " + appInfo.endTime()); + assertThat(appInfo.startTime()) + .as("startTime should match ISO 8601 format: " + appInfo.startTime()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(appInfo.endTime()) + .as("endTime should match ISO 8601 format: " + appInfo.endTime()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); // Verify timestamps match expected FilterHelper output - assertEquals( - FilterHelper.formatTimestamp(attackApp.getStartTime()), - appInfo.startTime(), - "startTime should match FilterHelper.formatTimestamp output"); - assertEquals( - FilterHelper.formatTimestamp(attackApp.getEndTime()), - appInfo.endTime(), - "endTime should match FilterHelper.formatTimestamp output"); + assertThat(appInfo.startTime()) + .as("startTime should match FilterHelper.formatTimestamp output") + .isEqualTo(FilterHelper.formatTimestamp(attackApp.getStartTime())); + assertThat(appInfo.endTime()) + .as("endTime should match FilterHelper.formatTimestamp output") + .isEqualTo(FilterHelper.formatTimestamp(attackApp.getEndTime())); } @Test @@ -150,14 +136,12 @@ void testApplicationAttackInfo_PreservesMillisecondTimestamps() { var appInfo = AttackSummary.ApplicationAttackInfo.fromAttackApplication(attackApp); // Then: Millisecond timestamps should be preserved exactly - assertEquals( - attackApp.getStartTime(), - appInfo.startTimeMs(), - "startTimeMs should preserve original millisecond value"); - assertEquals( - attackApp.getEndTime(), - appInfo.endTimeMs(), - "endTimeMs should preserve original millisecond value"); + assertThat(appInfo.startTimeMs()) + .as("startTimeMs should preserve original millisecond value") + .isEqualTo(attackApp.getStartTime()); + assertThat(appInfo.endTimeMs()) + .as("endTimeMs should preserve original millisecond value") + .isEqualTo(attackApp.getEndTime()); } @Test @@ -170,16 +154,22 @@ void testFromAttack_WithApplications_FormatsAllTimestamps() { var summary = AttackSummary.fromAttack(attack); // Then: Attack timestamps should be formatted - assertTrue( - summary.startTime().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "Attack startTime should match ISO 8601 format"); + assertThat( + summary + .startTime() + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}")) + .as("Attack startTime should match ISO 8601 format") + .isTrue(); // And: Application timestamps should also be formatted - assertFalse(summary.applications().isEmpty(), "Should have application info"); + assertThat(summary.applications().isEmpty()).as("Should have application info").isFalse(); var appInfo = summary.applications().get(0); - assertTrue( - appInfo.startTime().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "Application startTime should match ISO 8601 format"); + assertThat( + appInfo + .startTime() + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}")) + .as("Application startTime should match ISO 8601 format") + .isTrue(); } @Test @@ -195,14 +185,17 @@ void testTimestampFormat_ConsistentWithOtherMCPTools() { var expectedFormat = FilterHelper.formatTimestamp(TEST_TIMESTAMP); // Verify the format matches the pattern used throughout the codebase - assertTrue( - summary.startTime().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"), - "Timestamp format should be consistent with other MCP tools"); + assertThat( + summary + .startTime() + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}")) + .as("Timestamp format should be consistent with other MCP tools") + .isTrue(); // Should NOT match old Date.toString() format (e.g., "Thu Jan 15 10:30:00 EST 2025") - assertFalse( - summary.startTime().matches("\\w{3} \\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2} \\w+ \\d{4}"), - "Should NOT use legacy Date.toString() format"); + assertThat(summary.startTime().matches("\\w{3} \\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2} \\w+ \\d{4}")) + .as("Should NOT use legacy Date.toString() format") + .isFalse(); } // ========== Helper Methods ========== diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/hints/HintProviderTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/hints/HintProviderTest.java index a5044b1..da06ac6 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/hints/HintProviderTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/hints/HintProviderTest.java @@ -1,6 +1,6 @@ package com.contrast.labs.ai.mcp.contrast.hints; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; @@ -9,23 +9,22 @@ public class HintProviderTest { @Test public void testGetHintsForRule() { // Test getting hints for SQL injection - assertFalse(HintProvider.getHintsForRule("sql-injection").isEmpty()); + assertThat(HintProvider.getHintsForRule("sql-injection")).isNotEmpty(); // Test getting hints for a non-existent rule - assertTrue(HintProvider.getHintsForRule("non-existent-rule").isEmpty()); + assertThat(HintProvider.getHintsForRule("non-existent-rule")).isEmpty(); } @Test public void testGetGeneralGuidance() { // General guidance should not be empty - assertFalse(HintProvider.getGeneralGuidance().isEmpty()); + assertThat(HintProvider.getGeneralGuidance()).isNotEmpty(); } @Test public void testGetAllHintsForRule() { // For SQL injection, we should get both general and specific hints - assertTrue( - HintProvider.getAllHintsForRule("sql-injection").size() - > HintProvider.getHintsForRule("sql-injection").size()); + assertThat(HintProvider.getAllHintsForRule("sql-injection").size()) + .isGreaterThan(HintProvider.getHintsForRule("sql-injection").size()); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapperTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapperTest.java index 6fed547..34f92d3 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapperTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapperTest.java @@ -15,7 +15,7 @@ */ package com.contrast.labs.ai.mcp.contrast.mapper; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -62,8 +62,8 @@ void setUp() { @Test void toVulnLight_BasicTrace_TransformsCorrectly() { // Arrange - var trace = mock(Trace.class); - var app = mock(Application.class); + Trace trace = mock(); + Application app = mock(); when(app.getId()).thenReturn("app-123"); when(app.getName()).thenReturn("Test Application"); @@ -84,25 +84,25 @@ void toVulnLight_BasicTrace_TransformsCorrectly() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertEquals("SQL Injection", result.title()); - assertEquals("sql-injection", result.type()); - assertEquals("vuln-123", result.vulnID()); - assertEquals("CRITICAL", result.severity()); - assertEquals("app-123", result.appID()); - assertEquals("Test Application", result.appName()); - assertEquals("Reported", result.status()); - assertNotNull(result.lastSeenAt()); - assertNotNull(result.firstSeenAt()); - assertNull(result.closedAt()); - assertTrue(result.sessionMetadata().isEmpty()); - assertTrue(result.environments().isEmpty()); + assertThat(result).isNotNull(); + assertThat(result.title()).isEqualTo("SQL Injection"); + assertThat(result.type()).isEqualTo("sql-injection"); + assertThat(result.vulnID()).isEqualTo("vuln-123"); + assertThat(result.severity()).isEqualTo("CRITICAL"); + assertThat(result.appID()).isEqualTo("app-123"); + assertThat(result.appName()).isEqualTo("Test Application"); + assertThat(result.status()).isEqualTo("Reported"); + assertThat(result.lastSeenAt()).isNotNull(); + assertThat(result.firstSeenAt()).isNotNull(); + assertThat(result.closedAt()).isNull(); + assertThat(result.sessionMetadata()).isEmpty(); + assertThat(result.environments()).isEmpty(); } @Test void toVulnLight_TraceWithEnvironments_ExtractsEnvironments() { // Arrange - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test"); when(trace.getRule()).thenReturn("test-rule"); @@ -120,19 +120,19 @@ void toVulnLight_TraceWithEnvironments_ExtractsEnvironments() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertEquals(2, result.environments().size()); - assertTrue(result.environments().contains("PRODUCTION")); - assertTrue(result.environments().contains("QA")); + assertThat(result).isNotNull(); + assertThat(result.environments()).hasSize(2); + assertThat(result.environments()).contains("PRODUCTION"); + assertThat(result.environments()).contains("QA"); // Verify they're sorted - assertEquals("PRODUCTION", result.environments().get(0)); - assertEquals("QA", result.environments().get(1)); + assertThat(result.environments().get(0)).isEqualTo("PRODUCTION"); + assertThat(result.environments().get(1)).isEqualTo("QA"); } @Test void toVulnLight_NullTimestamps_HandledGracefully() { // Arrange - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test"); when(trace.getRule()).thenReturn("test-rule"); when(trace.getUuid()).thenReturn("vuln-null"); @@ -148,16 +148,16 @@ void toVulnLight_NullTimestamps_HandledGracefully() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertNotNull(result.lastSeenAt()); - assertNull(result.firstSeenAt()); - assertNull(result.closedAt()); + assertThat(result).isNotNull(); + assertThat(result.lastSeenAt()).isNotNull(); + assertThat(result.firstSeenAt()).isNull(); + assertThat(result.closedAt()).isNull(); } @Test void toFullVulnerability_WithContext_TransformsCorrectly() { // Arrange - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getUuid()).thenReturn("vuln-full"); when(trace.getTitle()).thenReturn("Command Injection"); when(trace.getRule()).thenReturn("cmd-injection"); @@ -167,7 +167,7 @@ void toFullVulnerability_WithContext_TransformsCorrectly() { when(trace.getClosedTime()).thenReturn(null); var stackLib = new StackLib("at com.example.Test.method()", "lib-hash-123"); - var library = mock(LibraryExtended.class); + LibraryExtended library = mock(); var context = VulnerabilityContext.builder() @@ -181,25 +181,25 @@ void toFullVulnerability_WithContext_TransformsCorrectly() { var result = mapper.toFullVulnerability(trace, context); // Assert - assertNotNull(result); - assertEquals("vuln-full", result.vulnID()); - assertEquals("Command Injection", result.title()); - assertEquals("cmd-injection", result.type()); - assertEquals("Confirmed", result.status()); - assertEquals("Sanitize user input", result.howToFix()); - assertEquals(1, result.stackTrace().size()); - assertEquals(stackLib, result.stackTrace().get(0)); - assertEquals(1, result.vulnerableLibraries().size()); - assertEquals(library, result.vulnerableLibraries().get(0)); - assertEquals("GET /api/test", result.httpRequest()); - assertNotNull(result.hint()); - assertFalse(result.hint().isEmpty()); // HintGenerator provides guidance + assertThat(result).isNotNull(); + assertThat(result.vulnID()).isEqualTo("vuln-full"); + assertThat(result.title()).isEqualTo("Command Injection"); + assertThat(result.type()).isEqualTo("cmd-injection"); + assertThat(result.status()).isEqualTo("Confirmed"); + assertThat(result.howToFix()).isEqualTo("Sanitize user input"); + assertThat(result.stackTrace()).hasSize(1); + assertThat(result.stackTrace().get(0)).isEqualTo(stackLib); + assertThat(result.vulnerableLibraries()).hasSize(1); + assertThat(result.vulnerableLibraries().get(0)).isEqualTo(library); + assertThat(result.httpRequest()).isEqualTo("GET /api/test"); + assertThat(result.hint()).isNotNull(); + assertThat(result.hint()).isNotEmpty(); // HintGenerator provides guidance } @Test void toVulnLight_EnvironmentsWithNullAndEmpty_FiltersCorrectly() { // Arrange - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test"); when(trace.getRule()).thenReturn("test-rule"); @@ -217,18 +217,18 @@ void toVulnLight_EnvironmentsWithNullAndEmpty_FiltersCorrectly() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertEquals(2, result.environments().size()); - assertTrue(result.environments().contains("PRODUCTION")); - assertTrue(result.environments().contains("DEVELOPMENT")); - assertFalse(result.environments().contains(null)); - assertFalse(result.environments().contains("")); + assertThat(result).isNotNull(); + assertThat(result.environments()).hasSize(2); + assertThat(result.environments()).contains("PRODUCTION"); + assertThat(result.environments()).contains("DEVELOPMENT"); + assertThat(result.environments()).doesNotContain((String) null); + assertThat(result.environments()).doesNotContain(""); } @Test void toVulnLight_ISO8601TimestampFormat_Validated() { // Arrange - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test"); when(trace.getRule()).thenReturn("test-rule"); when(trace.getUuid()).thenReturn("vuln-timestamp"); @@ -244,23 +244,23 @@ void toVulnLight_ISO8601TimestampFormat_Validated() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); + assertThat(result).isNotNull(); // Verify ISO 8601 format with timezone offset - assertTrue( - result.lastSeenAt().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}")); - assertTrue( - result.firstSeenAt().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}")); - assertTrue( - result.closedAt().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}")); - assertTrue(result.lastSeenAt().contains("T")); // Has time separator - assertTrue(result.lastSeenAt().contains(":")); // Has time colons - assertTrue(result.lastSeenAt().matches(".*[+-]\\d{2}:\\d{2}$")); // Has timezone offset + assertThat(result.lastSeenAt()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(result.firstSeenAt()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(result.closedAt()) + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}"); + assertThat(result.lastSeenAt()).contains("T"); // Has time separator + assertThat(result.lastSeenAt()).contains(":"); // Has time colons + assertThat(result.lastSeenAt()).matches(".*[+-]\\d{2}:\\d{2}$"); // Has timezone offset } @Test void toVulnLight_TraceWithTags_ExtractsTags() { // Arrange - var trace = mock(Trace.class); + Trace trace = mock(); var tags = Arrays.asList("SmartFix Remediated", "reviewed", "high-priority"); when(trace.getTitle()).thenReturn("Test"); @@ -278,18 +278,18 @@ void toVulnLight_TraceWithTags_ExtractsTags() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertNotNull(result.tags()); - assertEquals(3, result.tags().size()); - assertTrue(result.tags().contains("SmartFix Remediated")); - assertTrue(result.tags().contains("reviewed")); - assertTrue(result.tags().contains("high-priority")); + assertThat(result).isNotNull(); + assertThat(result.tags()).isNotNull(); + assertThat(result.tags()).hasSize(3); + assertThat(result.tags()).contains("SmartFix Remediated"); + assertThat(result.tags()).contains("reviewed"); + assertThat(result.tags()).contains("high-priority"); } @Test void toVulnLight_TraceWithNullTags_ReturnsEmptyList() { // Arrange - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test"); when(trace.getRule()).thenReturn("test-rule"); @@ -306,15 +306,15 @@ void toVulnLight_TraceWithNullTags_ReturnsEmptyList() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertNotNull(result.tags()); - assertTrue(result.tags().isEmpty()); + assertThat(result).isNotNull(); + assertThat(result.tags()).isNotNull(); + assertThat(result.tags()).isEmpty(); } @Test void toVulnLight_TraceWithEmptyTags_ReturnsEmptyList() { // Arrange - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Test"); when(trace.getRule()).thenReturn("test-rule"); @@ -331,16 +331,16 @@ void toVulnLight_TraceWithEmptyTags_ReturnsEmptyList() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertNotNull(result.tags()); - assertTrue(result.tags().isEmpty()); + assertThat(result).isNotNull(); + assertThat(result.tags()).isNotNull(); + assertThat(result.tags()).isEmpty(); } @Test void toVulnLight_TraceWithSessionMetadata_ExtractsSessionMetadata() { // Arrange - Test that SDK Trace now includes session metadata when expanded - var trace = mock(Trace.class); - var sessionMeta = mock(SessionMetadata.class); + Trace trace = mock(); + SessionMetadata sessionMeta = mock(); var sessionMetadataList = Arrays.asList(sessionMeta); when(trace.getTitle()).thenReturn("SQL Injection"); @@ -359,16 +359,16 @@ void toVulnLight_TraceWithSessionMetadata_ExtractsSessionMetadata() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertEquals("sql-injection", result.type()); - assertEquals(1, result.sessionMetadata().size()); - assertEquals(sessionMeta, result.sessionMetadata().get(0)); + assertThat(result).isNotNull(); + assertThat(result.type()).isEqualTo("sql-injection"); + assertThat(result.sessionMetadata()).hasSize(1); + assertThat(result.sessionMetadata().get(0)).isEqualTo(sessionMeta); } @Test void toVulnLight_TraceWithNullSessionMetadata_ReturnsEmptyList() { // Arrange - Test that SDK Trace handles null session metadata gracefully - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("XSS"); when(trace.getRule()).thenReturn("xss-reflected"); @@ -386,15 +386,15 @@ void toVulnLight_TraceWithNullSessionMetadata_ReturnsEmptyList() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertNotNull(result.sessionMetadata()); - assertTrue(result.sessionMetadata().isEmpty()); + assertThat(result).isNotNull(); + assertThat(result.sessionMetadata()).isNotNull(); + assertThat(result.sessionMetadata()).isEmpty(); } @Test void toVulnLight_TraceWithEmptySessionMetadata_ReturnsEmptyList() { // Arrange - Test that SDK Trace handles empty session metadata list - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getTitle()).thenReturn("Path Traversal"); when(trace.getRule()).thenReturn("path-traversal"); @@ -412,15 +412,15 @@ void toVulnLight_TraceWithEmptySessionMetadata_ReturnsEmptyList() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertNotNull(result.sessionMetadata()); - assertTrue(result.sessionMetadata().isEmpty()); + assertThat(result).isNotNull(); + assertThat(result.sessionMetadata()).isNotNull(); + assertThat(result.sessionMetadata()).isEmpty(); } @Test void toVulnLight_TraceWithNullApplication_HandlesGracefully() { // Arrange - Test that mapper handles null application without crashing - var trace = mock(Trace.class); + Trace trace = mock(); when(trace.getApplication()).thenReturn(null); // No application expanded when(trace.getTitle()).thenReturn("Test Vuln"); @@ -439,18 +439,20 @@ void toVulnLight_TraceWithNullApplication_HandlesGracefully() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertNull(result.appID(), "appID should be null when application is not expanded"); - assertNull(result.appName(), "appName should be null when application is not expanded"); - assertEquals("vuln-no-app", result.vulnID()); - assertEquals("HIGH", result.severity()); + assertThat(result).isNotNull(); + assertThat(result.appID()).as("appID should be null when application is not expanded").isNull(); + assertThat(result.appName()) + .as("appName should be null when application is not expanded") + .isNull(); + assertThat(result.vulnID()).isEqualTo("vuln-no-app"); + assertThat(result.severity()).isEqualTo("HIGH"); } @Test void toVulnLight_TraceWithApplication_ExtractsAppData() { // Arrange - Test that application data is correctly extracted when expanded - var trace = mock(Trace.class); - var app = mock(Application.class); + Trace trace = mock(); + Application app = mock(); when(app.getId()).thenReturn("app-abc-123"); when(app.getName()).thenReturn("My Production App"); @@ -471,14 +473,16 @@ void toVulnLight_TraceWithApplication_ExtractsAppData() { var result = mapper.toVulnLight(trace); // Assert - assertNotNull(result); - assertEquals("app-abc-123", result.appID(), "appID should match application ID"); - assertEquals("My Production App", result.appName(), "appName should match application name"); - assertEquals("vuln-with-app", result.vulnID()); - assertEquals("xss-reflected", result.type()); - assertEquals("CRITICAL", result.severity()); - assertEquals(1, result.environments().size()); - assertEquals("PRODUCTION", result.environments().get(0)); - assertEquals(1, result.tags().size()); + assertThat(result).isNotNull(); + assertThat(result.appID()).as("appID should match application ID").isEqualTo("app-abc-123"); + assertThat(result.appName()) + .as("appName should match application name") + .isEqualTo("My Production App"); + assertThat(result.vulnID()).isEqualTo("vuln-with-app"); + assertThat(result.type()).isEqualTo("xss-reflected"); + assertThat(result.severity()).isEqualTo("CRITICAL"); + assertThat(result.environments()).hasSize(1); + assertThat(result.environments().get(0)).isEqualTo("PRODUCTION"); + assertThat(result.tags()).hasSize(1); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkextension/SDKHelperTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkextension/SDKHelperTest.java index 5280668..b3969a0 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkextension/SDKHelperTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkextension/SDKHelperTest.java @@ -1,6 +1,7 @@ package com.contrast.labs.ai.mcp.contrast.sdkextension; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; import org.junit.jupiter.api.BeforeEach; @@ -28,25 +29,25 @@ void setUp() throws Exception { @Test void testGetProtocolAndServer_WithNull() { - assertNull(SDKHelper.getProtocolAndServer(null)); + assertThat(SDKHelper.getProtocolAndServer(null)).isNull(); } @Test void testGetProtocolAndServer_WithHttpProtocol() { var result = SDKHelper.getProtocolAndServer("http://example.com"); - assertEquals("http://example.com", result); + assertThat(result).isEqualTo("http://example.com"); } @Test void testGetProtocolAndServer_WithHttpsProtocol() { var result = SDKHelper.getProtocolAndServer("https://example.com"); - assertEquals("https://example.com", result); + assertThat(result).isEqualTo("https://example.com"); } @Test void testGetProtocolAndServer_WithoutProtocol() { var result = SDKHelper.getProtocolAndServer("example.com"); - assertEquals("https://example.com", result); + assertThat(result).isEqualTo("https://example.com"); } @Test @@ -54,77 +55,62 @@ void testGetProtocolAndServer_WithCustomProtocol() { when(environment.getProperty("contrast.api.protocol", "https")).thenReturn("http"); var result = SDKHelper.getProtocolAndServer("example.com"); - assertEquals("http://example.com", result); + assertThat(result).isEqualTo("http://example.com"); } @Test void testGetProtocolAndServer_WithEmptyString() { // Empty string should return null (consistent with null input handling) var result = SDKHelper.getProtocolAndServer(""); - assertNull(result); + assertThat(result).isNull(); } @Test void testGetProtocolAndServer_WithWhitespaceOnly() { // Whitespace-only string should return null (consistent with null input handling) var result = SDKHelper.getProtocolAndServer(" "); - assertNull(result); + assertThat(result).isNull(); } @Test void testGetProtocolAndServer_WithLeadingWhitespace() { var result = SDKHelper.getProtocolAndServer(" example.com"); - assertEquals("https://example.com", result); + assertThat(result).isEqualTo("https://example.com"); } @Test void testGetProtocolAndServer_WithTrailingWhitespace() { var result = SDKHelper.getProtocolAndServer("example.com "); - assertEquals("https://example.com", result); + assertThat(result).isEqualTo("https://example.com"); } @Test void testGetProtocolAndServer_WithLeadingAndTrailingWhitespace() { var result = SDKHelper.getProtocolAndServer(" https://example.com "); - assertEquals("https://example.com", result); + assertThat(result).isEqualTo("https://example.com"); } @Test void testGetProtocolAndServer_WithInvalidProtocol_Ftp() { - var exception = - assertThrows( - IllegalArgumentException.class, - () -> { - SDKHelper.getProtocolAndServer("ftp://example.com"); - }); - - assertTrue(exception.getMessage().contains("Invalid protocol")); - assertTrue(exception.getMessage().contains("ftp://example.com")); + assertThatThrownBy(() -> SDKHelper.getProtocolAndServer("ftp://example.com")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid protocol") + .hasMessageContaining("ftp://example.com"); } @Test void testGetProtocolAndServer_WithInvalidProtocol_Custom() { - var exception = - assertThrows( - IllegalArgumentException.class, - () -> { - SDKHelper.getProtocolAndServer("custom://example.com"); - }); - - assertTrue(exception.getMessage().contains("Invalid protocol")); + assertThatThrownBy(() -> SDKHelper.getProtocolAndServer("custom://example.com")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid protocol"); } @Test void testGetProtocolAndServer_WithMalformedProtocol() { // "ht://example.com" contains "://" but doesn't start with http:// or https:// - var exception = - assertThrows( - IllegalArgumentException.class, - () -> { - SDKHelper.getProtocolAndServer("ht://example.com"); - }); - - assertTrue(exception.getMessage().contains("Invalid protocol")); + assertThatThrownBy(() -> SDKHelper.getProtocolAndServer("ht://example.com")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid protocol"); } @Test @@ -133,16 +119,12 @@ void testGetSDK_WithHttpsUrl() { when(environment.getProperty("spring.ai.mcp.server.version", "unknown")).thenReturn("1.0.0"); // getSDK is a public static method, so we can call it directly - try { - var sdk = SDKHelper.getSDK(hostWithProtocol, "apiKey", "serviceKey", "username", null, null); + var sdk = SDKHelper.getSDK(hostWithProtocol, "apiKey", "serviceKey", "username", null, null); - assertNotNull(sdk); - // The SDK was successfully created with the https URL. - // Detailed URL validation would require accessing ContrastSDK's internal state, - // which is beyond the scope of a unit test and better suited for integration tests. - } catch (Exception e) { - fail("Exception occurred: " + e.getMessage()); - } + assertThat(sdk).isNotNull(); + // The SDK was successfully created with the https URL. + // Detailed URL validation would require accessing ContrastSDK's internal state, + // which is beyond the scope of a unit test and better suited for integration tests. } @Test @@ -150,38 +132,34 @@ void testGetSDK_WithHostnameOnly() { var hostname = "example.contrastsecurity.com"; when(environment.getProperty("spring.ai.mcp.server.version", "unknown")).thenReturn("1.0.0"); - try { - var sdk = SDKHelper.getSDK(hostname, "apiKey", "serviceKey", "username", null, null); + var sdk = SDKHelper.getSDK(hostname, "apiKey", "serviceKey", "username", null, null); - assertNotNull(sdk); - // The SDK should prepend https:// by default - } catch (Exception e) { - fail("Exception occurred: " + e.getMessage()); - } + assertThat(sdk).isNotNull(); + // The SDK should prepend https:// by default } @Test void testGetProtocolAndServer_WithTrailingSlash() { var result = SDKHelper.getProtocolAndServer("example.com/"); - assertEquals("https://example.com", result); + assertThat(result).isEqualTo("https://example.com"); } @Test void testGetProtocolAndServer_WithProtocolAndTrailingSlash() { var result = SDKHelper.getProtocolAndServer("https://example.com/"); - assertEquals("https://example.com", result); + assertThat(result).isEqualTo("https://example.com"); } @Test void testGetProtocolAndServer_WithHttpProtocolAndTrailingSlash() { var result = SDKHelper.getProtocolAndServer("http://example.com/"); - assertEquals("http://example.com", result); + assertThat(result).isEqualTo("http://example.com"); } @Test void testGetProtocolAndServer_WithMultipleTrailingSlashes() { // Note: Only one trailing slash is removed var result = SDKHelper.getProtocolAndServer("example.com//"); - assertEquals("https://example.com/", result); + assertThat(result).isEqualTo("https://example.com/"); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkextension/data/adr/AttacksFilterBodyTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkextension/data/adr/AttacksFilterBodyTest.java index 6593648..27a9e95 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkextension/data/adr/AttacksFilterBodyTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkextension/data/adr/AttacksFilterBodyTest.java @@ -15,7 +15,8 @@ */ package com.contrast.labs.ai.mcp.contrast.sdkextension.data.adr; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.ArrayList; import java.util.Arrays; @@ -32,29 +33,20 @@ void testBuilder_DefaultValues_AreCorrect() { var filterBody = AttacksFilterBody.builder().build(); // Then - assertEquals("ALL", filterBody.getQuickFilter()); - assertEquals("", filterBody.getKeyword()); - assertFalse(filterBody.isIncludeSuppressed()); - assertFalse(filterBody.isIncludeBotBlockers()); - assertFalse(filterBody.isIncludeIpBlacklist()); - assertNotNull(filterBody.getTags()); - assertTrue(filterBody.getTags().isEmpty()); - assertNotNull(filterBody.getStatusFilter()); - assertTrue(filterBody.getStatusFilter().isEmpty()); - assertNotNull(filterBody.getProtectionRules()); - assertTrue(filterBody.getProtectionRules().isEmpty()); - assertNotNull(filterBody.getApplications()); - assertTrue(filterBody.getApplications().isEmpty()); - assertNotNull(filterBody.getApplicationImportances()); - assertTrue(filterBody.getApplicationImportances().isEmpty()); - assertNotNull(filterBody.getAttackers()); - assertTrue(filterBody.getAttackers().isEmpty()); - assertNotNull(filterBody.getServers()); - assertTrue(filterBody.getServers().isEmpty()); - assertNotNull(filterBody.getServerEnvironments()); - assertTrue(filterBody.getServerEnvironments().isEmpty()); - assertNotNull(filterBody.getSeverities()); - assertTrue(filterBody.getSeverities().isEmpty()); + assertThat(filterBody.getQuickFilter()).isEqualTo("ALL"); + assertThat(filterBody.getKeyword()).isEqualTo(""); + assertThat(filterBody.isIncludeSuppressed()).isFalse(); + assertThat(filterBody.isIncludeBotBlockers()).isFalse(); + assertThat(filterBody.isIncludeIpBlacklist()).isFalse(); + assertThat(filterBody.getTags()).isNotNull().isEmpty(); + assertThat(filterBody.getStatusFilter()).isNotNull().isEmpty(); + assertThat(filterBody.getProtectionRules()).isNotNull().isEmpty(); + assertThat(filterBody.getApplications()).isNotNull().isEmpty(); + assertThat(filterBody.getApplicationImportances()).isNotNull().isEmpty(); + assertThat(filterBody.getAttackers()).isNotNull().isEmpty(); + assertThat(filterBody.getServers()).isNotNull().isEmpty(); + assertThat(filterBody.getServerEnvironments()).isNotNull().isEmpty(); + assertThat(filterBody.getSeverities()).isNotNull().isEmpty(); } // ========== Test: String Fields ========== @@ -65,7 +57,7 @@ void testBuilder_QuickFilter_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().quickFilter("PROBED").build(); // Then - assertEquals("PROBED", filterBody.getQuickFilter()); + assertThat(filterBody.getQuickFilter()).isEqualTo("PROBED"); } @Test @@ -74,7 +66,7 @@ void testBuilder_Keyword_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().keyword("sql injection").build(); // Then - assertEquals("sql injection", filterBody.getKeyword()); + assertThat(filterBody.getKeyword()).isEqualTo("sql injection"); } // ========== Test: Boolean Fields ========== @@ -85,7 +77,7 @@ void testBuilder_IncludeSuppressed_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().includeSuppressed(true).build(); // Then - assertTrue(filterBody.isIncludeSuppressed()); + assertThat(filterBody.isIncludeSuppressed()).isTrue(); } @Test @@ -94,7 +86,7 @@ void testBuilder_IncludeBotBlockers_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().includeBotBlockers(true).build(); // Then - assertTrue(filterBody.isIncludeBotBlockers()); + assertThat(filterBody.isIncludeBotBlockers()).isTrue(); } @Test @@ -103,7 +95,7 @@ void testBuilder_IncludeIpBlacklist_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().includeIpBlacklist(true).build(); // Then - assertTrue(filterBody.isIncludeIpBlacklist()); + assertThat(filterBody.isIncludeIpBlacklist()).isTrue(); } // ========== Test: List Fields ========== @@ -117,9 +109,9 @@ void testBuilder_Tags_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().tags(tags).build(); // Then - assertEquals(2, filterBody.getTags().size()); - assertEquals("tag1", filterBody.getTags().get(0)); - assertEquals("tag2", filterBody.getTags().get(1)); + assertThat(filterBody.getTags()).hasSize(2); + assertThat(filterBody.getTags().get(0)).isEqualTo("tag1"); + assertThat(filterBody.getTags().get(1)).isEqualTo("tag2"); } @Test @@ -131,8 +123,8 @@ void testBuilder_StatusFilter_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().statusFilter(statuses).build(); // Then - assertEquals(2, filterBody.getStatusFilter().size()); - assertEquals("status1", filterBody.getStatusFilter().get(0)); + assertThat(filterBody.getStatusFilter()).hasSize(2); + assertThat(filterBody.getStatusFilter().get(0)).isEqualTo("status1"); } @Test @@ -144,8 +136,8 @@ void testBuilder_ProtectionRules_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().protectionRules(rules).build(); // Then - assertEquals(2, filterBody.getProtectionRules().size()); - assertEquals("rule1", filterBody.getProtectionRules().get(0)); + assertThat(filterBody.getProtectionRules()).hasSize(2); + assertThat(filterBody.getProtectionRules().get(0)).isEqualTo("rule1"); } @Test @@ -157,8 +149,8 @@ void testBuilder_Applications_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().applications(apps).build(); // Then - assertEquals(2, filterBody.getApplications().size()); - assertEquals("app1", filterBody.getApplications().get(0)); + assertThat(filterBody.getApplications()).hasSize(2); + assertThat(filterBody.getApplications().get(0)).isEqualTo("app1"); } @Test @@ -170,8 +162,8 @@ void testBuilder_ApplicationImportances_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().applicationImportances(importances).build(); // Then - assertEquals(2, filterBody.getApplicationImportances().size()); - assertEquals("CRITICAL", filterBody.getApplicationImportances().get(0)); + assertThat(filterBody.getApplicationImportances()).hasSize(2); + assertThat(filterBody.getApplicationImportances().get(0)).isEqualTo("CRITICAL"); } @Test @@ -183,8 +175,8 @@ void testBuilder_Attackers_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().attackers(attackers).build(); // Then - assertEquals(2, filterBody.getAttackers().size()); - assertEquals("10.0.0.1", filterBody.getAttackers().get(0)); + assertThat(filterBody.getAttackers()).hasSize(2); + assertThat(filterBody.getAttackers().get(0)).isEqualTo("10.0.0.1"); } @Test @@ -196,8 +188,8 @@ void testBuilder_Servers_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().servers(servers).build(); // Then - assertEquals(2, filterBody.getServers().size()); - assertEquals("server1", filterBody.getServers().get(0)); + assertThat(filterBody.getServers()).hasSize(2); + assertThat(filterBody.getServers().get(0)).isEqualTo("server1"); } @Test @@ -209,8 +201,8 @@ void testBuilder_ServerEnvironments_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().serverEnvironments(envs).build(); // Then - assertEquals(2, filterBody.getServerEnvironments().size()); - assertEquals("PRODUCTION", filterBody.getServerEnvironments().get(0)); + assertThat(filterBody.getServerEnvironments()).hasSize(2); + assertThat(filterBody.getServerEnvironments().get(0)).isEqualTo("PRODUCTION"); } @Test @@ -222,8 +214,8 @@ void testBuilder_Severities_SetsCorrectly() { var filterBody = AttacksFilterBody.builder().severities(severities).build(); // Then - assertEquals(2, filterBody.getSeverities().size()); - assertEquals("HIGH", filterBody.getSeverities().get(0)); + assertThat(filterBody.getSeverities()).hasSize(2); + assertThat(filterBody.getSeverities().get(0)).isEqualTo("HIGH"); } // ========== Test: Immutability ========== @@ -235,11 +227,8 @@ void testBuilder_ReturnedLists_AreImmutable() { var filterBody = AttacksFilterBody.builder().tags(tags).build(); // When/Then - Should throw UnsupportedOperationException - assertThrows( - UnsupportedOperationException.class, - () -> { - filterBody.getTags().add("tag3"); - }); + assertThatThrownBy(() -> filterBody.getTags().add("tag3")) + .isInstanceOf(UnsupportedOperationException.class); } @Test @@ -256,8 +245,8 @@ void testBuilder_ModifyingSourceList_DoesNotAffectBuiltObject() { tags.add("tag3"); // Then - Built object should not be affected - assertEquals(2, filterBody.getTags().size()); - assertFalse(filterBody.getTags().contains("tag3")); + assertThat(filterBody.getTags()).hasSize(2); + assertThat(filterBody.getTags()).doesNotContain("tag3"); } // ========== Test: Fluent API ========== @@ -277,13 +266,13 @@ void testBuilder_FluentAPI_ChainsCorrectly() { .build(); // Then - assertEquals("EXPLOITED", filterBody.getQuickFilter()); - assertEquals("xss", filterBody.getKeyword()); - assertTrue(filterBody.isIncludeSuppressed()); - assertFalse(filterBody.isIncludeBotBlockers()); - assertTrue(filterBody.isIncludeIpBlacklist()); - assertEquals(1, filterBody.getTags().size()); - assertEquals(1, filterBody.getStatusFilter().size()); + assertThat(filterBody.getQuickFilter()).isEqualTo("EXPLOITED"); + assertThat(filterBody.getKeyword()).isEqualTo("xss"); + assertThat(filterBody.isIncludeSuppressed()).isTrue(); + assertThat(filterBody.isIncludeBotBlockers()).isFalse(); + assertThat(filterBody.isIncludeIpBlacklist()).isTrue(); + assertThat(filterBody.getTags()).hasSize(1); + assertThat(filterBody.getStatusFilter()).hasSize(1); } // ========== Test: Complex Scenarios ========== @@ -310,20 +299,20 @@ void testBuilder_AllFieldsSet_BuildsCorrectly() { .build(); // Then - assertEquals("PROBED", filterBody.getQuickFilter()); - assertEquals("sql", filterBody.getKeyword()); - assertTrue(filterBody.isIncludeSuppressed()); - assertTrue(filterBody.isIncludeBotBlockers()); - assertTrue(filterBody.isIncludeIpBlacklist()); - assertEquals(2, filterBody.getTags().size()); - assertEquals(1, filterBody.getStatusFilter().size()); - assertEquals(1, filterBody.getProtectionRules().size()); - assertEquals(1, filterBody.getApplications().size()); - assertEquals(1, filterBody.getApplicationImportances().size()); - assertEquals(1, filterBody.getAttackers().size()); - assertEquals(1, filterBody.getServers().size()); - assertEquals(1, filterBody.getServerEnvironments().size()); - assertEquals(1, filterBody.getSeverities().size()); + assertThat(filterBody.getQuickFilter()).isEqualTo("PROBED"); + assertThat(filterBody.getKeyword()).isEqualTo("sql"); + assertThat(filterBody.isIncludeSuppressed()).isTrue(); + assertThat(filterBody.isIncludeBotBlockers()).isTrue(); + assertThat(filterBody.isIncludeIpBlacklist()).isTrue(); + assertThat(filterBody.getTags()).hasSize(2); + assertThat(filterBody.getStatusFilter()).hasSize(1); + assertThat(filterBody.getProtectionRules()).hasSize(1); + assertThat(filterBody.getApplications()).hasSize(1); + assertThat(filterBody.getApplicationImportances()).hasSize(1); + assertThat(filterBody.getAttackers()).hasSize(1); + assertThat(filterBody.getServers()).hasSize(1); + assertThat(filterBody.getServerEnvironments()).hasSize(1); + assertThat(filterBody.getSeverities()).hasSize(1); } @Test @@ -333,9 +322,7 @@ void testBuilder_EmptyLists_BuildsCorrectly() { AttacksFilterBody.builder().tags(new ArrayList<>()).statusFilter(new ArrayList<>()).build(); // Then - assertNotNull(filterBody.getTags()); - assertTrue(filterBody.getTags().isEmpty()); - assertNotNull(filterBody.getStatusFilter()); - assertTrue(filterBody.getStatusFilter().isEmpty()); + assertThat(filterBody.getTags()).isNotNull().isEmpty(); + assertThat(filterBody.getStatusFilter()).isNotNull().isEmpty(); } } diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/utils/PaginationHandlerTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/utils/PaginationHandlerTest.java index 576b9b0..720cd31 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/utils/PaginationHandlerTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/utils/PaginationHandlerTest.java @@ -15,7 +15,7 @@ */ package com.contrast.labs.ai.mcp.contrast.utils; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import com.contrast.labs.ai.mcp.contrast.PaginationParams; import java.util.List; @@ -43,12 +43,14 @@ void testCreatePaginatedResponse_withTotalCount_firstPage() { var response = handler.createPaginatedResponse(items, params, 10); - assertEquals(3, response.items().size()); - assertEquals(1, response.page()); - assertEquals(3, response.pageSize()); - assertEquals(10, response.totalItems()); - assertTrue(response.hasMorePages(), "Should have more pages when 3 items fetched out of 10"); - assertNull(response.message(), "No message for successful page"); + assertThat(response.items().size()).isEqualTo(3); + assertThat(response.page()).isEqualTo(1); + assertThat(response.pageSize()).isEqualTo(3); + assertThat(response.totalItems()).isEqualTo(10); + assertThat(response.hasMorePages()) + .as("Should have more pages when 3 items fetched out of 10") + .isTrue(); + assertThat(response.message()).as("No message for successful page").isNull(); } @Test @@ -58,13 +60,14 @@ void testCreatePaginatedResponse_withTotalCount_lastPage() { var response = handler.createPaginatedResponse(items, params, 10); - assertEquals(3, response.items().size()); - assertEquals(4, response.page()); - assertEquals(3, response.pageSize()); - assertEquals(10, response.totalItems()); - assertFalse( - response.hasMorePages(), "Should not have more pages (page 4 * pageSize 3 = 12 >= 10)"); - assertNull(response.message()); + assertThat(response.items().size()).isEqualTo(3); + assertThat(response.page()).isEqualTo(4); + assertThat(response.pageSize()).isEqualTo(3); + assertThat(response.totalItems()).isEqualTo(10); + assertThat(response.hasMorePages()) + .as("Should not have more pages (page 4 * pageSize 3 = 12 >= 10)") + .isFalse(); + assertThat(response.message()).isNull(); } @Test @@ -74,11 +77,11 @@ void testCreatePaginatedResponse_withoutTotalCount_fullPage() { var response = handler.createPaginatedResponse(items, params, null); - assertEquals(3, response.items().size()); - assertNull(response.totalItems()); - assertTrue( - response.hasMorePages(), - "Full page without totalCount suggests more pages exist (heuristic)"); + assertThat(response.items().size()).isEqualTo(3); + assertThat(response.totalItems()).isNull(); + assertThat(response.hasMorePages()) + .as("Full page without totalCount suggests more pages exist (heuristic)") + .isTrue(); } @Test @@ -88,9 +91,11 @@ void testCreatePaginatedResponse_withoutTotalCount_partialPage() { var response = handler.createPaginatedResponse(items, params, null); - assertEquals(2, response.items().size()); - assertNull(response.totalItems()); - assertFalse(response.hasMorePages(), "Partial page suggests no more pages (heuristic)"); + assertThat(response.items().size()).isEqualTo(2); + assertThat(response.totalItems()).isNull(); + assertThat(response.hasMorePages()) + .as("Partial page suggests no more pages (heuristic)") + .isFalse(); } @Test @@ -100,10 +105,10 @@ void testCreatePaginatedResponse_emptyFirstPage() { var response = handler.createPaginatedResponse(items, params, 0); - assertTrue(response.items().isEmpty()); - assertEquals(0, response.totalItems()); - assertFalse(response.hasMorePages()); - assertEquals("No items found.", response.message()); + assertThat(response.items()).isEmpty(); + assertThat(response.totalItems()).isEqualTo(0); + assertThat(response.hasMorePages()).isFalse(); + assertThat(response.message()).isEqualTo("No items found."); } @Test @@ -113,10 +118,11 @@ void testCreatePaginatedResponse_emptySecondPage_withTotalCount() { var response = handler.createPaginatedResponse(items, params, 5); - assertTrue(response.items().isEmpty()); - assertEquals(5, response.totalItems()); - assertFalse(response.hasMorePages()); - assertEquals("Requested page 2 exceeds available pages (total: 1).", response.message()); + assertThat(response.items()).isEmpty(); + assertThat(response.totalItems()).isEqualTo(5); + assertThat(response.hasMorePages()).isFalse(); + assertThat(response.message()) + .isEqualTo("Requested page 2 exceeds available pages (total: 1)."); } @Test @@ -126,10 +132,10 @@ void testCreatePaginatedResponse_emptySecondPage_withoutTotalCount() { var response = handler.createPaginatedResponse(items, params, null); - assertTrue(response.items().isEmpty()); - assertNull(response.totalItems()); - assertFalse(response.hasMorePages()); - assertEquals("Requested page 2 returned no results.", response.message()); + assertThat(response.items()).isEmpty(); + assertThat(response.totalItems()).isNull(); + assertThat(response.hasMorePages()).isFalse(); + assertThat(response.message()).isEqualTo("Requested page 2 returned no results."); } @Test @@ -140,10 +146,10 @@ void testCreatePaginatedResponse_mergesWarnings() { var response = handler.createPaginatedResponse(items, params, 1); - assertNotNull(response.message()); - assertTrue( - response.message().contains("Invalid page number -5"), - "Should include pagination warning in message"); + assertThat(response.message()).isNotNull(); + assertThat(response.message()) + .as("Should include pagination warning in message") + .contains("Invalid page number -5"); } @Test @@ -154,13 +160,13 @@ void testCreatePaginatedResponse_mergesWarningsWithEmptyMessage() { var response = handler.createPaginatedResponse(items, params, 5); - assertNotNull(response.message()); - assertTrue( - response.message().contains("Requested pageSize 200 exceeds maximum 100"), - "Should include pageSize warning"); - assertTrue( - response.message().contains("Requested page 2 exceeds available pages"), - "Should include empty page message"); + assertThat(response.message()).isNotNull(); + assertThat(response.message()) + .as("Should include pageSize warning") + .contains("Requested pageSize 200 exceeds maximum 100"); + assertThat(response.message()) + .as("Should include empty page message") + .contains("Requested page 2 exceeds available pages"); } // ========== Edge Cases ========== @@ -174,7 +180,9 @@ void testHasMorePages_boundaryCase() { var response = handler.createPaginatedResponse(items, params, 20); - assertFalse(response.hasMorePages(), "No more pages when page*pageSize == totalItems"); + assertThat(response.hasMorePages()) + .as("No more pages when page*pageSize == totalItems") + .isFalse(); } @Test @@ -186,7 +194,7 @@ void testHasMorePages_justOverBoundary() { var response = handler.createPaginatedResponse(items, params, 21); - assertTrue(response.hasMorePages(), "More pages when page*pageSize < totalItems"); + assertThat(response.hasMorePages()).as("More pages when page*pageSize < totalItems").isTrue(); } @Test @@ -197,9 +205,9 @@ void testMessagePriority_warningAndEmpty() { var response = handler.createPaginatedResponse(items, params, 0); - assertNotNull(response.message()); - assertTrue(response.message().contains("Invalid page number")); - assertTrue(response.message().contains("No items found")); + assertThat(response.message()).isNotNull(); + assertThat(response.message()).contains("Invalid page number"); + assertThat(response.message()).contains("No items found"); } // ========== Helper Methods ==========