Skip to content

Commit 9868f4d

Browse files
committed
Fix UnnecessaryStubbingException in search_applications tests
- Only mock fields that are actually accessed during filtering and result construction - Apps that match filters need ALL fields for ApplicationData object - Apps that fail filters only need fields checked before failure - Fixed 6 failing tests, all 42 AssessServiceTest tests now pass
1 parent 8443f0d commit 9868f4d

File tree

1 file changed

+266
-15
lines changed

1 file changed

+266
-15
lines changed

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

Lines changed: 266 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,27 +1046,278 @@ void search_applications_should_validate_metadataValue_requires_metadataName() {
10461046
}
10471047

10481048
@Test
1049-
void search_applications_should_accept_all_null_parameters() throws IOException {
1050-
// Arrange - mock applications cache
1049+
void search_applications_should_return_all_when_no_filters() throws IOException {
1050+
// Arrange
1051+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app1 = mock();
1052+
when(app1.getName()).thenReturn("App1");
1053+
when(app1.getStatus()).thenReturn("ACTIVE");
1054+
when(app1.getAppId()).thenReturn("app-1");
1055+
when(app1.getLastSeen()).thenReturn(1000L);
1056+
when(app1.getLanguage()).thenReturn("Java");
1057+
when(app1.getTags()).thenReturn(List.of());
1058+
when(app1.getTechs()).thenReturn(List.of());
1059+
when(app1.getMetadataEntities()).thenReturn(List.of());
1060+
1061+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app2 = mock();
1062+
when(app2.getName()).thenReturn("App2");
1063+
when(app2.getStatus()).thenReturn("ACTIVE");
1064+
when(app2.getAppId()).thenReturn("app-2");
1065+
when(app2.getLastSeen()).thenReturn(2000L);
1066+
when(app2.getLanguage()).thenReturn("Python");
1067+
when(app2.getTags()).thenReturn(List.of());
1068+
when(app2.getTechs()).thenReturn(List.of());
1069+
when(app2.getMetadataEntities()).thenReturn(List.of());
1070+
1071+
mockedSDKHelper
1072+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1073+
.thenReturn(List.of(app1, app2));
1074+
1075+
// Act - no filters provided
1076+
var result = assessService.search_applications(null, null, null, null);
1077+
1078+
// Assert - returns all applications
1079+
assertThat(result).hasSize(2);
1080+
assertThat(result.get(0).name()).isEqualTo("App1");
1081+
assertThat(result.get(1).name()).isEqualTo("App2");
1082+
}
1083+
1084+
@Test
1085+
void search_applications_should_filter_by_name_partial_case_insensitive() throws IOException {
1086+
// Arrange
1087+
// app1 matches name filter, so ALL fields needed for result object
1088+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app1 = mock();
1089+
when(app1.getName()).thenReturn("MyProductionApp");
1090+
when(app1.getStatus()).thenReturn("ACTIVE");
1091+
when(app1.getAppId()).thenReturn("app-1");
1092+
when(app1.getLastSeen()).thenReturn(1000L);
1093+
when(app1.getLanguage()).thenReturn("Java");
1094+
when(app1.getTags()).thenReturn(List.of());
1095+
when(app1.getTechs()).thenReturn(List.of());
1096+
when(app1.getMetadataEntities()).thenReturn(List.of());
1097+
1098+
// app2 doesn't match name filter, only getName() is checked
1099+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app2 = mock();
1100+
when(app2.getName()).thenReturn("TestingApp");
1101+
1102+
mockedSDKHelper
1103+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1104+
.thenReturn(List.of(app1, app2));
1105+
1106+
// Act - search with lowercase "prod" should match "MyProductionApp"
1107+
var result = assessService.search_applications("prod", null, null, null);
1108+
1109+
// Assert - partial, case-insensitive match
1110+
assertThat(result).hasSize(1);
1111+
assertThat(result.get(0).name()).isEqualTo("MyProductionApp");
1112+
}
1113+
1114+
@Test
1115+
void search_applications_should_filter_by_name_no_matches() throws IOException {
1116+
// Arrange
10511117
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app = mock();
1052-
when(app.getName()).thenReturn("TestApp");
1053-
when(app.getStatus()).thenReturn("ACTIVE");
1054-
when(app.getAppId()).thenReturn("test-123");
1055-
when(app.getLastSeen()).thenReturn(1000L);
1056-
when(app.getLanguage()).thenReturn("Java");
1057-
when(app.getTags()).thenReturn(List.of());
1058-
when(app.getTechs()).thenReturn(List.of());
1059-
when(app.getMetadataEntities()).thenReturn(List.of());
1118+
when(app.getName()).thenReturn("MyApp");
10601119

10611120
mockedSDKHelper
10621121
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
10631122
.thenReturn(List.of(app));
10641123

1065-
// Act - call with all null parameters
1066-
var result = assessService.search_applications(null, null, null, null);
1124+
// Act - search for non-existent name
1125+
var result = assessService.search_applications("nonexistent", null, null, null);
1126+
1127+
// Assert - empty result
1128+
assertThat(result).isEmpty();
1129+
}
1130+
1131+
@Test
1132+
void search_applications_should_filter_by_tag_exact_case_sensitive() throws IOException {
1133+
// Arrange
1134+
// app1 matches tag filter, so ALL fields needed for result object
1135+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app1 = mock();
1136+
when(app1.getName()).thenReturn("App1");
1137+
when(app1.getStatus()).thenReturn("ACTIVE");
1138+
when(app1.getAppId()).thenReturn("app-1");
1139+
when(app1.getLastSeen()).thenReturn(1000L);
1140+
when(app1.getLanguage()).thenReturn("Java");
1141+
when(app1.getTags()).thenReturn(List.of("Production"));
1142+
when(app1.getTechs()).thenReturn(List.of());
1143+
when(app1.getMetadataEntities()).thenReturn(List.of());
1144+
1145+
// app2 doesn't match tag filter (case-sensitive), only getTags() is checked
1146+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app2 = mock();
1147+
when(app2.getTags()).thenReturn(List.of("production")); // lowercase
1148+
1149+
mockedSDKHelper
1150+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1151+
.thenReturn(List.of(app1, app2));
1152+
1153+
// Act - search with "Production" (capital P)
1154+
var result = assessService.search_applications(null, "Production", null, null);
1155+
1156+
// Assert - only exact case match (case-sensitive!)
1157+
assertThat(result).hasSize(1);
1158+
assertThat(result.get(0).name()).isEqualTo("App1");
1159+
}
1160+
1161+
@Test
1162+
void search_applications_should_filter_by_metadata_name_and_value() throws IOException {
1163+
// Arrange
1164+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Metadata metadata1 = mock();
1165+
when(metadata1.getName()).thenReturn("Environment");
1166+
when(metadata1.getValue()).thenReturn("Production");
1167+
1168+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Metadata metadata2 = mock();
1169+
when(metadata2.getName()).thenReturn("Environment");
1170+
when(metadata2.getValue()).thenReturn("Development");
1171+
1172+
// app1 matches metadata filter, so ALL fields needed for result object
1173+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app1 = mock();
1174+
when(app1.getName()).thenReturn("ProdApp");
1175+
when(app1.getStatus()).thenReturn("ACTIVE");
1176+
when(app1.getAppId()).thenReturn("app-1");
1177+
when(app1.getLastSeen()).thenReturn(1000L);
1178+
when(app1.getLanguage()).thenReturn("Java");
1179+
when(app1.getTags()).thenReturn(List.of());
1180+
when(app1.getTechs()).thenReturn(List.of());
1181+
when(app1.getMetadataEntities()).thenReturn(List.of(metadata1));
1182+
1183+
// app2 doesn't match metadata filter, only getMetadataEntities() is checked
1184+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app2 = mock();
1185+
when(app2.getMetadataEntities()).thenReturn(List.of(metadata2));
1186+
1187+
mockedSDKHelper
1188+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1189+
.thenReturn(List.of(app1, app2));
1190+
1191+
// Act - search with "environment" and "PRODUCTION" (case-insensitive)
1192+
var result = assessService.search_applications(null, null, "environment", "PRODUCTION");
1193+
1194+
// Assert - case-insensitive match for both name and value
1195+
assertThat(result).hasSize(1);
1196+
assertThat(result.get(0).name()).isEqualTo("ProdApp");
1197+
}
1198+
1199+
@Test
1200+
void search_applications_should_filter_by_metadata_name_only() throws IOException {
1201+
// Arrange
1202+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Metadata metadata1 = mock();
1203+
when(metadata1.getName()).thenReturn("Team");
1204+
when(metadata1.getValue()).thenReturn("Backend");
1205+
1206+
// metadata2 doesn't match name "team", so getValue() never called
1207+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Metadata metadata2 = mock();
1208+
when(metadata2.getName()).thenReturn("Owner");
1209+
1210+
// app1 matches metadata name filter, so ALL fields needed for result object
1211+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app1 = mock();
1212+
when(app1.getName()).thenReturn("App1");
1213+
when(app1.getStatus()).thenReturn("ACTIVE");
1214+
when(app1.getAppId()).thenReturn("app-1");
1215+
when(app1.getLastSeen()).thenReturn(1000L);
1216+
when(app1.getLanguage()).thenReturn("Java");
1217+
when(app1.getTags()).thenReturn(List.of());
1218+
when(app1.getTechs()).thenReturn(List.of());
1219+
when(app1.getMetadataEntities()).thenReturn(List.of(metadata1));
1220+
1221+
// app2 doesn't match metadata name filter, only getMetadataEntities() is checked
1222+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app2 = mock();
1223+
when(app2.getMetadataEntities()).thenReturn(List.of(metadata2));
1224+
1225+
mockedSDKHelper
1226+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1227+
.thenReturn(List.of(app1, app2));
1228+
1229+
// Act - search by metadata name only (any value)
1230+
var result = assessService.search_applications(null, null, "team", null);
1231+
1232+
// Assert - matches any app with "team" metadata (case-insensitive)
1233+
assertThat(result).hasSize(1);
1234+
assertThat(result.get(0).name()).isEqualTo("App1");
1235+
}
1236+
1237+
@Test
1238+
void search_applications_should_combine_multiple_filters_with_and_logic() throws IOException {
1239+
// Arrange
1240+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Metadata metadata = mock();
1241+
when(metadata.getName()).thenReturn("Environment");
1242+
when(metadata.getValue()).thenReturn("Production");
1243+
1244+
// app1 matches all filters, so ALL fields needed for result object
1245+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app1 = mock();
1246+
when(app1.getName()).thenReturn("ProdApp1");
1247+
when(app1.getStatus()).thenReturn("ACTIVE");
1248+
when(app1.getAppId()).thenReturn("app-1");
1249+
when(app1.getLastSeen()).thenReturn(1000L);
1250+
when(app1.getLanguage()).thenReturn("Java");
1251+
when(app1.getTags()).thenReturn(List.of("Production"));
1252+
when(app1.getTechs()).thenReturn(List.of());
1253+
when(app1.getMetadataEntities()).thenReturn(List.of(metadata));
1254+
1255+
// app2 passes name but fails tag check, only getName() and getTags() checked
1256+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app2 = mock();
1257+
when(app2.getName()).thenReturn("ProdApp2");
1258+
when(app2.getTags()).thenReturn(List.of("Development")); // Wrong tag
1259+
1260+
// app3 fails name check immediately, only getName() checked
1261+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app3 = mock();
1262+
when(app3.getName()).thenReturn("TestApp");
1263+
1264+
mockedSDKHelper
1265+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1266+
.thenReturn(List.of(app1, app2, app3));
1267+
1268+
// Act - combine name, tag, and metadata filters (AND logic)
1269+
var result =
1270+
assessService.search_applications("prod", "Production", "Environment", "Production");
1271+
1272+
// Assert - only app1 matches ALL filters
1273+
assertThat(result).hasSize(1);
1274+
assertThat(result.get(0).name()).isEqualTo("ProdApp1");
1275+
}
10671276

1068-
// Assert - should return all applications when no filters specified
1069-
assertThat(result).isNotNull().hasSize(1);
1070-
assertThat(result.get(0).name()).isEqualTo("TestApp");
1277+
@Test
1278+
void search_applications_should_handle_empty_metadata_list() throws IOException {
1279+
// Arrange - app with empty metadata list
1280+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app = mock();
1281+
when(app.getMetadataEntities()).thenReturn(List.of()); // Empty list - key test scenario
1282+
1283+
mockedSDKHelper
1284+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1285+
.thenReturn(List.of(app));
1286+
1287+
// Act - search with metadata filter
1288+
var result = assessService.search_applications(null, null, "Environment", null);
1289+
1290+
// Assert - no match (app has no metadata)
1291+
assertThat(result).isEmpty();
1292+
}
1293+
1294+
@Test
1295+
void search_applications_should_handle_null_metadata_entities() throws IOException {
1296+
// Arrange - app with null metadata entities
1297+
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app = mock();
1298+
when(app.getMetadataEntities()).thenReturn(null); // Null - key test scenario
1299+
1300+
mockedSDKHelper
1301+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1302+
.thenReturn(List.of(app));
1303+
1304+
// Act - search with metadata filter
1305+
var result = assessService.search_applications(null, null, "Environment", null);
1306+
1307+
// Assert - no match and no NPE (defensive coding)
1308+
assertThat(result).isEmpty();
1309+
}
1310+
1311+
@Test
1312+
void search_applications_should_propagate_IOException_from_cache() throws IOException {
1313+
// Arrange
1314+
mockedSDKHelper
1315+
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
1316+
.thenThrow(new IOException("Cache failure"));
1317+
1318+
// Act & Assert - IOException propagates
1319+
assertThatThrownBy(() -> assessService.search_applications(null, null, null, null))
1320+
.isInstanceOf(IOException.class)
1321+
.hasMessageContaining("Failed to search applications");
10711322
}
10721323
}

0 commit comments

Comments
 (0)