Skip to content

Commit 1aafe1a

Browse files
ChrisEdwardsclaude
andcommitted
Add verification tests for status filter fix (mcp-3sy)
Added 3 unit tests to verify status filters work correctly after mcp-b9y's unified refactoring eliminated the filterBody object: 1. searchAppVulnerabilities_should_apply_status_filters_with_sessionMetadataName() - Verifies explicit status filters are passed to SDK when using sessionMetadataName - Uses ArgumentCaptor to verify SDK receives status filters 2. searchAppVulnerabilities_should_apply_status_filters_with_useLatestSession() - Verifies explicit status filters are passed to SDK when using useLatestSession - Tests that status filtering works independently with session ID filtering 3. searchAppVulnerabilities_should_use_smart_defaults_with_sessionMetadataName() - Verifies smart default statuses (Reported, Suspicious, Confirmed) are applied - Confirms Fixed and Remediated are excluded from smart defaults - Tests status filtering with session metadata filtering combined All tests use ArgumentCaptor to verify the CRITICAL fix: status filters are now always passed to SDK in the filterForm, regardless of session filtering mode. This verifies the bug is fixed by the unified architecture approach. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent d948df0 commit 1aafe1a

File tree

1 file changed

+346
-0
lines changed

1 file changed

+346
-0
lines changed

src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceTest.java

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,4 +1990,350 @@ void searchAppVulnerabilities_should_pass_all_standard_filters_to_SDK() throws E
19901990
assertThat(form.getFilterTags()).isNotNull();
19911991
assertThat(form.getFilterTags().size()).isEqualTo(2);
19921992
}
1993+
1994+
// ========== Status Filter Verification Tests (mcp-3sy) ==========
1995+
// These tests verify that status filters work correctly after mcp-b9y's unified refactoring
1996+
// eliminated the filterBody object, fixing the bug where status filters were ignored
1997+
// when sessionMetadataName was provided.
1998+
1999+
@Test
2000+
void searchAppVulnerabilities_should_apply_status_filters_with_sessionMetadataName()
2001+
throws Exception {
2002+
// Given - Simulate SDK returning only traces matching status filter
2003+
// In reality, SDK filters server-side. We mock that behavior here.
2004+
var mockTraces = new Traces();
2005+
var traces = new ArrayList<Trace>();
2006+
2007+
// Trace 0: Reported status, branch=main
2008+
Trace trace0 = mock();
2009+
when(trace0.getTitle()).thenReturn("SQL Injection");
2010+
when(trace0.getRule()).thenReturn("sql-injection");
2011+
when(trace0.getUuid()).thenReturn("uuid-reported");
2012+
when(trace0.getSeverity()).thenReturn("HIGH");
2013+
when(trace0.getStatus()).thenReturn("REPORTED");
2014+
when(trace0.getLastTimeSeen()).thenReturn(System.currentTimeMillis());
2015+
when(trace0.getFirstTimeSeen()).thenReturn(System.currentTimeMillis() - 86400000L);
2016+
2017+
var sessionMetadata0 = mock(SessionMetadata.class);
2018+
var metadataItem0 = mock(MetadataItem.class);
2019+
lenient().when(metadataItem0.getDisplayLabel()).thenReturn("branch");
2020+
lenient().when(metadataItem0.getValue()).thenReturn("main");
2021+
lenient().when(sessionMetadata0.getMetadata()).thenReturn(List.of(metadataItem0));
2022+
lenient().when(trace0.getSessionMetadata()).thenReturn(List.of(sessionMetadata0));
2023+
traces.add(trace0);
2024+
2025+
// Trace 1: Suspicious status, branch=main
2026+
Trace trace1 = mock();
2027+
when(trace1.getTitle()).thenReturn("Path Traversal");
2028+
when(trace1.getRule()).thenReturn("path-traversal");
2029+
when(trace1.getUuid()).thenReturn("uuid-suspicious");
2030+
when(trace1.getSeverity()).thenReturn("HIGH");
2031+
when(trace1.getStatus()).thenReturn("SUSPICIOUS");
2032+
when(trace1.getLastTimeSeen()).thenReturn(System.currentTimeMillis());
2033+
when(trace1.getFirstTimeSeen()).thenReturn(System.currentTimeMillis() - 86400000L);
2034+
2035+
var sessionMetadata1 = mock(SessionMetadata.class);
2036+
var metadataItem1 = mock(MetadataItem.class);
2037+
lenient().when(metadataItem1.getDisplayLabel()).thenReturn("branch");
2038+
lenient().when(metadataItem1.getValue()).thenReturn("main");
2039+
lenient().when(sessionMetadata1.getMetadata()).thenReturn(List.of(metadataItem1));
2040+
lenient().when(trace1.getSessionMetadata()).thenReturn(List.of(sessionMetadata1));
2041+
traces.add(trace1);
2042+
2043+
// Trace 2: Reported status, branch=develop (excluded by session metadata filter)
2044+
Trace trace2 = mock();
2045+
when(trace2.getTitle()).thenReturn("XSS Reflected");
2046+
when(trace2.getRule()).thenReturn("xss-reflected");
2047+
when(trace2.getUuid()).thenReturn("uuid-develop");
2048+
when(trace2.getSeverity()).thenReturn("MEDIUM");
2049+
when(trace2.getStatus()).thenReturn("REPORTED");
2050+
when(trace2.getLastTimeSeen()).thenReturn(System.currentTimeMillis());
2051+
when(trace2.getFirstTimeSeen()).thenReturn(System.currentTimeMillis() - 86400000L);
2052+
2053+
var sessionMetadata2 = mock(SessionMetadata.class);
2054+
var metadataItem2 = mock(MetadataItem.class);
2055+
lenient().when(metadataItem2.getDisplayLabel()).thenReturn("branch");
2056+
lenient().when(metadataItem2.getValue()).thenReturn("develop");
2057+
lenient().when(sessionMetadata2.getMetadata()).thenReturn(List.of(metadataItem2));
2058+
lenient().when(trace2.getSessionMetadata()).thenReturn(List.of(sessionMetadata2));
2059+
traces.add(trace2);
2060+
2061+
// Setup mock traces using reflection
2062+
// Note: SDK filters by status server-side, so Fixed/Remediated wouldn't be returned
2063+
try {
2064+
var tracesField = Traces.class.getDeclaredField("traces");
2065+
tracesField.setAccessible(true);
2066+
tracesField.set(mockTraces, traces);
2067+
2068+
var countField = Traces.class.getDeclaredField("count");
2069+
countField.setAccessible(true);
2070+
countField.set(mockTraces, 3);
2071+
} catch (Exception e) {
2072+
throw new RuntimeException("Failed to create mock Traces", e);
2073+
}
2074+
2075+
when(mockContrastSDK.getTraces(eq(TEST_ORG_ID), eq(TEST_APP_ID), any(TraceFilterForm.class)))
2076+
.thenReturn(mockTraces);
2077+
2078+
// When - call with statuses="Reported,Suspicious" AND sessionMetadataName="branch"
2079+
var result =
2080+
assessService.searchAppVulnerabilities(
2081+
TEST_APP_ID,
2082+
null, // page
2083+
null, // pageSize
2084+
null, // severities
2085+
"Reported,Suspicious", // statuses (explicit filter)
2086+
null, // vulnTypes
2087+
null, // environments
2088+
null, // lastSeenAfter
2089+
null, // lastSeenBefore
2090+
null, // vulnTags
2091+
"branch", // sessionMetadataName
2092+
"main", // sessionMetadataValue
2093+
null); // useLatestSession
2094+
2095+
// Then - verify SDK received status filters (CRITICAL verification for mcp-3sy)
2096+
var captor = ArgumentCaptor.forClass(TraceFilterForm.class);
2097+
verify(mockContrastSDK).getTraces(eq(TEST_ORG_ID), eq(TEST_APP_ID), captor.capture());
2098+
2099+
var form = captor.getValue();
2100+
assertThat(form.getStatus())
2101+
.as("Status filters should be passed to SDK when session filtering is active")
2102+
.isNotNull()
2103+
.containsExactlyInAnyOrder("Reported", "Suspicious");
2104+
2105+
// Verify results: trace0 and trace1 match (Reported + Suspicious with branch=main)
2106+
// trace2 is excluded by session metadata filter (branch=develop)
2107+
assertThat(result.items())
2108+
.as("Should return traces matching both status and session metadata filters")
2109+
.hasSize(2);
2110+
assertThat(result.items().get(0).title()).isEqualTo("SQL Injection");
2111+
assertThat(result.items().get(1).title()).isEqualTo("Path Traversal");
2112+
}
2113+
2114+
@Test
2115+
void searchAppVulnerabilities_should_apply_status_filters_with_useLatestSession()
2116+
throws Exception {
2117+
// Given - Simulate SDK returning only Confirmed status traces (SDK filters server-side)
2118+
var mockTraces = new Traces();
2119+
var traces = new ArrayList<Trace>();
2120+
2121+
// Trace 0: Confirmed status, latest session
2122+
Trace trace0 = mock();
2123+
when(trace0.getTitle()).thenReturn("Command Injection");
2124+
when(trace0.getRule()).thenReturn("cmd-injection");
2125+
when(trace0.getUuid()).thenReturn("uuid-confirmed-latest");
2126+
when(trace0.getSeverity()).thenReturn("CRITICAL");
2127+
when(trace0.getStatus()).thenReturn("CONFIRMED");
2128+
when(trace0.getLastTimeSeen()).thenReturn(System.currentTimeMillis());
2129+
when(trace0.getFirstTimeSeen()).thenReturn(System.currentTimeMillis() - 86400000L);
2130+
2131+
var sessionMetadata0 = mock(SessionMetadata.class);
2132+
lenient().when(sessionMetadata0.getSessionId()).thenReturn("latest-session-id");
2133+
lenient().when(trace0.getSessionMetadata()).thenReturn(List.of(sessionMetadata0));
2134+
traces.add(trace0);
2135+
2136+
// Trace 1: Confirmed status, old session (excluded by useLatestSession filter)
2137+
Trace trace1 = mock();
2138+
when(trace1.getTitle()).thenReturn("LDAP Injection");
2139+
when(trace1.getRule()).thenReturn("ldap-injection");
2140+
when(trace1.getUuid()).thenReturn("uuid-confirmed-old");
2141+
when(trace1.getSeverity()).thenReturn("HIGH");
2142+
when(trace1.getStatus()).thenReturn("CONFIRMED");
2143+
when(trace1.getLastTimeSeen()).thenReturn(System.currentTimeMillis());
2144+
when(trace1.getFirstTimeSeen()).thenReturn(System.currentTimeMillis() - 86400000L);
2145+
2146+
var sessionMetadata1 = mock(SessionMetadata.class);
2147+
lenient().when(sessionMetadata1.getSessionId()).thenReturn("old-session-id");
2148+
lenient().when(trace1.getSessionMetadata()).thenReturn(List.of(sessionMetadata1));
2149+
traces.add(trace1);
2150+
2151+
// Setup mock traces using reflection
2152+
try {
2153+
var tracesField = Traces.class.getDeclaredField("traces");
2154+
tracesField.setAccessible(true);
2155+
tracesField.set(mockTraces, traces);
2156+
2157+
var countField = Traces.class.getDeclaredField("count");
2158+
countField.setAccessible(true);
2159+
countField.set(mockTraces, 2);
2160+
} catch (Exception e) {
2161+
throw new RuntimeException("Failed to create mock Traces", e);
2162+
}
2163+
2164+
when(mockContrastSDK.getTraces(eq(TEST_ORG_ID), eq(TEST_APP_ID), any(TraceFilterForm.class)))
2165+
.thenReturn(mockTraces);
2166+
2167+
// Mock SDKExtension to return latest session
2168+
var mockAgentSession =
2169+
new com.contrast.labs.ai.mcp.contrast.sdkextension.data.sessionmetadata.AgentSession();
2170+
mockAgentSession.setAgentSessionId("latest-session-id");
2171+
var mockSessionResponse =
2172+
new com.contrast.labs.ai.mcp.contrast.sdkextension.data.sessionmetadata
2173+
.SessionMetadataResponse();
2174+
mockSessionResponse.setAgentSession(mockAgentSession);
2175+
2176+
try (var mockedSDKExtension =
2177+
mockConstruction(
2178+
com.contrast.labs.ai.mcp.contrast.sdkextension.SDKExtension.class,
2179+
(mock, context) -> {
2180+
when(mock.getLatestSessionMetadata(eq(TEST_ORG_ID), eq(TEST_APP_ID)))
2181+
.thenReturn(mockSessionResponse);
2182+
})) {
2183+
2184+
// When - call with statuses="Confirmed" AND useLatestSession=true
2185+
var result =
2186+
assessService.searchAppVulnerabilities(
2187+
TEST_APP_ID,
2188+
null, // page
2189+
null, // pageSize
2190+
null, // severities
2191+
"Confirmed", // statuses (explicit filter)
2192+
null, // vulnTypes
2193+
null, // environments
2194+
null, // lastSeenAfter
2195+
null, // lastSeenBefore
2196+
null, // vulnTags
2197+
null, // sessionMetadataName
2198+
null, // sessionMetadataValue
2199+
true); // useLatestSession=true
2200+
2201+
// Then - verify SDK received status filters (CRITICAL verification for mcp-3sy)
2202+
var captor = ArgumentCaptor.forClass(TraceFilterForm.class);
2203+
verify(mockContrastSDK).getTraces(eq(TEST_ORG_ID), eq(TEST_APP_ID), captor.capture());
2204+
2205+
var form = captor.getValue();
2206+
assertThat(form.getStatus())
2207+
.as("Status filters should be passed to SDK even with useLatestSession")
2208+
.isNotNull()
2209+
.containsExactly("Confirmed");
2210+
2211+
// Verify results: only trace0 matches (Confirmed + latest session)
2212+
// trace1 excluded by in-memory session ID filtering
2213+
assertThat(result.items())
2214+
.as("Should return only Confirmed vulnerabilities from latest session")
2215+
.hasSize(1);
2216+
assertThat(result.items().get(0).title()).isEqualTo("Command Injection");
2217+
assertThat(result.items().get(0).status()).isEqualTo("CONFIRMED");
2218+
}
2219+
}
2220+
2221+
@Test
2222+
void searchAppVulnerabilities_should_use_smart_defaults_with_sessionMetadataName()
2223+
throws Exception {
2224+
// Given - Simulate SDK returning only smart default statuses (SDK filters server-side)
2225+
// Smart defaults = Reported, Suspicious, Confirmed (exclude Fixed and Remediated)
2226+
var mockTraces = new Traces();
2227+
var traces = new ArrayList<Trace>();
2228+
2229+
// Trace 0: Reported (included in smart defaults), Environment=Production
2230+
Trace trace0 = mock();
2231+
when(trace0.getTitle()).thenReturn("Reported Vuln");
2232+
when(trace0.getRule()).thenReturn("rule-0");
2233+
when(trace0.getUuid()).thenReturn("uuid-0");
2234+
when(trace0.getSeverity()).thenReturn("HIGH");
2235+
when(trace0.getStatus()).thenReturn("REPORTED");
2236+
when(trace0.getLastTimeSeen()).thenReturn(System.currentTimeMillis());
2237+
when(trace0.getFirstTimeSeen()).thenReturn(System.currentTimeMillis() - 86400000L);
2238+
2239+
var sessionMetadata0 = mock(SessionMetadata.class);
2240+
var metadataItem0 = mock(MetadataItem.class);
2241+
lenient().when(metadataItem0.getDisplayLabel()).thenReturn("Environment");
2242+
lenient().when(metadataItem0.getValue()).thenReturn("Production");
2243+
lenient().when(sessionMetadata0.getMetadata()).thenReturn(List.of(metadataItem0));
2244+
lenient().when(trace0.getSessionMetadata()).thenReturn(List.of(sessionMetadata0));
2245+
traces.add(trace0);
2246+
2247+
// Trace 1: Confirmed (included in smart defaults), Environment=Production
2248+
Trace trace1 = mock();
2249+
when(trace1.getTitle()).thenReturn("Confirmed Vuln");
2250+
when(trace1.getRule()).thenReturn("rule-1");
2251+
when(trace1.getUuid()).thenReturn("uuid-1");
2252+
when(trace1.getSeverity()).thenReturn("CRITICAL");
2253+
when(trace1.getStatus()).thenReturn("CONFIRMED");
2254+
when(trace1.getLastTimeSeen()).thenReturn(System.currentTimeMillis());
2255+
when(trace1.getFirstTimeSeen()).thenReturn(System.currentTimeMillis() - 86400000L);
2256+
2257+
var sessionMetadata1 = mock(SessionMetadata.class);
2258+
var metadataItem1 = mock(MetadataItem.class);
2259+
lenient().when(metadataItem1.getDisplayLabel()).thenReturn("Environment");
2260+
lenient().when(metadataItem1.getValue()).thenReturn("Production");
2261+
lenient().when(sessionMetadata1.getMetadata()).thenReturn(List.of(metadataItem1));
2262+
lenient().when(trace1.getSessionMetadata()).thenReturn(List.of(sessionMetadata1));
2263+
traces.add(trace1);
2264+
2265+
// Trace 2: Reported (included in smart defaults), Environment=QA (excluded by metadata)
2266+
Trace trace2 = mock();
2267+
when(trace2.getTitle()).thenReturn("QA Vuln");
2268+
when(trace2.getRule()).thenReturn("rule-2");
2269+
when(trace2.getUuid()).thenReturn("uuid-2");
2270+
when(trace2.getSeverity()).thenReturn("MEDIUM");
2271+
when(trace2.getStatus()).thenReturn("REPORTED");
2272+
when(trace2.getLastTimeSeen()).thenReturn(System.currentTimeMillis());
2273+
when(trace2.getFirstTimeSeen()).thenReturn(System.currentTimeMillis() - 86400000L);
2274+
2275+
var sessionMetadata2 = mock(SessionMetadata.class);
2276+
var metadataItem2 = mock(MetadataItem.class);
2277+
lenient().when(metadataItem2.getDisplayLabel()).thenReturn("Environment");
2278+
lenient().when(metadataItem2.getValue()).thenReturn("QA");
2279+
lenient().when(sessionMetadata2.getMetadata()).thenReturn(List.of(metadataItem2));
2280+
lenient().when(trace2.getSessionMetadata()).thenReturn(List.of(sessionMetadata2));
2281+
traces.add(trace2);
2282+
2283+
// Setup mock traces using reflection
2284+
// Note: Fixed/Remediated traces filtered out by SDK (smart defaults)
2285+
try {
2286+
var tracesField = Traces.class.getDeclaredField("traces");
2287+
tracesField.setAccessible(true);
2288+
tracesField.set(mockTraces, traces);
2289+
2290+
var countField = Traces.class.getDeclaredField("count");
2291+
countField.setAccessible(true);
2292+
countField.set(mockTraces, 3);
2293+
} catch (Exception e) {
2294+
throw new RuntimeException("Failed to create mock Traces", e);
2295+
}
2296+
2297+
when(mockContrastSDK.getTraces(eq(TEST_ORG_ID), eq(TEST_APP_ID), any(TraceFilterForm.class)))
2298+
.thenReturn(mockTraces);
2299+
2300+
// When - call with sessionMetadataName but NO explicit statuses (triggers smart defaults)
2301+
var result =
2302+
assessService.searchAppVulnerabilities(
2303+
TEST_APP_ID,
2304+
null, // page
2305+
null, // pageSize
2306+
null, // severities
2307+
null, // statuses (NOT provided - should use smart defaults)
2308+
null, // vulnTypes
2309+
null, // environments
2310+
null, // lastSeenAfter
2311+
null, // lastSeenBefore
2312+
null, // vulnTags
2313+
"Environment", // sessionMetadataName
2314+
"Production", // sessionMetadataValue
2315+
null); // useLatestSession
2316+
2317+
// Then - verify SDK received smart default statuses (CRITICAL verification for mcp-3sy)
2318+
var captor = ArgumentCaptor.forClass(TraceFilterForm.class);
2319+
verify(mockContrastSDK).getTraces(eq(TEST_ORG_ID), eq(TEST_APP_ID), captor.capture());
2320+
2321+
var form = captor.getValue();
2322+
assertThat(form.getStatus())
2323+
.as(
2324+
"Smart default statuses should be passed to SDK (Reported, Suspicious, Confirmed -"
2325+
+ " excluding Fixed and Remediated)")
2326+
.isNotNull()
2327+
.containsExactlyInAnyOrder("Reported", "Suspicious", "Confirmed")
2328+
.doesNotContain("Fixed", "Remediated");
2329+
2330+
// Verify results: trace0 and trace1 match (Reported + Confirmed with Environment=Production)
2331+
// trace2 excluded by session metadata filter (Environment=QA)
2332+
// Note: Smart default warning message is tested separately in VulnerabilityFilterParamsTest
2333+
assertThat(result.items())
2334+
.as("Should return only actionable statuses (excluding Fixed and Remediated)")
2335+
.hasSize(2);
2336+
assertThat(result.items().get(0).title()).isEqualTo("Reported Vuln");
2337+
assertThat(result.items().get(1).title()).isEqualTo("Confirmed Vuln");
2338+
}
19932339
}

0 commit comments

Comments
 (0)