Skip to content

Commit 9ad4c82

Browse files
committed
Refactor search_applications tests to use AnonymousApplicationBuilder
- Created AnonymousApplicationBuilder for building test Application mocks - Uses lenient stubbing to avoid UnnecessaryStubbingException - Makes tests much more readable - only specify fields that matter - Tests now focus on behavior instead of mock implementation details - All 257 tests pass
1 parent 9868f4d commit 9ad4c82

File tree

2 files changed

+169
-108
lines changed

2 files changed

+169
-108
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.contrast.labs.ai.mcp.contrast;
2+
3+
import static org.mockito.Mockito.lenient;
4+
import static org.mockito.Mockito.mock;
5+
6+
import com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application;
7+
import com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Metadata;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.UUID;
11+
12+
/**
13+
* Builder for creating anonymous Application mocks with sensible defaults. Only override fields
14+
* that matter for your specific test.
15+
*
16+
* <p>Example usage:
17+
*
18+
* <pre>
19+
* Application app = AnonymousApplicationBuilder.validApp()
20+
* .withName("MyApp")
21+
* .withTag("Production")
22+
* .build();
23+
* </pre>
24+
*/
25+
public class AnonymousApplicationBuilder {
26+
private final Application app;
27+
private String name = "TestApp-" + UUID.randomUUID().toString().substring(0, 8);
28+
private String status = "ACTIVE";
29+
private String appId = "app-" + UUID.randomUUID().toString().substring(0, 8);
30+
private Long lastSeen = System.currentTimeMillis();
31+
private String language = "Java";
32+
private List<String> tags = new ArrayList<>();
33+
private List<String> techs = new ArrayList<>();
34+
private List<Metadata> metadataEntities = new ArrayList<>();
35+
36+
private AnonymousApplicationBuilder() {
37+
this.app = mock(Application.class);
38+
}
39+
40+
/** Create a builder with valid defaults for all required fields. */
41+
public static AnonymousApplicationBuilder validApp() {
42+
return new AnonymousApplicationBuilder();
43+
}
44+
45+
public AnonymousApplicationBuilder withName(String name) {
46+
this.name = name;
47+
return this;
48+
}
49+
50+
public AnonymousApplicationBuilder withStatus(String status) {
51+
this.status = status;
52+
return this;
53+
}
54+
55+
public AnonymousApplicationBuilder withAppId(String appId) {
56+
this.appId = appId;
57+
return this;
58+
}
59+
60+
public AnonymousApplicationBuilder withLastSeen(Long lastSeen) {
61+
this.lastSeen = lastSeen;
62+
return this;
63+
}
64+
65+
public AnonymousApplicationBuilder withLanguage(String language) {
66+
this.language = language;
67+
return this;
68+
}
69+
70+
public AnonymousApplicationBuilder withTag(String tag) {
71+
this.tags.add(tag);
72+
return this;
73+
}
74+
75+
public AnonymousApplicationBuilder withTags(List<String> tags) {
76+
this.tags = tags;
77+
return this;
78+
}
79+
80+
public AnonymousApplicationBuilder withTech(String tech) {
81+
this.techs.add(tech);
82+
return this;
83+
}
84+
85+
public AnonymousApplicationBuilder withTechs(List<String> techs) {
86+
this.techs = techs;
87+
return this;
88+
}
89+
90+
public AnonymousApplicationBuilder withMetadata(String name, String value) {
91+
Metadata metadata = mock(Metadata.class);
92+
lenient().when(metadata.getName()).thenReturn(name);
93+
lenient().when(metadata.getValue()).thenReturn(value);
94+
this.metadataEntities.add(metadata);
95+
return this;
96+
}
97+
98+
public AnonymousApplicationBuilder withMetadataEntities(List<Metadata> metadataEntities) {
99+
this.metadataEntities = metadataEntities;
100+
return this;
101+
}
102+
103+
/**
104+
* Build the Application mock with all configured values. Uses lenient stubbing to avoid
105+
* UnnecessaryStubbingException for fields not accessed in specific tests.
106+
*/
107+
public Application build() {
108+
lenient().when(app.getName()).thenReturn(name);
109+
lenient().when(app.getStatus()).thenReturn(status);
110+
lenient().when(app.getAppId()).thenReturn(appId);
111+
lenient().when(app.getLastSeen()).thenReturn(lastSeen);
112+
lenient().when(app.getLanguage()).thenReturn(language);
113+
lenient().when(app.getTags()).thenReturn(tags);
114+
lenient().when(app.getTechs()).thenReturn(techs);
115+
lenient().when(app.getMetadataEntities()).thenReturn(metadataEntities);
116+
return app;
117+
}
118+
}

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

Lines changed: 51 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,21 +1083,9 @@ void search_applications_should_return_all_when_no_filters() throws IOException
10831083

10841084
@Test
10851085
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");
1086+
// Arrange - only specify names, everything else is valid defaults
1087+
var app1 = AnonymousApplicationBuilder.validApp().withName("MyProductionApp").build();
1088+
var app2 = AnonymousApplicationBuilder.validApp().withName("TestingApp").build();
11011089

11021090
mockedSDKHelper
11031091
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
@@ -1113,9 +1101,8 @@ void search_applications_should_filter_by_name_partial_case_insensitive() throws
11131101

11141102
@Test
11151103
void search_applications_should_filter_by_name_no_matches() throws IOException {
1116-
// Arrange
1117-
com.contrast.labs.ai.mcp.contrast.sdkextension.data.application.Application app = mock();
1118-
when(app.getName()).thenReturn("MyApp");
1104+
// Arrange - name doesn't match search term
1105+
var app = AnonymousApplicationBuilder.validApp().withName("MyApp").build();
11191106

11201107
mockedSDKHelper
11211108
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
@@ -1130,21 +1117,13 @@ void search_applications_should_filter_by_name_no_matches() throws IOException {
11301117

11311118
@Test
11321119
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
1120+
// Arrange - demonstrate case-sensitive tag matching
1121+
var app1 =
1122+
AnonymousApplicationBuilder.validApp().withName("App1").withTag("Production").build();
1123+
var app2 =
1124+
AnonymousApplicationBuilder.validApp()
1125+
.withTag("production")
1126+
.build(); // lowercase doesn't match
11481127

11491128
mockedSDKHelper
11501129
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
@@ -1160,29 +1139,14 @@ void search_applications_should_filter_by_tag_exact_case_sensitive() throws IOEx
11601139

11611140
@Test
11621141
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));
1142+
// Arrange - demonstrate case-insensitive metadata matching
1143+
var app1 =
1144+
AnonymousApplicationBuilder.validApp()
1145+
.withName("ProdApp")
1146+
.withMetadata("Environment", "Production")
1147+
.build();
1148+
var app2 =
1149+
AnonymousApplicationBuilder.validApp().withMetadata("Environment", "Development").build();
11861150

11871151
mockedSDKHelper
11881152
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
@@ -1198,29 +1162,13 @@ void search_applications_should_filter_by_metadata_name_and_value() throws IOExc
11981162

11991163
@Test
12001164
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));
1165+
// Arrange - match by metadata name regardless of value
1166+
var app1 =
1167+
AnonymousApplicationBuilder.validApp()
1168+
.withName("App1")
1169+
.withMetadata("Team", "Backend")
1170+
.build();
1171+
var app2 = AnonymousApplicationBuilder.validApp().withMetadata("Owner", "Alice").build();
12241172

12251173
mockedSDKHelper
12261174
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
@@ -1236,30 +1184,27 @@ void search_applications_should_filter_by_metadata_name_only() throws IOExceptio
12361184

12371185
@Test
12381186
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");
1187+
// Arrange - only app1 matches ALL filters (name, tag, metadata)
1188+
var app1 =
1189+
AnonymousApplicationBuilder.validApp()
1190+
.withName("ProdApp1")
1191+
.withTag("Production")
1192+
.withMetadata("Environment", "Production")
1193+
.build();
1194+
1195+
var app2 =
1196+
AnonymousApplicationBuilder.validApp()
1197+
.withName("ProdApp2")
1198+
.withTag("Development") // Wrong tag
1199+
.withMetadata("Environment", "Production")
1200+
.build();
1201+
1202+
var app3 =
1203+
AnonymousApplicationBuilder.validApp()
1204+
.withName("TestApp") // Wrong name
1205+
.withTag("Production")
1206+
.withMetadata("Environment", "Production")
1207+
.build();
12631208

12641209
mockedSDKHelper
12651210
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
@@ -1276,9 +1221,8 @@ void search_applications_should_combine_multiple_filters_with_and_logic() throws
12761221

12771222
@Test
12781223
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
1224+
// Arrange - app with empty metadata list (default from builder)
1225+
var app = AnonymousApplicationBuilder.validApp().build();
12821226

12831227
mockedSDKHelper
12841228
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))
@@ -1294,8 +1238,7 @@ void search_applications_should_handle_empty_metadata_list() throws IOException
12941238
@Test
12951239
void search_applications_should_handle_null_metadata_entities() throws IOException {
12961240
// 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
1241+
var app = AnonymousApplicationBuilder.validApp().withMetadataEntities(null).build();
12991242

13001243
mockedSDKHelper
13011244
.when(() -> SDKHelper.getApplicationsWithCache(anyString(), any()))

0 commit comments

Comments
 (0)