From 0437da1076c02525f594347dbedab9e6f8f2998d Mon Sep 17 00:00:00 2001 From: theghost5800 Date: Wed, 1 Apr 2026 17:50:34 +0300 Subject: [PATCH 1/2] Add pagination for long queries in CustomControllerClients JIRA:LMCROSSITXSADEPLOY-3432 --- .../cf/clients/CustomControllerClient.java | 48 +- .../cf/clients/CustomServiceKeysClient.java | 29 +- .../clients/ServiceInstanceRoutesGetter.java | 43 +- ...ppBoundServiceInstanceNamesGetterTest.java | 308 +++++++++ .../clients/CFOptimizedEventGetterTest.java | 302 +++++++++ .../core/cf/clients/CfRolesGetterTest.java | 306 +++++++++ .../clients/CustomServiceKeysClientTest.java | 588 ++++++++++++++++++ .../ServiceInstanceRoutesGetterTest.java | 410 ++++++++++++ 8 files changed, 1974 insertions(+), 60 deletions(-) create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/AppBoundServiceInstanceNamesGetterTest.java create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CFOptimizedEventGetterTest.java create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CfRolesGetterTest.java create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetterTest.java diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClient.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClient.java index 8edf474e8b..ec29bc3a2a 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClient.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClient.java @@ -17,6 +17,8 @@ public abstract class CustomControllerClient { + protected static final int MAX_URI_QUERY_LENGTH = 4000; + private final WebClient webClient; private String correlationId = StringUtils.EMPTY; private final CloudControllerHeaderConfiguration headerConfiguration; @@ -34,17 +36,55 @@ protected CustomControllerClient(ApplicationConfiguration configuration, WebClie this.headerConfiguration = new CloudControllerHeaderConfiguration(configuration.getVersion()); } - protected List getListOfResources(ResourcesResponseMapper responseMapper, String uri, Object... urlVariables) { - PaginationV3 pagination = addPageOfResources(uri, responseMapper, urlVariables); + protected List getListOfResources(ResourcesResponseMapper responseMapper, String uri) { + PaginationV3 pagination = addPageOfResources(uri, responseMapper); while (!StringUtils.isEmpty(pagination.getNextUri())) { pagination = addPageOfResources(pagination.getNextUri(), responseMapper); } return responseMapper.getMappedResources(); } - private PaginationV3 addPageOfResources(String uri, ResourcesResponseMapper responseMapper, Object... urlVariables) { + protected List getListOfResourcesInBatches(ResourcesResponseMapper responseMapper, String uriPrefix, String batchParamPrefix, + List batchValues) { + int fixedUriLength = uriPrefix.length() + batchParamPrefix.length(); + List> batches = splitIntoBatches(batchValues, fixedUriLength); + return batches.stream() + .map(batch -> { + String uri = uriPrefix + batchParamPrefix + String.join(",", batch); + return getListOfResources(responseMapper, uri); + }) + .flatMap(List::stream) + .toList(); + } + + List> splitIntoBatches(List values, int fixedUriLength) { + int maxBatchLength = Math.max(1, MAX_URI_QUERY_LENGTH - fixedUriLength); + List> batches = new ArrayList<>(); + List currentBatch = new ArrayList<>(); + int currentLength = 0; + + for (String value : values) { + // Account for the comma separator between values + int addedLength = currentBatch.isEmpty() ? value.length() : value.length() + 1; + if (!currentBatch.isEmpty() && currentLength + addedLength > maxBatchLength) { + batches.add(currentBatch); + currentBatch = new ArrayList<>(); + currentLength = 0; + addedLength = value.length(); + } + currentBatch.add(value); + currentLength += addedLength; + } + + if (!currentBatch.isEmpty()) { + batches.add(currentBatch); + } + return batches; + } + + private PaginationV3 addPageOfResources(String uri, ResourcesResponseMapper responseMapper) { String responseString = webClient.get() - .uri(uri, urlVariables) + .uri(uri) .headers(httpHeaders -> httpHeaders.addAll(generateRequestHeaders())) .retrieve() .bodyToMono(String.class) diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java index 3d4def6631..6a62a8e372 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java @@ -18,11 +18,10 @@ import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; public class CustomServiceKeysClient extends CustomControllerClient { - private static final String SERVICE_KEYS_RESOURCE_BASE_URI = "/v3/service_credential_bindings"; private static final String SERVICE_KEYS_BY_METADATA_SELECTOR_URI = SERVICE_KEYS_RESOURCE_BASE_URI + "?type=key&label_selector={value}"; private static final String INCLUDE_SERVICE_INSTANCE_RESOURCES_PARAM = "&include=service_instance"; - + private static final String SERVICE_INSTANCE_GUIDS_PARAM_PREFIX = "&service_instance_guids="; private final CloudEntityResourceMapper resourceMapper = new CloudEntityResourceMapper(); public CustomServiceKeysClient(ApplicationConfiguration configuration, WebClientFactory webClientFactory, CloudCredentials credentials, @@ -35,17 +34,13 @@ public List getServiceKeysByMetadataAndExistingGuids( String mtaId, String mtaNamespace, List existingServiceGuids) { - String labelSelector = buildMtaMetadataLabelSelector(spaceGuid, mtaId, mtaNamespace); - List allServiceGuids = existingServiceGuids.stream() .filter(Objects::nonNull) .toList(); - if (allServiceGuids.isEmpty()) { return List.of(); } - return new CustomControllerClientErrorHandler() .handleErrorsOrReturnResult( () -> getServiceKeysByMetadataInternal(labelSelector, allServiceGuids) @@ -57,15 +52,11 @@ public List getServiceKeysByMetadataAndManagedServices( String mtaId, String mtaNamespace, List services) { - String labelSelector = buildMtaMetadataLabelSelector(spaceGuid, mtaId, mtaNamespace); - List managedGuids = extractManagedServiceGuids(services); - if (managedGuids.isEmpty()) { return List.of(); } - return new CustomControllerClientErrorHandler() .handleErrorsOrReturnResult( () -> getServiceKeysByMetadataInternal(labelSelector, managedGuids) @@ -75,7 +66,6 @@ public List getServiceKeysByMetadataAndManagedServices( private String buildMtaMetadataLabelSelector(String spaceGuid, String mtaId, String mtaNamespace) { - return MtaMetadataCriteriaBuilder.builder() .label(MtaMetadataLabels.SPACE_GUID) .hasValue(spaceGuid) @@ -98,13 +88,12 @@ private List extractManagedServiceGuids(List service } private List getServiceKeysByMetadataInternal(String labelSelector, List guids) { - - String uriSuffix = INCLUDE_SERVICE_INSTANCE_RESOURCES_PARAM - + "&service_instance_guids=" + String.join(",", guids); - - return getListOfResources(new ServiceKeysResponseMapper(), - SERVICE_KEYS_BY_METADATA_SELECTOR_URI + uriSuffix, - labelSelector); + String expandedUriPrefix = SERVICE_KEYS_BY_METADATA_SELECTOR_URI.replace("{value}", labelSelector) + + INCLUDE_SERVICE_INSTANCE_RESOURCES_PARAM; + return getListOfResourcesInBatches(new ServiceKeysResponseMapper(), + expandedUriPrefix, + SERVICE_INSTANCE_GUIDS_PARAM_PREFIX, + guids); } private List getManagedServices(List services) { @@ -132,12 +121,10 @@ public List getMappedResources() { public Map getIncludedServiceInstancesMapping() { List serviceInstances = getIncludedResources().getOrDefault("service_instances", Collections.emptyList()); - return serviceInstances.stream() .distinct() .map(service -> (Map) service) .collect(Collectors.toMap(service -> (String) service.get("guid"), resourceMapper::mapService)); - } } -} \ No newline at end of file +} diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetter.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetter.java index 280c5e0786..a665ce12ec 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetter.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetter.java @@ -1,7 +1,5 @@ package org.cloudfoundry.multiapps.controller.core.cf.clients; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -13,48 +11,23 @@ public class ServiceInstanceRoutesGetter extends CustomControllerClient { - private static final int MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST = 4000; + private static final String SERVICE_ROUTE_BINDINGS_URI_PREFIX = "/v3/service_route_bindings?"; + private static final String ROUTE_GUIDS_PARAM_PREFIX = "route_guids="; public ServiceInstanceRoutesGetter(ApplicationConfiguration configuration, WebClientFactory webClientFactory, CloudCredentials credentials, String correlationId) { super(configuration, webClientFactory, credentials, correlationId); } - public List getServiceRouteBindings(Collection routeGuids) { + public List getServiceRouteBindings(List routeGuids) { return new CustomControllerClientErrorHandler().handleErrorsOrReturnResult(() -> doGetServiceRouteBindings(routeGuids)); } - private List doGetServiceRouteBindings(Collection routeGuids) { - var batchedRouteGuids = getBatchedRouteGuids(routeGuids); - var responseMapper = new ServiceRouteBindingsResponseMapper(); - return batchedRouteGuids.stream() - .map(this::getServiceRouteBindingsUrl) - .map(url -> getListOfResources(responseMapper, url)) - .flatMap(List::stream) - .collect(Collectors.toList()); - } - - private List> getBatchedRouteGuids(Collection routeGuids) { - List> batches = new ArrayList<>(); - int currentBatchLength = 0, currentBatchIndex = 0; - batches.add(new ArrayList<>()); - - for (String routeGuid : routeGuids) { - int elementLength = routeGuid.length(); - if (elementLength + currentBatchLength >= MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST) { - batches.add(new ArrayList<>()); - currentBatchIndex++; - currentBatchLength = 0; - } - batches.get(currentBatchIndex) - .add(routeGuid); - currentBatchLength += elementLength; - } - return batches; - } - - private String getServiceRouteBindingsUrl(Collection routeGuids) { - return "/v3/service_route_bindings?route_guids=" + String.join(",", routeGuids); + private List doGetServiceRouteBindings(List routeGuids) { + return getListOfResourcesInBatches(new ServiceRouteBindingsResponseMapper(), + SERVICE_ROUTE_BINDINGS_URI_PREFIX, + ROUTE_GUIDS_PARAM_PREFIX, + routeGuids); } protected static class ServiceRouteBindingsResponseMapper extends ResourcesResponseMapper { diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/AppBoundServiceInstanceNamesGetterTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/AppBoundServiceInstanceNamesGetterTest.java new file mode 100644 index 0000000000..882bf947e7 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/AppBoundServiceInstanceNamesGetterTest.java @@ -0,0 +1,308 @@ +package org.cloudfoundry.multiapps.controller.core.cf.clients; + +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AppBoundServiceInstanceNamesGetterTest { + + private static final String CORRELATION_ID = "test-correlation-id"; + + @Mock + private WebClientFactory webClientFactory; + @Mock + private ApplicationConfiguration applicationConfiguration; + @Mock + private WebClient webClient; + + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); + @Mock + private WebClient.ResponseSpec responseSpec; + + private AppBoundServiceInstanceNamesGetter client; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + Mockito.when(applicationConfiguration.getVersion()) + .thenReturn("1.0.0"); + Mockito.when(webClientFactory.getWebClient(Mockito.any(CloudCredentials.class))) + .thenReturn(webClient); + + client = new AppBoundServiceInstanceNamesGetter(applicationConfiguration, webClientFactory, new CloudCredentials("user", "pass"), + CORRELATION_ID); + } + + @Test + void testGetServiceInstanceNamesBoundToAppBuildsCorrectUri() { + stubWebClientToReturnEmptyPage(); + + UUID appGuid = UUID.randomUUID(); + client.getServiceInstanceNamesBoundToApp(appGuid); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.startsWith("/v3/service_credential_bindings?"), + "URI should start with the service credential bindings base path"); + assertTrue(capturedUri.contains("include=service_instance"), + "URI should include service_instance"); + assertTrue(capturedUri.contains("app_guids=" + appGuid), + "URI should contain the app guid"); + } + + @Test + void testGetServiceInstanceNamesBoundToAppWithEmptyResponseReturnsEmptyList() { + stubWebClientToReturnEmptyPage(); + + UUID appGuid = UUID.randomUUID(); + List result = client.getServiceInstanceNamesBoundToApp(appGuid); + + assertTrue(result.isEmpty()); + } + + @Test + void testGetServiceInstanceNamesBoundToAppReturnsSingleServiceName() { + String serviceName = "my-database"; + String responseJson = buildResponseWithIncludedServiceInstances(List.of(serviceName)); + stubWebClientToReturn(responseJson); + + UUID appGuid = UUID.randomUUID(); + List result = client.getServiceInstanceNamesBoundToApp(appGuid); + + assertEquals(1, result.size()); + assertEquals(serviceName, result.getFirst()); + } + + @Test + void testGetServiceInstanceNamesBoundToAppReturnsMultipleServiceNames() { + List serviceNames = List.of("db-service", "cache-service", "queue-service"); + String responseJson = buildResponseWithIncludedServiceInstances(serviceNames); + stubWebClientToReturn(responseJson); + + UUID appGuid = UUID.randomUUID(); + List result = client.getServiceInstanceNamesBoundToApp(appGuid); + + assertEquals(3, result.size()); + assertEquals("db-service", result.get(0)); + assertEquals("cache-service", result.get(1)); + assertEquals("queue-service", result.get(2)); + } + + @Test + void testGetServiceInstanceNamesBoundToAppDeduplicatesServiceNames() { + List serviceNames = List.of("my-service", "my-service", "other-service"); + String responseJson = buildResponseWithIncludedServiceInstances(serviceNames); + stubWebClientToReturn(responseJson); + + UUID appGuid = UUID.randomUUID(); + List result = client.getServiceInstanceNamesBoundToApp(appGuid); + + assertEquals(2, result.size()); + assertEquals("my-service", result.get(0)); + assertEquals("other-service", result.get(1)); + } + + @Test + void testGetServiceInstanceNamesBoundToAppWithNoIncludedResourcesReturnsEmptyList() { + // Response has resources but no "included" section + String responseJson = "{\"resources\":[{\"guid\":\"" + UUID.randomUUID() + "\"}],\"pagination\":{\"next\":null}}"; + stubWebClientToReturn(responseJson); + + UUID appGuid = UUID.randomUUID(); + List result = client.getServiceInstanceNamesBoundToApp(appGuid); + + assertTrue(result.isEmpty()); + } + + @Test + void testGetServiceInstanceNamesBoundToAppPaginatedResponseFollowsAllPages() { + String page1Json = buildResponseWithIncludedServiceInstancesAndPagination( + List.of("svc-page1"), "/v3/service_credential_bindings?page=2"); + String page2Json = buildResponseWithIncludedServiceInstances(List.of("svc-page2")); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + UUID appGuid = UUID.randomUUID(); + List result = client.getServiceInstanceNamesBoundToApp(appGuid); + + assertEquals(2, result.size()); + assertEquals("svc-page1", result.get(0)); + assertEquals("svc-page2", result.get(1)); + } + + @Test + void testGetServiceInstanceNamesBoundToAppMakesExactlyOneHttpCallForSinglePage() { + stubWebClientToReturnEmptyPage(); + + UUID appGuid = UUID.randomUUID(); + client.getServiceInstanceNamesBoundToApp(appGuid); + + Mockito.verify(webClient, Mockito.times(1)) + .get(); + } + + @Test + void testGetServiceInstanceNamesBoundToAppMakesTwoHttpCallsForTwoPages() { + String page1Json = buildResponseWithIncludedServiceInstancesAndPagination( + List.of("svc-1"), "/v3/service_credential_bindings?page=2"); + String page2Json = buildResponseWithIncludedServiceInstances(List.of("svc-2")); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + UUID appGuid = UUID.randomUUID(); + client.getServiceInstanceNamesBoundToApp(appGuid); + + Mockito.verify(webClient, Mockito.times(2)) + .get(); + } + + @Test + void testGetServiceInstanceNamesBoundToAppWithResourcesButEmptyIncludedServiceInstancesReturnsEmptyList() { + // Response has "included" but with an empty "service_instances" list + String responseJson = "{\"resources\":[{\"guid\":\"" + UUID.randomUUID() + "\"}]," + + "\"included\":{\"service_instances\":[]}," + + "\"pagination\":{\"next\":null}}"; + stubWebClientToReturn(responseJson); + + UUID appGuid = UUID.randomUUID(); + List result = client.getServiceInstanceNamesBoundToApp(appGuid); + + assertTrue(result.isEmpty()); + } + + @Test + void testGetServiceInstanceNamesBoundToAppDeduplicatesAcrossPages() { + // Same service name appears on both pages + String page1Json = buildResponseWithIncludedServiceInstancesAndPagination( + List.of("shared-service"), "/v3/service_credential_bindings?page=2"); + String page2Json = buildResponseWithIncludedServiceInstances(List.of("shared-service")); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + UUID appGuid = UUID.randomUUID(); + List result = client.getServiceInstanceNamesBoundToApp(appGuid); + + assertEquals(1, result.size(), "Duplicate service names across pages should be deduplicated"); + assertEquals("shared-service", result.getFirst()); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnEmptyPage() { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturn(String responseJson) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responseJson)); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnSequentially(String... responses) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + + if (responses.length == 1) { + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0])); + } else { + @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; + for (int i = 1; i < responses.length; i++) { + remaining[i - 1] = Mono.just(responses[i]); + } + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0]), remaining); + } + } + + private String buildResponseWithIncludedServiceInstances(List serviceNames) { + return buildResponseWithIncludedServiceInstancesAndPagination(serviceNames, null); + } + + private String buildResponseWithIncludedServiceInstancesAndPagination(List serviceNames, String nextPageHref) { + String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; + + StringBuilder bindingResources = new StringBuilder(); + StringBuilder serviceInstanceResources = new StringBuilder(); + + for (int i = 0; i < serviceNames.size(); i++) { + UUID serviceInstanceGuid = UUID.randomUUID(); + + if (i > 0) { + bindingResources.append(","); + serviceInstanceResources.append(","); + } + + bindingResources.append("{") + .append("\"guid\":\"") + .append(UUID.randomUUID()) + .append("\",") + .append("\"type\":\"app\",") + .append("\"relationships\":{") + .append(" \"service_instance\":{\"data\":{\"guid\":\"") + .append(serviceInstanceGuid) + .append("\"}}") + .append("}") + .append("}"); + + serviceInstanceResources.append("{") + .append("\"guid\":\"") + .append(serviceInstanceGuid) + .append("\",") + .append("\"name\":\"") + .append(serviceNames.get(i)) + .append("\",") + .append("\"type\":\"managed\"") + .append("}"); + } + + return "{\"resources\":[" + bindingResources + "]," + + "\"included\":{\"service_instances\":[" + serviceInstanceResources + "]}," + + "\"pagination\":{\"next\":" + nextPage + "}}"; + } +} + diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CFOptimizedEventGetterTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CFOptimizedEventGetterTest.java new file mode 100644 index 0000000000..79faf39c7b --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CFOptimizedEventGetterTest.java @@ -0,0 +1,302 @@ +package org.cloudfoundry.multiapps.controller.core.cf.clients; + +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CFOptimizedEventGetterTest { + + private static final String EVENT_TYPE = "audit.app.update"; + private static final String TIMESTAMP = "2024-06-01T00:00:00Z"; + + @Mock + private WebClientFactory webClientFactory; + @Mock + private ApplicationConfiguration applicationConfiguration; + @Mock + private WebClient webClient; + + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); + @Mock + private WebClient.ResponseSpec responseSpec; + + private CFOptimizedEventGetter client; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + Mockito.when(applicationConfiguration.getVersion()) + .thenReturn("1.0.0"); + Mockito.when(webClientFactory.getWebClient(Mockito.any(CloudCredentials.class))) + .thenReturn(webClient); + + client = new CFOptimizedEventGetter(applicationConfiguration, webClientFactory, new CloudCredentials("user", "pass")); + } + + @Test + void testFindEventsBuildsCorrectUri() { + stubWebClientToReturnEmptyPage(); + + client.findEvents(EVENT_TYPE, TIMESTAMP); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.startsWith("/v3/audit_events?"), "URI should start with the audit events base path"); + assertTrue(capturedUri.contains("types=" + EVENT_TYPE), "URI should contain the event type"); + assertTrue(capturedUri.contains("per_page=100"), "URI should contain per_page=100"); + assertTrue(capturedUri.contains("created_ats[gt]=" + TIMESTAMP), "URI should contain the timestamp filter"); + } + + @Test + void testFindEventsWithEmptyResponseReturnsEmptyList() { + stubWebClientToReturnEmptyPage(); + + List result = client.findEvents(EVENT_TYPE, TIMESTAMP); + + assertTrue(result.isEmpty()); + } + + @Test + void testFindEventsReturnsSingleSpaceId() { + String spaceGuid = UUID.randomUUID() + .toString(); + String responseJson = buildAuditEventsResponse(List.of(spaceGuid)); + stubWebClientToReturn(responseJson); + + List result = client.findEvents(EVENT_TYPE, TIMESTAMP); + + assertEquals(1, result.size()); + assertEquals(spaceGuid, result.getFirst()); + } + + @Test + void testFindEventsReturnsMultipleSpaceIds() { + String spaceGuid1 = UUID.randomUUID() + .toString(); + String spaceGuid2 = UUID.randomUUID() + .toString(); + String spaceGuid3 = UUID.randomUUID() + .toString(); + String responseJson = buildAuditEventsResponse(List.of(spaceGuid1, spaceGuid2, spaceGuid3)); + stubWebClientToReturn(responseJson); + + List result = client.findEvents(EVENT_TYPE, TIMESTAMP); + + assertEquals(3, result.size()); + assertEquals(spaceGuid1, result.get(0)); + assertEquals(spaceGuid2, result.get(1)); + assertEquals(spaceGuid3, result.get(2)); + } + + @Test + void testFindEventsFiltersNullSpaceGuids() { + String spaceGuid = UUID.randomUUID() + .toString(); + // Build a response with one event having a valid space guid and one with null guid + String responseJson = buildAuditEventsResponseWithNullSpaceGuid(spaceGuid); + stubWebClientToReturn(responseJson); + + List result = client.findEvents(EVENT_TYPE, TIMESTAMP); + + assertEquals(1, result.size()); + assertEquals(spaceGuid, result.getFirst()); + } + + @Test + void testFindEventsPaginatedResponseFollowsAllPages() { + String spaceGuid1 = UUID.randomUUID() + .toString(); + String spaceGuid2 = UUID.randomUUID() + .toString(); + + String page1Json = buildAuditEventsResponseWithPagination(List.of(spaceGuid1), "/v3/audit_events?page=2"); + String page2Json = buildAuditEventsResponse(List.of(spaceGuid2)); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + List result = client.findEvents(EVENT_TYPE, TIMESTAMP); + + assertEquals(2, result.size()); + assertEquals(spaceGuid1, result.getFirst()); + assertEquals(spaceGuid2, result.get(1)); + } + + @Test + void testFindEventsMakesExactlyOneHttpCallForSinglePage() { + stubWebClientToReturnEmptyPage(); + + client.findEvents(EVENT_TYPE, TIMESTAMP); + + Mockito.verify(webClient, Mockito.times(1)) + .get(); + } + + @Test + void testFindEventsMakesTwoHttpCallsForTwoPages() { + String spaceGuid = UUID.randomUUID() + .toString(); + + String page1Json = buildAuditEventsResponseWithPagination(List.of(spaceGuid), "/v3/audit_events?page=2"); + String page2Json = buildAuditEventsResponse(List.of(spaceGuid)); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + client.findEvents(EVENT_TYPE, TIMESTAMP); + + Mockito.verify(webClient, Mockito.times(2)) + .get(); + } + + @Test + void testFindEventsWithDifferentEventType() { + stubWebClientToReturnEmptyPage(); + + String customType = "audit.app.delete"; + client.findEvents(customType, TIMESTAMP); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.contains("types=" + customType), "URI should contain the custom event type"); + } + + @Test + void testFindEventsWithDifferentTimestamp() { + stubWebClientToReturnEmptyPage(); + + String customTimestamp = "2025-12-31T23:59:59Z"; + client.findEvents(EVENT_TYPE, customTimestamp); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.contains("created_ats[gt]=" + customTimestamp), "URI should contain the custom timestamp"); + } + + @Test + void testFindEventsDuplicateSpaceIdsArePreserved() { + String spaceGuid = UUID.randomUUID() + .toString(); + // Same space guid appearing in multiple events + String responseJson = buildAuditEventsResponse(List.of(spaceGuid, spaceGuid, spaceGuid)); + stubWebClientToReturn(responseJson); + + List result = client.findEvents(EVENT_TYPE, TIMESTAMP); + + assertEquals(3, result.size(), "Duplicate space IDs should be preserved since the mapper does not deduplicate"); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnEmptyPage() { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturn(String responseJson) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responseJson)); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnSequentially(String... responses) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + + if (responses.length == 1) { + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0])); + } else { + @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; + for (int i = 1; i < responses.length; i++) { + remaining[i - 1] = Mono.just(responses[i]); + } + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0]), remaining); + } + } + + private String buildAuditEventsResponse(List spaceGuids) { + return buildAuditEventsResponseWithPagination(spaceGuids, null); + } + + private String buildAuditEventsResponseWithPagination(List spaceGuids, String nextPageHref) { + String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; + StringBuilder resources = new StringBuilder(); + for (int i = 0; i < spaceGuids.size(); i++) { + if (i > 0) { + resources.append(","); + } + resources.append("{") + .append("\"guid\":\"") + .append(UUID.randomUUID()) + .append("\",") + .append("\"type\":\"audit.app.update\",") + .append("\"created_at\":\"2024-06-15T10:00:00Z\",") + .append("\"space\":{\"guid\":\"") + .append(spaceGuids.get(i)) + .append("\"}") + .append("}"); + } + return "{\"resources\":[" + resources + "],\"pagination\":{\"next\":" + nextPage + "}}"; + } + + private String buildAuditEventsResponseWithNullSpaceGuid(String validSpaceGuid) { + return "{\"resources\":[" + + "{\"guid\":\"" + UUID.randomUUID() + "\",\"type\":\"audit.app.update\"," + + "\"created_at\":\"2024-06-15T10:00:00Z\"," + + "\"space\":{\"guid\":\"" + validSpaceGuid + "\"}}," + + "{\"guid\":\"" + UUID.randomUUID() + "\",\"type\":\"audit.app.update\"," + + "\"created_at\":\"2024-06-15T10:00:00Z\"," + + "\"space\":{\"guid\":null}}" + + "],\"pagination\":{\"next\":null}}"; + } +} + diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CfRolesGetterTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CfRolesGetterTest.java new file mode 100644 index 0000000000..7468368e73 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CfRolesGetterTest.java @@ -0,0 +1,306 @@ +package org.cloudfoundry.multiapps.controller.core.cf.clients; + +import java.util.Arrays; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; +import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CfRolesGetterTest { + + @Mock + private WebClientFactory webClientFactory; + @Mock + private ApplicationConfiguration applicationConfiguration; + @Mock + private WebClient webClient; + + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); + @Mock + private WebClient.ResponseSpec responseSpec; + + private CfRolesGetter client; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + Mockito.when(applicationConfiguration.getVersion()) + .thenReturn("1.0.0"); + Mockito.when(webClientFactory.getWebClient(Mockito.any(CloudCredentials.class))) + .thenReturn(webClient); + + client = new CfRolesGetter(applicationConfiguration, webClientFactory, new CloudCredentials("user", "pass")); + } + + @Test + void testGetRolesBuildsCorrectUri() { + stubWebClientToReturnEmptyPage(); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + client.getRoles(spaceGuid, userGuid); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.startsWith("/v3/roles?"), "URI should start with /v3/roles?"); + assertTrue(capturedUri.contains("space_guids=" + spaceGuid), "URI should contain the space guid"); + assertTrue(capturedUri.contains("user_guids=" + userGuid), "URI should contain the user guid"); + } + + @Test + void testGetRolesUriContainsAllRoleTypes() { + stubWebClientToReturnEmptyPage(); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + client.getRoles(spaceGuid, userGuid); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + String expectedTypesFilter = Arrays.stream(UserRole.values()) + .map(UserRole::getName) + .collect(Collectors.joining(",")); + assertTrue(capturedUri.contains("types=" + expectedTypesFilter), "URI should contain all UserRole types as a filter"); + } + + @Test + void testGetRolesWithEmptyResponseReturnsEmptySet() { + stubWebClientToReturnEmptyPage(); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + Set result = client.getRoles(spaceGuid, userGuid); + + assertTrue(result.isEmpty()); + } + + @Test + void testGetRolesReturnsSingleRole() { + String responseJson = buildRolesResponse("space_developer"); + stubWebClientToReturn(responseJson); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + Set result = client.getRoles(spaceGuid, userGuid); + + assertEquals(1, result.size()); + assertTrue(result.contains(UserRole.SPACE_DEVELOPER)); + } + + @Test + void testGetRolesReturnsMultipleRoles() { + String responseJson = buildRolesResponse("space_developer", "space_manager", "space_auditor"); + stubWebClientToReturn(responseJson); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + Set result = client.getRoles(spaceGuid, userGuid); + + assertEquals(3, result.size()); + assertTrue(result.contains(UserRole.SPACE_DEVELOPER)); + assertTrue(result.contains(UserRole.SPACE_MANAGER)); + assertTrue(result.contains(UserRole.SPACE_AUDITOR)); + } + + @Test + void testGetRolesReturnsOrganizationRoles() { + String responseJson = buildRolesResponse("organization_manager", "organization_auditor"); + stubWebClientToReturn(responseJson); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + Set result = client.getRoles(spaceGuid, userGuid); + + assertEquals(2, result.size()); + assertTrue(result.contains(UserRole.ORGANIZATION_MANAGER)); + assertTrue(result.contains(UserRole.ORGANIZATION_AUDITOR)); + } + + @Test + void testGetRolesDeduplicatesDuplicateRoles() { + String responseJson = buildRolesResponse("space_developer", "space_developer"); + stubWebClientToReturn(responseJson); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + Set result = client.getRoles(spaceGuid, userGuid); + + assertEquals(1, result.size(), "Duplicate roles should be deduplicated in the EnumSet"); + assertTrue(result.contains(UserRole.SPACE_DEVELOPER)); + } + + @Test + void testGetRolesPaginatedResponseFollowsAllPages() { + String page1Json = buildRolesResponseWithPagination(new String[] { "space_developer" }, "/v3/roles?page=2"); + String page2Json = buildRolesResponse("space_manager"); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + Set result = client.getRoles(spaceGuid, userGuid); + + assertEquals(2, result.size()); + assertTrue(result.contains(UserRole.SPACE_DEVELOPER)); + assertTrue(result.contains(UserRole.SPACE_MANAGER)); + } + + @Test + void testGetRolesMakesExactlyOneHttpCallForSinglePage() { + stubWebClientToReturnEmptyPage(); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + client.getRoles(spaceGuid, userGuid); + + Mockito.verify(webClient, Mockito.times(1)) + .get(); + } + + @Test + void testGetRolesMakesTwoHttpCallsForTwoPages() { + String page1Json = buildRolesResponseWithPagination(new String[] { "space_developer" }, "/v3/roles?page=2"); + String page2Json = buildRolesResponse("space_auditor"); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + client.getRoles(spaceGuid, userGuid); + + Mockito.verify(webClient, Mockito.times(2)) + .get(); + } + + @Test + void testGetRolesReturnsAllSpaceRoles() { + String responseJson = buildRolesResponse("space_developer", "space_manager", "space_auditor"); + stubWebClientToReturn(responseJson); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + Set result = client.getRoles(spaceGuid, userGuid); + + assertTrue(result.contains(UserRole.SPACE_DEVELOPER)); + assertTrue(result.contains(UserRole.SPACE_MANAGER)); + assertTrue(result.contains(UserRole.SPACE_AUDITOR)); + } + + @Test + void testGetRolesReturnsMixedSpaceAndOrgRoles() { + String responseJson = buildRolesResponse("space_developer", "organization_manager", "organization_user"); + stubWebClientToReturn(responseJson); + + UUID spaceGuid = UUID.randomUUID(); + UUID userGuid = UUID.randomUUID(); + Set result = client.getRoles(spaceGuid, userGuid); + + assertEquals(3, result.size()); + assertTrue(result.contains(UserRole.SPACE_DEVELOPER)); + assertTrue(result.contains(UserRole.ORGANIZATION_MANAGER)); + assertTrue(result.contains(UserRole.ORGANIZATION_USER)); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnEmptyPage() { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturn(String responseJson) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responseJson)); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnSequentially(String... responses) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + + if (responses.length == 1) { + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0])); + } else { + @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; + for (int i = 1; i < responses.length; i++) { + remaining[i - 1] = Mono.just(responses[i]); + } + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0]), remaining); + } + } + + private String buildRolesResponse(String... roleTypes) { + return buildRolesResponseWithPagination(roleTypes, null); + } + + private String buildRolesResponseWithPagination(String[] roleTypes, String nextPageHref) { + String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; + StringBuilder resources = new StringBuilder(); + for (int i = 0; i < roleTypes.length; i++) { + if (i > 0) { + resources.append(","); + } + resources.append("{") + .append("\"guid\":\"") + .append(UUID.randomUUID()) + .append("\",") + .append("\"type\":\"") + .append(roleTypes[i]) + .append("\",") + .append("\"created_at\":\"2024-01-01T00:00:00Z\",") + .append("\"updated_at\":\"2024-01-01T00:00:00Z\"") + .append("}"); + } + return "{\"resources\":[" + resources + "],\"pagination\":{\"next\":" + nextPage + "}}"; + } +} + diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java new file mode 100644 index 0000000000..2db2b93faa --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java @@ -0,0 +1,588 @@ +package org.cloudfoundry.multiapps.controller.core.cf.clients; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; +import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaService; +import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaServiceKey; +import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMtaService; +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CustomServiceKeysClientTest { + + private static final String SPACE_GUID = "space-guid-123"; + private static final String MTA_ID = "my-mta"; + private static final String MTA_NAMESPACE = "my-namespace"; + private static final String CORRELATION_ID = "test-correlation-id"; + + @Mock + private WebClientFactory webClientFactory; + @Mock + private ApplicationConfiguration applicationConfiguration; + @Mock + private WebClient webClient; + + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); + @Mock + private WebClient.ResponseSpec responseSpec; + + private CustomServiceKeysClient client; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + Mockito.when(applicationConfiguration.getVersion()) + .thenReturn("1.0.0"); + Mockito.when(webClientFactory.getWebClient(Mockito.any(CloudCredentials.class))) + .thenReturn(webClient); + + client = new CustomServiceKeysClient(applicationConfiguration, webClientFactory, new CloudCredentials("user", "pass"), + CORRELATION_ID); + } + + @Test + void testGetServiceKeysByExistingGuidsWithEmptyGuidsReturnsEmptyList() { + List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + Collections.emptyList()); + assertTrue(result.isEmpty()); + Mockito.verifyNoInteractions(webClient); + } + + @Test + void testGetServiceKeysByExistingGuidsWithAllNullGuidsReturnsEmptyList() { + List nullGuids = new ArrayList<>(); + nullGuids.add(null); + nullGuids.add(null); + + List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, nullGuids); + assertTrue(result.isEmpty()); + Mockito.verifyNoInteractions(webClient); + } + + @Test + void testGetServiceKeysByExistingGuidsFiltersNullGuids() { + stubWebClientToReturnEmptyPage(); + + String randomServiceGuid = UUID.randomUUID() + .toString(); + List guidsWithNulls = new ArrayList<>(); + guidsWithNulls.add(null); + guidsWithNulls.add(randomServiceGuid); + guidsWithNulls.add(null); + + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + guidsWithNulls); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.endsWith(MessageFormat.format("&service_instance_guids={0}", randomServiceGuid)), + "service_instance_guids should contain only the non-null guid"); + } + + @Test + void testGetServiceKeysByExistingGuidsBuildsCorrectUri() { + stubWebClientToReturnEmptyPage(); + + String guid = UUID.randomUUID() + .toString(); + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(guid)); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.startsWith("/v3/service_credential_bindings?type=key&label_selector="), + "URI should start with the service keys base path"); + assertTrue(capturedUri.contains("space_guid=" + SPACE_GUID), "URI should contain the space_guid label selector"); + assertTrue(capturedUri.contains("&include=service_instance"), "URI should include service_instance"); + assertTrue(capturedUri.contains("&service_instance_guids=" + guid), + "URI should contain the service instance guid parameter"); + } + + @Test + void testGetServiceKeysByExistingGuidsJoinsMultipleGuidsWithComma() { + stubWebClientToReturnEmptyPage(); + + String guid1 = UUID.randomUUID() + .toString(); + String guid2 = UUID.randomUUID() + .toString(); + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(guid1, guid2)); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.contains("&service_instance_guids=" + guid1 + "," + guid2), + "URI should contain both guids joined by comma"); + } + + @Test + void testGetServiceKeysByExistingGuidsReturnsServiceKeys() { + UUID serviceInstanceGuid = UUID.randomUUID(); + UUID serviceKeyGuid = UUID.randomUUID(); + + String responseJson = buildServiceKeysResponse(serviceKeyGuid, serviceInstanceGuid, "my-key", "my-service"); + stubWebClientToReturn(responseJson); + + List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(serviceInstanceGuid.toString())); + + assertEquals(1, result.size()); + DeployedMtaServiceKey key = result.getFirst(); + assertEquals("my-key", key.getName()); + assertEquals(serviceKeyGuid, key.getMetadata() + .getGuid()); + assertNotNull(key.getServiceInstance()); + assertEquals("my-service", key.getServiceInstance() + .getName()); + } + + @Test + void testGetServiceKeysByExistingGuidsWithEmptyResponseReturnsEmptyList() { + stubWebClientToReturnEmptyPage(); + + String guid = UUID.randomUUID() + .toString(); + List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(guid)); + assertTrue(result.isEmpty()); + } + + @Test + void testGetServiceKeysByManagedServicesWithEmptyServiceListReturnsEmptyList() { + List result = client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + Collections.emptyList()); + assertTrue(result.isEmpty()); + Mockito.verifyNoInteractions(webClient); + } + + @Test + void testGetServiceKeysByManagedServicesWithOnlyUserProvidedReturnsEmptyList() { + DeployedMtaService userProvidedService = ImmutableDeployedMtaService.builder() + .name("ups") + .type(ServiceInstanceType.USER_PROVIDED) + .metadata(ImmutableCloudMetadata.builder() + .guid(UUID.randomUUID()) + .build()) + .build(); + + List result = client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(userProvidedService)); + assertTrue(result.isEmpty()); + Mockito.verifyNoInteractions(webClient); + } + + @Test + void testGetServiceKeysByManagedServicesWithNullMetadataReturnsEmptyList() { + DeployedMtaService serviceWithNullMetadata = ImmutableDeployedMtaService.builder() + .name("no-meta") + .type(ServiceInstanceType.MANAGED) + .build(); + + List result = client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(serviceWithNullMetadata)); + assertTrue(result.isEmpty()); + Mockito.verifyNoInteractions(webClient); + } + + @Test + void testGetServiceKeysByManagedServicesWithNullGuidInMetadataReturnsEmptyList() { + DeployedMtaService serviceWithNullGuid = ImmutableDeployedMtaService.builder() + .name("null-guid") + .type(ServiceInstanceType.MANAGED) + .metadata(ImmutableCloudMetadata.builder() + .build()) + .build(); + + List result = client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(serviceWithNullGuid)); + assertTrue(result.isEmpty()); + Mockito.verifyNoInteractions(webClient); + } + + @Test + void testGetServiceKeysByManagedServicesMakesHttpCall() { + stubWebClientToReturnEmptyPage(); + + UUID serviceGuid = UUID.randomUUID(); + DeployedMtaService managedService = buildManagedService("managed-svc", serviceGuid); + + client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(managedService)); + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.contains("&service_instance_guids=" + serviceGuid), "URI should contain the guid of the managed service"); + Mockito.verify(webClient, Mockito.times(1)) + .get(); + } + + @Test + void testGetServiceKeysByManagedServicesUsesOnlyManagedGuids() { + stubWebClientToReturnEmptyPage(); + + UUID managedGuid = UUID.randomUUID(); + UUID userProvidedGuid = UUID.randomUUID(); + DeployedMtaService managedService = buildManagedService("managed-svc", managedGuid); + DeployedMtaService userProvidedService = ImmutableDeployedMtaService.builder() + .name("ups") + .type(ServiceInstanceType.USER_PROVIDED) + .metadata(ImmutableCloudMetadata.builder() + .guid(userProvidedGuid) + .build()) + .build(); + + client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(managedService, userProvidedService)); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.endsWith("&service_instance_guids=" + managedGuid), + "URI should contain only the managed service guid"); + assertFalse(capturedUri.contains(userProvidedGuid.toString()), "URI should not contain the user-provided service guid"); + } + + @Test + void testGetServiceKeysByManagedServicesReturnsServiceKeys() { + UUID serviceInstanceGuid = UUID.randomUUID(); + UUID serviceKeyGuid = UUID.randomUUID(); + + String serviceKeyName = "sk-1"; + String serviceInstanceName = "svc-1"; + String responseJson = buildServiceKeysResponse(serviceKeyGuid, serviceInstanceGuid, serviceKeyName, serviceInstanceName); + stubWebClientToReturn(responseJson); + + DeployedMtaService managedService = buildManagedService(serviceInstanceName, serviceInstanceGuid); + + List result = client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(managedService)); + + assertEquals(1, result.size()); + assertEquals(serviceKeyName, result.getFirst() + .getName()); + assertEquals(serviceInstanceName, result.getFirst() + .getServiceInstance() + .getName()); + } + + @Test + void testLabelSelectorContainsSpaceGuidMtaIdAndMtaNamespace() { + stubWebClientToReturnEmptyPage(); + + String guid = UUID.randomUUID() + .toString(); + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(guid)); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.contains("space_guid=" + SPACE_GUID)); + assertTrue(capturedUri.contains("mta_id=")); + assertTrue(capturedUri.contains("mta_namespace=")); + } + + @Test + void testLabelSelectorWithNullNamespaceUsesDoesNotExist() { + stubWebClientToReturnEmptyPage(); + + String guid = UUID.randomUUID() + .toString(); + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, null, List.of(guid)); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + // When namespace is null/empty, the label selector uses "!" prefix (doesNotExist) + assertTrue(capturedUri.contains("!mta_namespace"), "When namespace is null, label selector should use !mta_namespace"); + } + + @Test + void testLabelSelectorWithEmptyNamespaceUsesDoesNotExist() { + stubWebClientToReturnEmptyPage(); + + String guid = UUID.randomUUID() + .toString(); + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, "", List.of(guid)); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.contains("!mta_namespace"), "When namespace is empty, label selector should use !mta_namespace"); + } + + @Test + void testBatchingTriggeredWithManyGuids() { + stubWebClientToReturnEmptyPage(); + + // Generate enough GUIDs to force multiple batches (each UUID is 36 chars, limit is 4000) + List manyGuids = generateRandomGuids(200); + + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, manyGuids); + + // With 200 GUIDs and a fixed prefix consuming some of the 4000-char budget, multiple requests are expected + Mockito.verify(webClient, Mockito.atLeast(2)) + .get(); + } + + @Test + void testSingleBatchWithFewGuids() { + stubWebClientToReturnEmptyPage(); + + List fewGuids = generateRandomGuids(3); + + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, fewGuids); + + Mockito.verify(webClient, Mockito.times(1)) + .get(); + } + + @Test + void testBatchedUrisNeverExceedMaxLength() { + stubWebClientToReturnEmptyPage(); + + List manyGuids = generateRandomGuids(800); + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, manyGuids); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec, Mockito.atLeastOnce()) + .uri(uriCaptor.capture()); + + for (String uri : uriCaptor.getAllValues()) { + assertTrue(uri.length() <= CustomControllerClient.MAX_URI_QUERY_LENGTH, + "URI length " + uri.length() + " exceeds MAX_URI_QUERY_LENGTH " + + CustomControllerClient.MAX_URI_QUERY_LENGTH); + } + } + + @Test + void testBatchedRequestsContainAllGuidsInOrder() { + stubWebClientToReturnEmptyPage(); + + List manyGuids = generateRandomGuids(200); + client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, manyGuids); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec, Mockito.atLeastOnce()) + .uri(uriCaptor.capture()); + + List allCapturedGuids = new ArrayList<>(); + for (String uri : uriCaptor.getAllValues()) { + int idx = uri.indexOf("&service_instance_guids="); + assertTrue(idx >= 0, "URI should contain &service_instance_guids="); + String guidsStr = uri.substring(idx + "&service_instance_guids=".length()); + String[] guids = guidsStr.split(","); + Collections.addAll(allCapturedGuids, guids); + } + + assertEquals(manyGuids.size(), allCapturedGuids.size(), "All guids must be sent across batched requests"); + assertEquals(manyGuids, allCapturedGuids, "Guids must appear in original order across batches"); + } + + @Test + void testPaginatedResponseFollowsAllPages() { + UUID siGuid = UUID.randomUUID(); + + UUID keyGuid1 = UUID.randomUUID(); + UUID keyGuid2 = UUID.randomUUID(); + + String page1Json = buildServiceKeysResponseWithPagination(keyGuid1, siGuid, "key-1", "svc-1", + "/v3/service_credential_bindings?page=2"); + String page2Json = buildServiceKeysResponse(keyGuid2, siGuid, "key-2", "svc-1"); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(siGuid.toString())); + + assertEquals(2, result.size()); + assertEquals("key-1", result.getFirst() + .getName()); + assertEquals("key-2", result.get(1) + .getName()); + } + + @Test + void testMultipleKeysForSameServiceAreMappedCorrectly() { + UUID siGuid = UUID.randomUUID(); + UUID keyGuid1 = UUID.randomUUID(); + UUID keyGuid2 = UUID.randomUUID(); + + String responseJson = buildMultiKeyResponse(List.of(keyGuid1, keyGuid2), siGuid, List.of("key-a", "key-b"), "shared-service"); + stubWebClientToReturn(responseJson); + + List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, + List.of(siGuid.toString())); + + assertEquals(2, result.size()); + assertEquals("key-a", result.getFirst() + .getName()); + assertEquals("key-b", result.get(1) + .getName()); + // Both keys should reference the same service instance + assertEquals("shared-service", result.getFirst() + .getServiceInstance() + .getName()); + assertEquals("shared-service", result.get(1) + .getServiceInstance() + .getName()); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnEmptyPage() { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturn(String responseJson) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responseJson)); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnSequentially(String... responses) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + + if (responses.length == 1) { + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0])); + } else { + @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; + for (int i = 1; i < responses.length; i++) { + remaining[i - 1] = Mono.just(responses[i]); + } + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0]), remaining); + } + } + + private DeployedMtaService buildManagedService(String name, UUID guid) { + return ImmutableDeployedMtaService.builder() + .name(name) + .type(ServiceInstanceType.MANAGED) + .metadata(ImmutableCloudMetadata.builder() + .guid(guid) + .build()) + .build(); + } + + private List generateRandomGuids(int count) { + List guids = new ArrayList<>(); + for (int i = 0; i < count; i++) { + guids.add(UUID.randomUUID() + .toString()); + } + return guids; + } + + private String buildServiceKeysResponse(UUID serviceKeyGuid, UUID serviceInstanceGuid, String keyName, String serviceName) { + return buildServiceKeysResponseWithPagination(serviceKeyGuid, serviceInstanceGuid, keyName, serviceName, null); + } + + private String buildServiceKeysResponseWithPagination(UUID serviceKeyGuid, UUID serviceInstanceGuid, String keyName, String serviceName, + String nextPageHref) { + String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; + return "{" + "\"resources\":[" + " {" + " \"guid\":\"" + serviceKeyGuid + "\"," + " \"name\":\"" + keyName + "\"," + + " \"type\":\"key\"," + " \"created_at\":\"2024-01-01T00:00:00Z\"," + " \"updated_at\":\"2024-01-01T00:00:00Z\"," + + " \"metadata\":{\"labels\":{},\"annotations\":{}}," + " \"relationships\":{" + + " \"service_instance\":{\"data\":{\"guid\":\"" + serviceInstanceGuid + "\"}}" + " }" + " }" + "]," + "\"included\":{" + + " \"service_instances\":[" + " {" + " \"guid\":\"" + serviceInstanceGuid + "\"," + " \"name\":\"" + serviceName + + "\"," + " \"type\":\"managed\"," + " \"created_at\":\"2024-01-01T00:00:00Z\"," + + " \"updated_at\":\"2024-01-01T00:00:00Z\"," + " \"metadata\":{\"labels\":{},\"annotations\":{}}" + " }" + " ]" + + "}," + "\"pagination\":{\"next\":" + nextPage + "}" + "}"; + } + + private String buildMultiKeyResponse(List keyGuids, UUID serviceInstanceGuid, List keyNames, String serviceName) { + StringBuilder resources = new StringBuilder(); + for (int i = 0; i < keyGuids.size(); i++) { + if (i > 0) { + resources.append(","); + } + resources.append("{") + .append("\"guid\":\"") + .append(keyGuids.get(i)) + .append("\",") + .append("\"name\":\"") + .append(keyNames.get(i)) + .append("\",") + .append("\"type\":\"key\",") + .append("\"created_at\":\"2024-01-01T00:00:00Z\",") + .append("\"updated_at\":\"2024-01-01T00:00:00Z\",") + .append("\"metadata\":{\"labels\":{},\"annotations\":{}},") + .append("\"relationships\":{") + .append(" \"service_instance\":{\"data\":{\"guid\":\"") + .append(serviceInstanceGuid) + .append("\"}}") + .append("}") + .append("}"); + } + + return "{" + "\"resources\":[" + resources + "]," + "\"included\":{" + " \"service_instances\":[" + " {" + " \"guid\":\"" + + serviceInstanceGuid + "\"," + " \"name\":\"" + serviceName + "\"," + " \"type\":\"managed\"," + + " \"created_at\":\"2024-01-01T00:00:00Z\"," + " \"updated_at\":\"2024-01-01T00:00:00Z\"," + + " \"metadata\":{\"labels\":{},\"annotations\":{}}" + " }" + " ]" + "}," + "\"pagination\":{\"next\":null}" + "}"; + } +} + diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetterTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetterTest.java new file mode 100644 index 0000000000..db84a62158 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetterTest.java @@ -0,0 +1,410 @@ +package org.cloudfoundry.multiapps.controller.core.cf.clients; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ServiceRouteBinding; +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ServiceInstanceRoutesGetterTest { + + private static final String CORRELATION_ID = "test-correlation-id"; + + @Mock + private WebClientFactory webClientFactory; + @Mock + private ApplicationConfiguration applicationConfiguration; + @Mock + private WebClient webClient; + + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); + @SuppressWarnings("rawtypes") + private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); + @Mock + private WebClient.ResponseSpec responseSpec; + + private ServiceInstanceRoutesGetter client; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + Mockito.when(applicationConfiguration.getVersion()) + .thenReturn("1.0.0"); + Mockito.when(webClientFactory.getWebClient(Mockito.any(CloudCredentials.class))) + .thenReturn(webClient); + + client = new ServiceInstanceRoutesGetter(applicationConfiguration, webClientFactory, new CloudCredentials("user", "pass"), + CORRELATION_ID); + } + + @Test + void testGetServiceRouteBindingsWithEmptyGuidsReturnsEmptyList() { + List result = client.getServiceRouteBindings(Collections.emptyList()); + assertTrue(result.isEmpty()); + Mockito.verifyNoInteractions(webClient); + } + + @Test + void testGetServiceRouteBindingsBuildsCorrectUri() { + stubWebClientToReturnEmptyPage(); + + String routeGuid = UUID.randomUUID() + .toString(); + client.getServiceRouteBindings(List.of(routeGuid)); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.startsWith("/v3/service_route_bindings?"), + "URI should start with the service route bindings base path"); + assertTrue(capturedUri.contains("route_guids=" + routeGuid), + "URI should contain the route_guids parameter with the provided guid"); + } + + @Test + void testGetServiceRouteBindingsJoinsMultipleGuidsWithComma() { + stubWebClientToReturnEmptyPage(); + + String routeGuid1 = UUID.randomUUID() + .toString(); + String routeGuid2 = UUID.randomUUID() + .toString(); + client.getServiceRouteBindings(List.of(routeGuid1, routeGuid2)); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec) + .uri(uriCaptor.capture()); + + String capturedUri = uriCaptor.getValue(); + assertTrue(capturedUri.contains("route_guids=" + routeGuid1 + "," + routeGuid2), + "URI should contain both guids joined by comma"); + } + + @Test + void testGetServiceRouteBindingsReturnsMappedBindings() { + String routeGuid = UUID.randomUUID() + .toString(); + String serviceInstanceGuid = UUID.randomUUID() + .toString(); + + String responseJson = buildServiceRouteBindingsResponse(routeGuid, serviceInstanceGuid); + stubWebClientToReturn(responseJson); + + List result = client.getServiceRouteBindings(List.of(routeGuid)); + + assertEquals(1, result.size()); + ServiceRouteBinding binding = result.getFirst(); + assertEquals(routeGuid, binding.getRouteId()); + assertEquals(serviceInstanceGuid, binding.getServiceInstanceId()); + } + + @Test + void testGetServiceRouteBindingsWithEmptyResponseReturnsEmptyList() { + stubWebClientToReturnEmptyPage(); + + String routeGuid = UUID.randomUUID() + .toString(); + List result = client.getServiceRouteBindings(List.of(routeGuid)); + assertTrue(result.isEmpty()); + } + + @Test + void testGetServiceRouteBindingsReturnsMultipleBindings() { + String routeGuid1 = UUID.randomUUID() + .toString(); + String routeGuid2 = UUID.randomUUID() + .toString(); + String serviceInstanceGuid1 = UUID.randomUUID() + .toString(); + String serviceInstanceGuid2 = UUID.randomUUID() + .toString(); + + String responseJson = buildMultipleServiceRouteBindingsResponse( + List.of(routeGuid1, routeGuid2), + List.of(serviceInstanceGuid1, serviceInstanceGuid2)); + stubWebClientToReturn(responseJson); + + List result = client.getServiceRouteBindings(List.of(routeGuid1, routeGuid2)); + + assertEquals(2, result.size()); + assertEquals(routeGuid1, result.getFirst() + .getRouteId()); + assertEquals(serviceInstanceGuid1, result.getFirst() + .getServiceInstanceId()); + assertEquals(routeGuid2, result.get(1) + .getRouteId()); + assertEquals(serviceInstanceGuid2, result.get(1) + .getServiceInstanceId()); + } + + @Test + void testGetServiceRouteBindingsMultipleBindingsForSameRoute() { + String routeGuid = UUID.randomUUID() + .toString(); + String serviceInstanceGuid1 = UUID.randomUUID() + .toString(); + String serviceInstanceGuid2 = UUID.randomUUID() + .toString(); + + String responseJson = buildMultipleServiceRouteBindingsResponse( + List.of(routeGuid, routeGuid), + List.of(serviceInstanceGuid1, serviceInstanceGuid2)); + stubWebClientToReturn(responseJson); + + List result = client.getServiceRouteBindings(List.of(routeGuid)); + + assertEquals(2, result.size()); + assertEquals(routeGuid, result.getFirst() + .getRouteId()); + assertEquals(serviceInstanceGuid1, result.getFirst() + .getServiceInstanceId()); + assertEquals(routeGuid, result.get(1) + .getRouteId()); + assertEquals(serviceInstanceGuid2, result.get(1) + .getServiceInstanceId()); + } + + @Test + void testGetServiceRouteBindingsPaginatedResponseFollowsAllPages() { + String routeGuid = UUID.randomUUID() + .toString(); + String serviceInstanceGuid1 = UUID.randomUUID() + .toString(); + String serviceInstanceGuid2 = UUID.randomUUID() + .toString(); + + String page1Json = buildServiceRouteBindingsResponseWithPagination(routeGuid, serviceInstanceGuid1, + "/v3/service_route_bindings?page=2"); + String page2Json = buildServiceRouteBindingsResponse(routeGuid, serviceInstanceGuid2); + + stubWebClientToReturnSequentially(page1Json, page2Json); + + List result = client.getServiceRouteBindings(List.of(routeGuid)); + + assertEquals(2, result.size()); + assertEquals(serviceInstanceGuid1, result.getFirst() + .getServiceInstanceId()); + assertEquals(serviceInstanceGuid2, result.get(1) + .getServiceInstanceId()); + } + + @Test + void testBatchingTriggeredWithManyGuids() { + stubWebClientToReturnEmptyPage(); + + List manyGuids = generateRandomGuids(200); + + client.getServiceRouteBindings(manyGuids); + + Mockito.verify(webClient, Mockito.atLeast(2)) + .get(); + } + + @Test + void testSingleBatchWithFewGuids() { + stubWebClientToReturnEmptyPage(); + + List fewGuids = generateRandomGuids(3); + + client.getServiceRouteBindings(fewGuids); + + Mockito.verify(webClient, Mockito.times(1)) + .get(); + } + + @Test + void testBatchedUrisNeverExceedMaxLength() { + stubWebClientToReturnEmptyPage(); + + List manyGuids = generateRandomGuids(800); + client.getServiceRouteBindings(manyGuids); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec, Mockito.atLeastOnce()) + .uri(uriCaptor.capture()); + + for (String uri : uriCaptor.getAllValues()) { + assertTrue(uri.length() <= CustomControllerClient.MAX_URI_QUERY_LENGTH, + "URI length " + uri.length() + " exceeds MAX_URI_QUERY_LENGTH " + + CustomControllerClient.MAX_URI_QUERY_LENGTH); + } + } + + @Test + void testBatchedRequestsContainAllGuidsInOrder() { + stubWebClientToReturnEmptyPage(); + + List manyGuids = generateRandomGuids(200); + client.getServiceRouteBindings(manyGuids); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(requestHeadersUriSpec, Mockito.atLeastOnce()) + .uri(uriCaptor.capture()); + + List allCapturedGuids = new ArrayList<>(); + for (String uri : uriCaptor.getAllValues()) { + int idx = uri.indexOf("route_guids="); + assertTrue(idx >= 0, "URI should contain route_guids="); + String guidsStr = uri.substring(idx + "route_guids=".length()); + String[] guids = guidsStr.split(","); + Collections.addAll(allCapturedGuids, guids); + } + + assertEquals(manyGuids.size(), allCapturedGuids.size(), "All guids must be sent across batched requests"); + assertEquals(manyGuids, allCapturedGuids, "Guids must appear in original order across batches"); + } + + @Test + void testBatchedRequestsAggregateResultsFromAllBatches() { + String routeGuid1 = UUID.randomUUID() + .toString(); + String serviceInstanceGuid1 = UUID.randomUUID() + .toString(); + + String response = buildServiceRouteBindingsResponse(routeGuid1, serviceInstanceGuid1); + stubWebClientToReturn(response); + + List result = client.getServiceRouteBindings(List.of(routeGuid1)); + + assertEquals(1, result.size()); + assertEquals(routeGuid1, result.getFirst() + .getRouteId()); + assertEquals(serviceInstanceGuid1, result.getFirst() + .getServiceInstanceId()); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnEmptyPage() { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturn(String responseJson) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responseJson)); + } + + @SuppressWarnings("unchecked") + private void stubWebClientToReturnSequentially(String... responses) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + + if (responses.length == 1) { + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0])); + } else { + @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; + for (int i = 1; i < responses.length; i++) { + remaining[i - 1] = Mono.just(responses[i]); + } + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0]), remaining); + } + } + + private List generateRandomGuids(int count) { + List guids = new ArrayList<>(); + for (int i = 0; i < count; i++) { + guids.add(UUID.randomUUID() + .toString()); + } + return guids; + } + + private String buildServiceRouteBindingsResponse(String routeGuid, String serviceInstanceGuid) { + return buildServiceRouteBindingsResponseWithPagination(routeGuid, serviceInstanceGuid, null); + } + + private String buildServiceRouteBindingsResponseWithPagination(String routeGuid, String serviceInstanceGuid, String nextPageHref) { + String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; + return "{" + + "\"resources\":[" + + " {" + + " \"guid\":\"" + UUID.randomUUID() + "\"," + + " \"created_at\":\"2024-01-01T00:00:00Z\"," + + " \"updated_at\":\"2024-01-01T00:00:00Z\"," + + " \"relationships\":{" + + " \"route\":{\"data\":{\"guid\":\"" + routeGuid + "\"}}," + + " \"service_instance\":{\"data\":{\"guid\":\"" + serviceInstanceGuid + "\"}}" + + " }" + + " }" + + "]," + + "\"pagination\":{\"next\":" + nextPage + "}" + + "}"; + } + + private String buildMultipleServiceRouteBindingsResponse(List routeGuids, List serviceInstanceGuids) { + StringBuilder resources = new StringBuilder(); + for (int i = 0; i < routeGuids.size(); i++) { + if (i > 0) { + resources.append(","); + } + resources.append("{") + .append("\"guid\":\"") + .append(UUID.randomUUID()) + .append("\",") + .append("\"created_at\":\"2024-01-01T00:00:00Z\",") + .append("\"updated_at\":\"2024-01-01T00:00:00Z\",") + .append("\"relationships\":{") + .append(" \"route\":{\"data\":{\"guid\":\"") + .append(routeGuids.get(i)) + .append("\"}},") + .append(" \"service_instance\":{\"data\":{\"guid\":\"") + .append(serviceInstanceGuids.get(i)) + .append("\"}}") + .append("}") + .append("}"); + } + + return "{" + + "\"resources\":[" + resources + "]," + + "\"pagination\":{\"next\":null}" + + "}"; + } +} + + + + From 769c5fa1ce949383849a4422ff6560e75a431336 Mon Sep 17 00:00:00 2001 From: theghost5800 Date: Mon, 6 Apr 2026 13:33:13 +0300 Subject: [PATCH 2/2] Fix comments 1 --- .../cf/clients/CustomControllerClient.java | 86 ++++-- .../cf/clients/CustomServiceKeysClient.java | 7 +- .../clients/ServiceInstanceRoutesGetter.java | 3 +- ...ppBoundServiceInstanceNamesGetterTest.java | 181 ++++--------- .../clients/CFOptimizedEventGetterTest.java | 95 +------ .../core/cf/clients/CfRolesGetterTest.java | 97 +------ .../CustomControllerClientBaseTest.java | 97 +++++++ .../clients/CustomServiceKeysClientTest.java | 255 ++++++------------ .../ServiceInstanceRoutesGetterTest.java | 175 ++++-------- 9 files changed, 366 insertions(+), 630 deletions(-) create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClientBaseTest.java diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClient.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClient.java index ec29bc3a2a..aca1514279 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClient.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClient.java @@ -36,28 +36,71 @@ protected CustomControllerClient(ApplicationConfiguration configuration, WebClie this.headerConfiguration = new CloudControllerHeaderConfiguration(configuration.getVersion()); } - protected List getListOfResources(ResourcesResponseMapper responseMapper, String uri) { - PaginationV3 pagination = addPageOfResources(uri, responseMapper); + protected List getListOfResources(ResourcesResponseMapper responseMapper, String uri, Object... urlVariables) { + PaginationV3 pagination = addPageOfResources(uri, responseMapper, urlVariables); while (!StringUtils.isEmpty(pagination.getNextUri())) { pagination = addPageOfResources(pagination.getNextUri(), responseMapper); } return responseMapper.getMappedResources(); } - protected List getListOfResourcesInBatches(ResourcesResponseMapper responseMapper, String uriPrefix, String batchParamPrefix, - List batchValues) { - int fixedUriLength = uriPrefix.length() + batchParamPrefix.length(); - List> batches = splitIntoBatches(batchValues, fixedUriLength); + private PaginationV3 addPageOfResources(String uri, ResourcesResponseMapper responseMapper, Object... urlVariables) { + String responseString = webClient.get() + .uri(uri, urlVariables) + .headers(httpHeaders -> httpHeaders.addAll(generateRequestHeaders())) + .retrieve() + .bodyToMono(String.class) + .block(); + Map responseMap = JsonUtil.convertJsonToMap(responseString); + responseMapper.addResources(responseMap); + return PaginationV3.fromResponse(responseMap); + } + + private MultiValueMap generateRequestHeaders() { + var result = new LinkedMultiValueMap(); + headerConfiguration.generateHeaders(correlationId) + .forEach(result::add); + return result; + } + + protected List getListOfResourcesInBatches(ResourcesResponseMapper responseMapper, String uriPrefix, + List batchValues, Object... urlVariables) { + int expandedPrefixLength = calculateExpandedLength(uriPrefix, urlVariables); + List> batches = splitIntoBatches(batchValues, expandedPrefixLength); return batches.stream() - .map(batch -> { - String uri = uriPrefix + batchParamPrefix + String.join(",", batch); - return getListOfResources(responseMapper, uri); - }) + .map(batch -> + getListOfResources(responseMapper, uriPrefix, batch, urlVariables) + ) .flatMap(List::stream) .toList(); } - List> splitIntoBatches(List values, int fixedUriLength) { + private int calculateExpandedLength(String uriPrefix, Object[] urlVariables) { + if (urlVariables == null || urlVariables.length == 0) { + return uriPrefix.length(); + } + int urlVariablesLength = 0; + for (Object variable : urlVariables) { + urlVariablesLength += String.valueOf(variable) + .length(); + } + int placeholdersLength = calculatePlaceholdersLength(uriPrefix); + return (uriPrefix.length() - placeholdersLength) + urlVariablesLength; + } + + private int calculatePlaceholdersLength(String uriPrefix) { + int length = 0; + int searchFrom = 0; + int start; + int end; + while ((start = uriPrefix.indexOf('{', searchFrom)) >= 0 && (end = uriPrefix.indexOf('}', start)) >= 0) { + length += end - start + 1; + searchFrom = end + 1; + } + return length; + } + + protected List> splitIntoBatches(List values, int fixedUriLength) { int maxBatchLength = Math.max(1, MAX_URI_QUERY_LENGTH - fixedUriLength); List> batches = new ArrayList<>(); List currentBatch = new ArrayList<>(); @@ -82,23 +125,10 @@ List> splitIntoBatches(List values, int fixedUriLength) { return batches; } - private PaginationV3 addPageOfResources(String uri, ResourcesResponseMapper responseMapper) { - String responseString = webClient.get() - .uri(uri) - .headers(httpHeaders -> httpHeaders.addAll(generateRequestHeaders())) - .retrieve() - .bodyToMono(String.class) - .block(); - Map responseMap = JsonUtil.convertJsonToMap(responseString); - responseMapper.addResources(responseMap); - return PaginationV3.fromResponse(responseMap); - } - - private MultiValueMap generateRequestHeaders() { - var result = new LinkedMultiValueMap(); - headerConfiguration.generateHeaders(correlationId) - .forEach(result::add); - return result; + private List getListOfResources(ResourcesResponseMapper responseMapper, String uriPrefix, + List batch, Object... urlVariables) { + String uri = uriPrefix + String.join(",", batch); + return getListOfResources(responseMapper, uri, urlVariables); } public static abstract class ResourcesResponseMapper { diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java index 6a62a8e372..8f4bfdd489 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java @@ -88,12 +88,11 @@ private List extractManagedServiceGuids(List service } private List getServiceKeysByMetadataInternal(String labelSelector, List guids) { - String expandedUriPrefix = SERVICE_KEYS_BY_METADATA_SELECTOR_URI.replace("{value}", labelSelector) - + INCLUDE_SERVICE_INSTANCE_RESOURCES_PARAM; + String expandedUriPrefix = + SERVICE_KEYS_BY_METADATA_SELECTOR_URI + INCLUDE_SERVICE_INSTANCE_RESOURCES_PARAM + SERVICE_INSTANCE_GUIDS_PARAM_PREFIX; return getListOfResourcesInBatches(new ServiceKeysResponseMapper(), expandedUriPrefix, - SERVICE_INSTANCE_GUIDS_PARAM_PREFIX, - guids); + guids, labelSelector); } private List getManagedServices(List services) { diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetter.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetter.java index a665ce12ec..5a44b6fc23 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetter.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetter.java @@ -25,8 +25,7 @@ public List getServiceRouteBindings(List routeGuids private List doGetServiceRouteBindings(List routeGuids) { return getListOfResourcesInBatches(new ServiceRouteBindingsResponseMapper(), - SERVICE_ROUTE_BINDINGS_URI_PREFIX, - ROUTE_GUIDS_PARAM_PREFIX, + SERVICE_ROUTE_BINDINGS_URI_PREFIX + ROUTE_GUIDS_PARAM_PREFIX, routeGuids); } diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/AppBoundServiceInstanceNamesGetterTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/AppBoundServiceInstanceNamesGetterTest.java index 882bf947e7..180ac8a851 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/AppBoundServiceInstanceNamesGetterTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/AppBoundServiceInstanceNamesGetterTest.java @@ -2,40 +2,21 @@ import java.util.List; import java.util.UUID; -import java.util.function.Consumer; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; -import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -class AppBoundServiceInstanceNamesGetterTest { +class AppBoundServiceInstanceNamesGetterTest extends CustomControllerClientBaseTest { private static final String CORRELATION_ID = "test-correlation-id"; - @Mock - private WebClientFactory webClientFactory; - @Mock - private ApplicationConfiguration applicationConfiguration; - @Mock - private WebClient webClient; - - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); - @Mock - private WebClient.ResponseSpec responseSpec; - private AppBoundServiceInstanceNamesGetter client; @BeforeEach @@ -53,7 +34,7 @@ void setUp() throws Exception { @Test void testGetServiceInstanceNamesBoundToAppBuildsCorrectUri() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID appGuid = UUID.randomUUID(); client.getServiceInstanceNamesBoundToApp(appGuid); @@ -73,7 +54,7 @@ void testGetServiceInstanceNamesBoundToAppBuildsCorrectUri() { @Test void testGetServiceInstanceNamesBoundToAppWithEmptyResponseReturnsEmptyList() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID appGuid = UUID.randomUUID(); List result = client.getServiceInstanceNamesBoundToApp(appGuid); @@ -84,8 +65,8 @@ void testGetServiceInstanceNamesBoundToAppWithEmptyResponseReturnsEmptyList() { @Test void testGetServiceInstanceNamesBoundToAppReturnsSingleServiceName() { String serviceName = "my-database"; - String responseJson = buildResponseWithIncludedServiceInstances(List.of(serviceName)); - stubWebClientToReturn(responseJson); + String responseJson = buildResponseWithIncludedServiceInstances(List.of(serviceName), null); + stubWebClientToReturnResponse(responseJson); UUID appGuid = UUID.randomUUID(); List result = client.getServiceInstanceNamesBoundToApp(appGuid); @@ -97,8 +78,8 @@ void testGetServiceInstanceNamesBoundToAppReturnsSingleServiceName() { @Test void testGetServiceInstanceNamesBoundToAppReturnsMultipleServiceNames() { List serviceNames = List.of("db-service", "cache-service", "queue-service"); - String responseJson = buildResponseWithIncludedServiceInstances(serviceNames); - stubWebClientToReturn(responseJson); + String responseJson = buildResponseWithIncludedServiceInstances(serviceNames, null); + stubWebClientToReturnResponse(responseJson); UUID appGuid = UUID.randomUUID(); List result = client.getServiceInstanceNamesBoundToApp(appGuid); @@ -112,8 +93,8 @@ void testGetServiceInstanceNamesBoundToAppReturnsMultipleServiceNames() { @Test void testGetServiceInstanceNamesBoundToAppDeduplicatesServiceNames() { List serviceNames = List.of("my-service", "my-service", "other-service"); - String responseJson = buildResponseWithIncludedServiceInstances(serviceNames); - stubWebClientToReturn(responseJson); + String responseJson = buildResponseWithIncludedServiceInstances(serviceNames, null); + stubWebClientToReturnResponse(responseJson); UUID appGuid = UUID.randomUUID(); List result = client.getServiceInstanceNamesBoundToApp(appGuid); @@ -127,7 +108,7 @@ void testGetServiceInstanceNamesBoundToAppDeduplicatesServiceNames() { void testGetServiceInstanceNamesBoundToAppWithNoIncludedResourcesReturnsEmptyList() { // Response has resources but no "included" section String responseJson = "{\"resources\":[{\"guid\":\"" + UUID.randomUUID() + "\"}],\"pagination\":{\"next\":null}}"; - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); UUID appGuid = UUID.randomUUID(); List result = client.getServiceInstanceNamesBoundToApp(appGuid); @@ -137,11 +118,11 @@ void testGetServiceInstanceNamesBoundToAppWithNoIncludedResourcesReturnsEmptyLis @Test void testGetServiceInstanceNamesBoundToAppPaginatedResponseFollowsAllPages() { - String page1Json = buildResponseWithIncludedServiceInstancesAndPagination( + String page1Json = buildResponseWithIncludedServiceInstances( List.of("svc-page1"), "/v3/service_credential_bindings?page=2"); - String page2Json = buildResponseWithIncludedServiceInstances(List.of("svc-page2")); + String page2Json = buildResponseWithIncludedServiceInstances(List.of("svc-page2"), null); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); UUID appGuid = UUID.randomUUID(); List result = client.getServiceInstanceNamesBoundToApp(appGuid); @@ -153,7 +134,7 @@ void testGetServiceInstanceNamesBoundToAppPaginatedResponseFollowsAllPages() { @Test void testGetServiceInstanceNamesBoundToAppMakesExactlyOneHttpCallForSinglePage() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID appGuid = UUID.randomUUID(); client.getServiceInstanceNamesBoundToApp(appGuid); @@ -164,11 +145,11 @@ void testGetServiceInstanceNamesBoundToAppMakesExactlyOneHttpCallForSinglePage() @Test void testGetServiceInstanceNamesBoundToAppMakesTwoHttpCallsForTwoPages() { - String page1Json = buildResponseWithIncludedServiceInstancesAndPagination( + String page1Json = buildResponseWithIncludedServiceInstances( List.of("svc-1"), "/v3/service_credential_bindings?page=2"); - String page2Json = buildResponseWithIncludedServiceInstances(List.of("svc-2")); + String page2Json = buildResponseWithIncludedServiceInstances(List.of("svc-2"), null); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); UUID appGuid = UUID.randomUUID(); client.getServiceInstanceNamesBoundToApp(appGuid); @@ -183,7 +164,7 @@ void testGetServiceInstanceNamesBoundToAppWithResourcesButEmptyIncludedServiceIn String responseJson = "{\"resources\":[{\"guid\":\"" + UUID.randomUUID() + "\"}]," + "\"included\":{\"service_instances\":[]}," + "\"pagination\":{\"next\":null}}"; - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); UUID appGuid = UUID.randomUUID(); List result = client.getServiceInstanceNamesBoundToApp(appGuid); @@ -194,11 +175,11 @@ void testGetServiceInstanceNamesBoundToAppWithResourcesButEmptyIncludedServiceIn @Test void testGetServiceInstanceNamesBoundToAppDeduplicatesAcrossPages() { // Same service name appears on both pages - String page1Json = buildResponseWithIncludedServiceInstancesAndPagination( + String page1Json = buildResponseWithIncludedServiceInstances( List.of("shared-service"), "/v3/service_credential_bindings?page=2"); - String page2Json = buildResponseWithIncludedServiceInstances(List.of("shared-service")); + String page2Json = buildResponseWithIncludedServiceInstances(List.of("shared-service"), null); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); UUID appGuid = UUID.randomUUID(); List result = client.getServiceInstanceNamesBoundToApp(appGuid); @@ -207,102 +188,58 @@ void testGetServiceInstanceNamesBoundToAppDeduplicatesAcrossPages() { assertEquals("shared-service", result.getFirst()); } - @SuppressWarnings("unchecked") - private void stubWebClientToReturnEmptyPage() { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); - } + private String buildResponseWithIncludedServiceInstances(List serviceNames, String nextPageHref) { + List serviceInstanceGuids = serviceNames.stream() + .map(service -> UUID.randomUUID()) + .toList(); + String bindingResourcesJson = buildBindingResourcesJson(serviceInstanceGuids); + String serviceInstancesJson = buildServiceInstanceResourcesJson(serviceNames, serviceInstanceGuids); + String paginationJson = buildPaginationJson(nextPageHref); - @SuppressWarnings("unchecked") - private void stubWebClientToReturn(String responseJson) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responseJson)); + return "{\"resources\":[" + bindingResourcesJson + "]," + + "\"included\":{\"service_instances\":[" + serviceInstancesJson + "]}," + + "\"pagination\":" + paginationJson + "}"; } - @SuppressWarnings("unchecked") - private void stubWebClientToReturnSequentially(String... responses) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - - if (responses.length == 1) { - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0])); - } else { - @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; - for (int i = 1; i < responses.length; i++) { - remaining[i - 1] = Mono.just(responses[i]); + private String buildBindingResourcesJson(List serviceInstanceGuids) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < serviceInstanceGuids.size(); i++) { + if (i > 0) { + sb.append(","); } - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0]), remaining); + sb.append(buildSingleBindingResourceJson(serviceInstanceGuids.get(i))); } + return sb.toString(); } - private String buildResponseWithIncludedServiceInstances(List serviceNames) { - return buildResponseWithIncludedServiceInstancesAndPagination(serviceNames, null); + private String buildSingleBindingResourceJson(UUID serviceInstanceGuid) { + return "{\"guid\":\"" + UUID.randomUUID() + "\"," + + "\"type\":\"app\"," + + "\"relationships\":{" + + "\"service_instance\":{\"data\":{\"guid\":\"" + serviceInstanceGuid + "\"}}" + + "}}"; } - private String buildResponseWithIncludedServiceInstancesAndPagination(List serviceNames, String nextPageHref) { - String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; - - StringBuilder bindingResources = new StringBuilder(); - StringBuilder serviceInstanceResources = new StringBuilder(); - + private String buildServiceInstanceResourcesJson(List serviceNames, List serviceInstanceGuids) { + StringBuilder sb = new StringBuilder(); for (int i = 0; i < serviceNames.size(); i++) { - UUID serviceInstanceGuid = UUID.randomUUID(); - if (i > 0) { - bindingResources.append(","); - serviceInstanceResources.append(","); + sb.append(","); } - - bindingResources.append("{") - .append("\"guid\":\"") - .append(UUID.randomUUID()) - .append("\",") - .append("\"type\":\"app\",") - .append("\"relationships\":{") - .append(" \"service_instance\":{\"data\":{\"guid\":\"") - .append(serviceInstanceGuid) - .append("\"}}") - .append("}") - .append("}"); - - serviceInstanceResources.append("{") - .append("\"guid\":\"") - .append(serviceInstanceGuid) - .append("\",") - .append("\"name\":\"") - .append(serviceNames.get(i)) - .append("\",") - .append("\"type\":\"managed\"") - .append("}"); + sb.append(buildSingleServiceInstanceJson(serviceNames.get(i), serviceInstanceGuids.get(i))); } + return sb.toString(); + } - return "{\"resources\":[" + bindingResources + "]," - + "\"included\":{\"service_instances\":[" + serviceInstanceResources + "]}," - + "\"pagination\":{\"next\":" + nextPage + "}}"; + private String buildSingleServiceInstanceJson(String serviceName, UUID serviceInstanceGuid) { + return "{\"guid\":\"" + serviceInstanceGuid + "\"," + + "\"name\":\"" + serviceName + "\"," + + "\"type\":\"managed\"}"; + } + + private String buildPaginationJson(String nextPageHref) { + String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; + return "{\"next\":" + nextPage + "}"; } } diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CFOptimizedEventGetterTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CFOptimizedEventGetterTest.java index 79faf39c7b..35ca4f393d 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CFOptimizedEventGetterTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CFOptimizedEventGetterTest.java @@ -2,41 +2,22 @@ import java.util.List; import java.util.UUID; -import java.util.function.Consumer; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; -import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -class CFOptimizedEventGetterTest { +class CFOptimizedEventGetterTest extends CustomControllerClientBaseTest { private static final String EVENT_TYPE = "audit.app.update"; private static final String TIMESTAMP = "2024-06-01T00:00:00Z"; - @Mock - private WebClientFactory webClientFactory; - @Mock - private ApplicationConfiguration applicationConfiguration; - @Mock - private WebClient webClient; - - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); - @Mock - private WebClient.ResponseSpec responseSpec; - private CFOptimizedEventGetter client; @BeforeEach @@ -53,7 +34,7 @@ void setUp() throws Exception { @Test void testFindEventsBuildsCorrectUri() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); client.findEvents(EVENT_TYPE, TIMESTAMP); @@ -70,7 +51,7 @@ void testFindEventsBuildsCorrectUri() { @Test void testFindEventsWithEmptyResponseReturnsEmptyList() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); List result = client.findEvents(EVENT_TYPE, TIMESTAMP); @@ -82,7 +63,7 @@ void testFindEventsReturnsSingleSpaceId() { String spaceGuid = UUID.randomUUID() .toString(); String responseJson = buildAuditEventsResponse(List.of(spaceGuid)); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); List result = client.findEvents(EVENT_TYPE, TIMESTAMP); @@ -99,7 +80,7 @@ void testFindEventsReturnsMultipleSpaceIds() { String spaceGuid3 = UUID.randomUUID() .toString(); String responseJson = buildAuditEventsResponse(List.of(spaceGuid1, spaceGuid2, spaceGuid3)); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); List result = client.findEvents(EVENT_TYPE, TIMESTAMP); @@ -115,7 +96,7 @@ void testFindEventsFiltersNullSpaceGuids() { .toString(); // Build a response with one event having a valid space guid and one with null guid String responseJson = buildAuditEventsResponseWithNullSpaceGuid(spaceGuid); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); List result = client.findEvents(EVENT_TYPE, TIMESTAMP); @@ -133,7 +114,7 @@ void testFindEventsPaginatedResponseFollowsAllPages() { String page1Json = buildAuditEventsResponseWithPagination(List.of(spaceGuid1), "/v3/audit_events?page=2"); String page2Json = buildAuditEventsResponse(List.of(spaceGuid2)); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); List result = client.findEvents(EVENT_TYPE, TIMESTAMP); @@ -144,7 +125,7 @@ void testFindEventsPaginatedResponseFollowsAllPages() { @Test void testFindEventsMakesExactlyOneHttpCallForSinglePage() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); client.findEvents(EVENT_TYPE, TIMESTAMP); @@ -160,7 +141,7 @@ void testFindEventsMakesTwoHttpCallsForTwoPages() { String page1Json = buildAuditEventsResponseWithPagination(List.of(spaceGuid), "/v3/audit_events?page=2"); String page2Json = buildAuditEventsResponse(List.of(spaceGuid)); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); client.findEvents(EVENT_TYPE, TIMESTAMP); @@ -170,7 +151,7 @@ void testFindEventsMakesTwoHttpCallsForTwoPages() { @Test void testFindEventsWithDifferentEventType() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String customType = "audit.app.delete"; client.findEvents(customType, TIMESTAMP); @@ -185,7 +166,7 @@ void testFindEventsWithDifferentEventType() { @Test void testFindEventsWithDifferentTimestamp() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String customTimestamp = "2025-12-31T23:59:59Z"; client.findEvents(EVENT_TYPE, customTimestamp); @@ -204,65 +185,13 @@ void testFindEventsDuplicateSpaceIdsArePreserved() { .toString(); // Same space guid appearing in multiple events String responseJson = buildAuditEventsResponse(List.of(spaceGuid, spaceGuid, spaceGuid)); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); List result = client.findEvents(EVENT_TYPE, TIMESTAMP); assertEquals(3, result.size(), "Duplicate space IDs should be preserved since the mapper does not deduplicate"); } - @SuppressWarnings("unchecked") - private void stubWebClientToReturnEmptyPage() { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); - } - - @SuppressWarnings("unchecked") - private void stubWebClientToReturn(String responseJson) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responseJson)); - } - - @SuppressWarnings("unchecked") - private void stubWebClientToReturnSequentially(String... responses) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - - if (responses.length == 1) { - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0])); - } else { - @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; - for (int i = 1; i < responses.length; i++) { - remaining[i - 1] = Mono.just(responses[i]); - } - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0]), remaining); - } - } - private String buildAuditEventsResponse(List spaceGuids) { return buildAuditEventsResponseWithPagination(spaceGuids, null); } diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CfRolesGetterTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CfRolesGetterTest.java index 7468368e73..68c454d34c 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CfRolesGetterTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CfRolesGetterTest.java @@ -3,39 +3,20 @@ import java.util.Arrays; import java.util.Set; import java.util.UUID; -import java.util.function.Consumer; import java.util.stream.Collectors; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; -import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -class CfRolesGetterTest { - - @Mock - private WebClientFactory webClientFactory; - @Mock - private ApplicationConfiguration applicationConfiguration; - @Mock - private WebClient webClient; - - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); - @Mock - private WebClient.ResponseSpec responseSpec; +class CfRolesGetterTest extends CustomControllerClientBaseTest { private CfRolesGetter client; @@ -53,7 +34,7 @@ void setUp() throws Exception { @Test void testGetRolesBuildsCorrectUri() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -71,7 +52,7 @@ void testGetRolesBuildsCorrectUri() { @Test void testGetRolesUriContainsAllRoleTypes() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -90,7 +71,7 @@ void testGetRolesUriContainsAllRoleTypes() { @Test void testGetRolesWithEmptyResponseReturnsEmptySet() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -102,7 +83,7 @@ void testGetRolesWithEmptyResponseReturnsEmptySet() { @Test void testGetRolesReturnsSingleRole() { String responseJson = buildRolesResponse("space_developer"); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -115,7 +96,7 @@ void testGetRolesReturnsSingleRole() { @Test void testGetRolesReturnsMultipleRoles() { String responseJson = buildRolesResponse("space_developer", "space_manager", "space_auditor"); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -130,7 +111,7 @@ void testGetRolesReturnsMultipleRoles() { @Test void testGetRolesReturnsOrganizationRoles() { String responseJson = buildRolesResponse("organization_manager", "organization_auditor"); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -144,7 +125,7 @@ void testGetRolesReturnsOrganizationRoles() { @Test void testGetRolesDeduplicatesDuplicateRoles() { String responseJson = buildRolesResponse("space_developer", "space_developer"); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -159,7 +140,7 @@ void testGetRolesPaginatedResponseFollowsAllPages() { String page1Json = buildRolesResponseWithPagination(new String[] { "space_developer" }, "/v3/roles?page=2"); String page2Json = buildRolesResponse("space_manager"); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -172,7 +153,7 @@ void testGetRolesPaginatedResponseFollowsAllPages() { @Test void testGetRolesMakesExactlyOneHttpCallForSinglePage() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -187,7 +168,7 @@ void testGetRolesMakesTwoHttpCallsForTwoPages() { String page1Json = buildRolesResponseWithPagination(new String[] { "space_developer" }, "/v3/roles?page=2"); String page2Json = buildRolesResponse("space_auditor"); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -200,7 +181,7 @@ void testGetRolesMakesTwoHttpCallsForTwoPages() { @Test void testGetRolesReturnsAllSpaceRoles() { String responseJson = buildRolesResponse("space_developer", "space_manager", "space_auditor"); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -214,7 +195,7 @@ void testGetRolesReturnsAllSpaceRoles() { @Test void testGetRolesReturnsMixedSpaceAndOrgRoles() { String responseJson = buildRolesResponse("space_developer", "organization_manager", "organization_user"); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); UUID spaceGuid = UUID.randomUUID(); UUID userGuid = UUID.randomUUID(); @@ -226,58 +207,6 @@ void testGetRolesReturnsMixedSpaceAndOrgRoles() { assertTrue(result.contains(UserRole.ORGANIZATION_USER)); } - @SuppressWarnings("unchecked") - private void stubWebClientToReturnEmptyPage() { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); - } - - @SuppressWarnings("unchecked") - private void stubWebClientToReturn(String responseJson) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responseJson)); - } - - @SuppressWarnings("unchecked") - private void stubWebClientToReturnSequentially(String... responses) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - - if (responses.length == 1) { - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0])); - } else { - @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; - for (int i = 1; i < responses.length; i++) { - remaining[i - 1] = Mono.just(responses[i]); - } - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0]), remaining); - } - } - private String buildRolesResponse(String... roleTypes) { return buildRolesResponseWithPagination(roleTypes, null); } diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClientBaseTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClientBaseTest.java new file mode 100644 index 0000000000..87271c84e2 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomControllerClientBaseTest.java @@ -0,0 +1,97 @@ +package org.cloudfoundry.multiapps.controller.core.cf.clients; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +abstract class CustomControllerClientBaseTest { + + private static final Pattern URI_VARIABLE_PATTERN = Pattern.compile("\\{[^}]+}"); + + @Mock + protected WebClientFactory webClientFactory; + @Mock + protected ApplicationConfiguration applicationConfiguration; + @Mock + protected WebClient webClient; + + @SuppressWarnings("rawtypes") + protected final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); + @SuppressWarnings("rawtypes") + protected final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); + @Mock + protected WebClient.ResponseSpec responseSpec; + + protected final List capturedResolvedUris = new ArrayList<>(); + + @SuppressWarnings("unchecked") + protected void stubWebClientToReturnResponse(String... responses) { + Mockito.when(webClient.get()) + .thenReturn(requestHeadersUriSpec); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) + .thenAnswer(this::resolveUriTemplateAndReturn); + Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString(), Mockito. any())) + .thenAnswer(this::resolveUriTemplateAndReturn); + Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) + .thenReturn(requestHeadersSpec); + Mockito.when(requestHeadersSpec.retrieve()) + .thenReturn(responseSpec); + + if (responses.length == 0) { + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); + return; + } + + if (responses.length == 1) { + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0])); + return; + } + @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; + for (int i = 1; i < responses.length; i++) { + remaining[i - 1] = Mono.just(responses[i]); + } + Mockito.when(responseSpec.bodyToMono(String.class)) + .thenReturn(Mono.just(responses[0]), remaining); + } + + @SuppressWarnings("rawtypes") + private WebClient.RequestHeadersSpec resolveUriTemplateAndReturn(InvocationOnMock invocation) { + String uriTemplate = invocation.getArgument(0); + Object[] allArgs = invocation.getArguments(); + Object[] uriVariables; + if (allArgs.length > 1 && allArgs[1] instanceof Object[]) { + uriVariables = (Object[]) allArgs[1]; + } else { + uriVariables = new Object[allArgs.length - 1]; + System.arraycopy(allArgs, 1, uriVariables, 0, allArgs.length - 1); + } + String resolved = resolveTemplate(uriTemplate, uriVariables); + capturedResolvedUris.add(resolved); + return requestHeadersSpec; + } + + private String resolveTemplate(String uriTemplate, Object[] uriVariables) { + if (uriVariables == null || uriVariables.length == 0) { + return uriTemplate; + } + Matcher matcher = URI_VARIABLE_PATTERN.matcher(uriTemplate); + StringBuilder sb = new StringBuilder(); + int varIndex = 0; + while (matcher.find() && varIndex < uriVariables.length) { + matcher.appendReplacement(sb, Matcher.quoteReplacement(String.valueOf(uriVariables[varIndex++]))); + } + matcher.appendTail(sb); + return sb.toString(); + } +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java index 2db2b93faa..cb8a092634 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java @@ -5,7 +5,6 @@ import java.util.Collections; import java.util.List; import java.util.UUID; -import java.util.function.Consumer; import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; @@ -13,42 +12,23 @@ import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaService; import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaServiceKey; import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMtaService; -import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -class CustomServiceKeysClientTest { +class CustomServiceKeysClientTest extends CustomControllerClientBaseTest { private static final String SPACE_GUID = "space-guid-123"; private static final String MTA_ID = "my-mta"; private static final String MTA_NAMESPACE = "my-namespace"; private static final String CORRELATION_ID = "test-correlation-id"; - @Mock - private WebClientFactory webClientFactory; - @Mock - private ApplicationConfiguration applicationConfiguration; - @Mock - private WebClient webClient; - - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); - @Mock - private WebClient.ResponseSpec responseSpec; - private CustomServiceKeysClient client; @BeforeEach @@ -85,7 +65,7 @@ void testGetServiceKeysByExistingGuidsWithAllNullGuidsReturnsEmptyList() { @Test void testGetServiceKeysByExistingGuidsFiltersNullGuids() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String randomServiceGuid = UUID.randomUUID() .toString(); @@ -97,27 +77,20 @@ void testGetServiceKeysByExistingGuidsFiltersNullGuids() { client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, guidsWithNulls); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec) - .uri(uriCaptor.capture()); - String capturedUri = uriCaptor.getValue(); + String capturedUri = capturedResolvedUris.getFirst(); assertTrue(capturedUri.endsWith(MessageFormat.format("&service_instance_guids={0}", randomServiceGuid)), "service_instance_guids should contain only the non-null guid"); } @Test void testGetServiceKeysByExistingGuidsBuildsCorrectUri() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String guid = UUID.randomUUID() .toString(); client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(guid)); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec) - .uri(uriCaptor.capture()); - - String capturedUri = uriCaptor.getValue(); + String capturedUri = capturedResolvedUris.getFirst(); assertTrue(capturedUri.startsWith("/v3/service_credential_bindings?type=key&label_selector="), "URI should start with the service keys base path"); assertTrue(capturedUri.contains("space_guid=" + SPACE_GUID), "URI should contain the space_guid label selector"); @@ -128,7 +101,7 @@ void testGetServiceKeysByExistingGuidsBuildsCorrectUri() { @Test void testGetServiceKeysByExistingGuidsJoinsMultipleGuidsWithComma() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String guid1 = UUID.randomUUID() .toString(); @@ -136,11 +109,7 @@ void testGetServiceKeysByExistingGuidsJoinsMultipleGuidsWithComma() { .toString(); client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(guid1, guid2)); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec) - .uri(uriCaptor.capture()); - - String capturedUri = uriCaptor.getValue(); + String capturedUri = capturedResolvedUris.getFirst(); assertTrue(capturedUri.contains("&service_instance_guids=" + guid1 + "," + guid2), "URI should contain both guids joined by comma"); } @@ -150,8 +119,8 @@ void testGetServiceKeysByExistingGuidsReturnsServiceKeys() { UUID serviceInstanceGuid = UUID.randomUUID(); UUID serviceKeyGuid = UUID.randomUUID(); - String responseJson = buildServiceKeysResponse(serviceKeyGuid, serviceInstanceGuid, "my-key", "my-service"); - stubWebClientToReturn(responseJson); + String responseJson = buildServiceKeysResponse(serviceKeyGuid, serviceInstanceGuid, "my-key", "my-service", null); + stubWebClientToReturnResponse(responseJson); List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(serviceInstanceGuid.toString())); @@ -168,7 +137,7 @@ void testGetServiceKeysByExistingGuidsReturnsServiceKeys() { @Test void testGetServiceKeysByExistingGuidsWithEmptyResponseReturnsEmptyList() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String guid = UUID.randomUUID() .toString(); @@ -231,18 +200,15 @@ void testGetServiceKeysByManagedServicesWithNullGuidInMetadataReturnsEmptyList() @Test void testGetServiceKeysByManagedServicesMakesHttpCall() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID serviceGuid = UUID.randomUUID(); DeployedMtaService managedService = buildManagedService("managed-svc", serviceGuid); client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(managedService)); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec) - .uri(uriCaptor.capture()); - String capturedUri = uriCaptor.getValue(); + String capturedUri = capturedResolvedUris.getFirst(); assertTrue(capturedUri.contains("&service_instance_guids=" + serviceGuid), "URI should contain the guid of the managed service"); Mockito.verify(webClient, Mockito.times(1)) .get(); @@ -250,7 +216,7 @@ void testGetServiceKeysByManagedServicesMakesHttpCall() { @Test void testGetServiceKeysByManagedServicesUsesOnlyManagedGuids() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); UUID managedGuid = UUID.randomUUID(); UUID userProvidedGuid = UUID.randomUUID(); @@ -265,11 +231,7 @@ void testGetServiceKeysByManagedServicesUsesOnlyManagedGuids() { client.getServiceKeysByMetadataAndManagedServices(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(managedService, userProvidedService)); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec) - .uri(uriCaptor.capture()); - - String capturedUri = uriCaptor.getValue(); + String capturedUri = capturedResolvedUris.getFirst(); assertTrue(capturedUri.endsWith("&service_instance_guids=" + managedGuid), "URI should contain only the managed service guid"); assertFalse(capturedUri.contains(userProvidedGuid.toString()), "URI should not contain the user-provided service guid"); @@ -282,8 +244,8 @@ void testGetServiceKeysByManagedServicesReturnsServiceKeys() { String serviceKeyName = "sk-1"; String serviceInstanceName = "svc-1"; - String responseJson = buildServiceKeysResponse(serviceKeyGuid, serviceInstanceGuid, serviceKeyName, serviceInstanceName); - stubWebClientToReturn(responseJson); + String responseJson = buildServiceKeysResponse(serviceKeyGuid, serviceInstanceGuid, serviceKeyName, serviceInstanceName, null); + stubWebClientToReturnResponse(responseJson); DeployedMtaService managedService = buildManagedService(serviceInstanceName, serviceInstanceGuid); @@ -300,17 +262,13 @@ void testGetServiceKeysByManagedServicesReturnsServiceKeys() { @Test void testLabelSelectorContainsSpaceGuidMtaIdAndMtaNamespace() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String guid = UUID.randomUUID() .toString(); client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(guid)); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec) - .uri(uriCaptor.capture()); - - String capturedUri = uriCaptor.getValue(); + String capturedUri = capturedResolvedUris.getFirst(); assertTrue(capturedUri.contains("space_guid=" + SPACE_GUID)); assertTrue(capturedUri.contains("mta_id=")); assertTrue(capturedUri.contains("mta_namespace=")); @@ -318,40 +276,32 @@ void testLabelSelectorContainsSpaceGuidMtaIdAndMtaNamespace() { @Test void testLabelSelectorWithNullNamespaceUsesDoesNotExist() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String guid = UUID.randomUUID() .toString(); client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, null, List.of(guid)); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec) - .uri(uriCaptor.capture()); - - String capturedUri = uriCaptor.getValue(); + String capturedUri = capturedResolvedUris.getFirst(); // When namespace is null/empty, the label selector uses "!" prefix (doesNotExist) assertTrue(capturedUri.contains("!mta_namespace"), "When namespace is null, label selector should use !mta_namespace"); } @Test void testLabelSelectorWithEmptyNamespaceUsesDoesNotExist() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String guid = UUID.randomUUID() .toString(); client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, "", List.of(guid)); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec) - .uri(uriCaptor.capture()); - - String capturedUri = uriCaptor.getValue(); + String capturedUri = capturedResolvedUris.getFirst(); assertTrue(capturedUri.contains("!mta_namespace"), "When namespace is empty, label selector should use !mta_namespace"); } @Test void testBatchingTriggeredWithManyGuids() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); // Generate enough GUIDs to force multiple batches (each UUID is 36 chars, limit is 4000) List manyGuids = generateRandomGuids(200); @@ -365,7 +315,7 @@ void testBatchingTriggeredWithManyGuids() { @Test void testSingleBatchWithFewGuids() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); List fewGuids = generateRandomGuids(3); @@ -377,16 +327,12 @@ void testSingleBatchWithFewGuids() { @Test void testBatchedUrisNeverExceedMaxLength() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); List manyGuids = generateRandomGuids(800); client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, manyGuids); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec, Mockito.atLeastOnce()) - .uri(uriCaptor.capture()); - - for (String uri : uriCaptor.getAllValues()) { + for (String uri : capturedResolvedUris) { assertTrue(uri.length() <= CustomControllerClient.MAX_URI_QUERY_LENGTH, "URI length " + uri.length() + " exceeds MAX_URI_QUERY_LENGTH " + CustomControllerClient.MAX_URI_QUERY_LENGTH); @@ -395,17 +341,13 @@ void testBatchedUrisNeverExceedMaxLength() { @Test void testBatchedRequestsContainAllGuidsInOrder() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); List manyGuids = generateRandomGuids(200); client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, manyGuids); - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(String.class); - Mockito.verify(requestHeadersUriSpec, Mockito.atLeastOnce()) - .uri(uriCaptor.capture()); - List allCapturedGuids = new ArrayList<>(); - for (String uri : uriCaptor.getAllValues()) { + for (String uri : capturedResolvedUris) { int idx = uri.indexOf("&service_instance_guids="); assertTrue(idx >= 0, "URI should contain &service_instance_guids="); String guidsStr = uri.substring(idx + "&service_instance_guids=".length()); @@ -424,11 +366,11 @@ void testPaginatedResponseFollowsAllPages() { UUID keyGuid1 = UUID.randomUUID(); UUID keyGuid2 = UUID.randomUUID(); - String page1Json = buildServiceKeysResponseWithPagination(keyGuid1, siGuid, "key-1", "svc-1", - "/v3/service_credential_bindings?page=2"); - String page2Json = buildServiceKeysResponse(keyGuid2, siGuid, "key-2", "svc-1"); + String page1Json = buildServiceKeysResponse(keyGuid1, siGuid, "key-1", "svc-1", + "/v3/service_credential_bindings?page=2"); + String page2Json = buildServiceKeysResponse(keyGuid2, siGuid, "key-2", "svc-1", null); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(siGuid.toString())); @@ -447,7 +389,7 @@ void testMultipleKeysForSameServiceAreMappedCorrectly() { UUID keyGuid2 = UUID.randomUUID(); String responseJson = buildMultiKeyResponse(List.of(keyGuid1, keyGuid2), siGuid, List.of("key-a", "key-b"), "shared-service"); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); List result = client.getServiceKeysByMetadataAndExistingGuids(SPACE_GUID, MTA_ID, MTA_NAMESPACE, List.of(siGuid.toString())); @@ -466,58 +408,6 @@ void testMultipleKeysForSameServiceAreMappedCorrectly() { .getName()); } - @SuppressWarnings("unchecked") - private void stubWebClientToReturnEmptyPage() { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); - } - - @SuppressWarnings("unchecked") - private void stubWebClientToReturn(String responseJson) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responseJson)); - } - - @SuppressWarnings("unchecked") - private void stubWebClientToReturnSequentially(String... responses) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - - if (responses.length == 1) { - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0])); - } else { - @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; - for (int i = 1; i < responses.length; i++) { - remaining[i - 1] = Mono.just(responses[i]); - } - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0]), remaining); - } - } - private DeployedMtaService buildManagedService(String name, UUID guid) { return ImmutableDeployedMtaService.builder() .name(name) @@ -537,52 +427,59 @@ private List generateRandomGuids(int count) { return guids; } - private String buildServiceKeysResponse(UUID serviceKeyGuid, UUID serviceInstanceGuid, String keyName, String serviceName) { - return buildServiceKeysResponseWithPagination(serviceKeyGuid, serviceInstanceGuid, keyName, serviceName, null); + private String buildServiceKeysResponse(UUID serviceKeyGuid, UUID serviceInstanceGuid, String keyName, String serviceName, + String nextPageHref) { + String keyResourceJson = buildServiceKeyResourceJson(serviceKeyGuid, keyName, serviceInstanceGuid); + String serviceInstanceJson = buildServiceInstanceJson(serviceInstanceGuid, serviceName); + return assembleResponseJson(keyResourceJson, serviceInstanceJson, buildPaginationJson(nextPageHref)); + } + + private String buildServiceKeyResourceJson(UUID keyGuid, String keyName, UUID serviceInstanceGuid) { + return "{\"guid\":\"" + keyGuid + "\"," + + "\"name\":\"" + keyName + "\"," + + "\"type\":\"key\"," + + "\"created_at\":\"2024-01-01T00:00:00Z\"," + + "\"updated_at\":\"2024-01-01T00:00:00Z\"," + + "\"metadata\":{\"labels\":{},\"annotations\":{}}," + + "\"relationships\":{\"service_instance\":{\"data\":{\"guid\":\"" + serviceInstanceGuid + "\"}}}" + + "}"; + } + + private String buildServiceInstanceJson(UUID serviceInstanceGuid, String serviceName) { + return "{\"guid\":\"" + serviceInstanceGuid + "\"," + + "\"name\":\"" + serviceName + "\"," + + "\"type\":\"managed\"," + + "\"created_at\":\"2024-01-01T00:00:00Z\"," + + "\"updated_at\":\"2024-01-01T00:00:00Z\"," + + "\"metadata\":{\"labels\":{},\"annotations\":{}}}"; } - private String buildServiceKeysResponseWithPagination(UUID serviceKeyGuid, UUID serviceInstanceGuid, String keyName, String serviceName, - String nextPageHref) { + private String buildPaginationJson(String nextPageHref) { String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; - return "{" + "\"resources\":[" + " {" + " \"guid\":\"" + serviceKeyGuid + "\"," + " \"name\":\"" + keyName + "\"," - + " \"type\":\"key\"," + " \"created_at\":\"2024-01-01T00:00:00Z\"," + " \"updated_at\":\"2024-01-01T00:00:00Z\"," - + " \"metadata\":{\"labels\":{},\"annotations\":{}}," + " \"relationships\":{" - + " \"service_instance\":{\"data\":{\"guid\":\"" + serviceInstanceGuid + "\"}}" + " }" + " }" + "]," + "\"included\":{" - + " \"service_instances\":[" + " {" + " \"guid\":\"" + serviceInstanceGuid + "\"," + " \"name\":\"" + serviceName - + "\"," + " \"type\":\"managed\"," + " \"created_at\":\"2024-01-01T00:00:00Z\"," - + " \"updated_at\":\"2024-01-01T00:00:00Z\"," + " \"metadata\":{\"labels\":{},\"annotations\":{}}" + " }" + " ]" - + "}," + "\"pagination\":{\"next\":" + nextPage + "}" + "}"; + return "{\"next\":" + nextPage + "}"; + } + + private String assembleResponseJson(String resourcesJson, String serviceInstancesJson, String paginationJson) { + return "{\"resources\":[" + resourcesJson + "]," + + "\"included\":{\"service_instances\":[" + serviceInstancesJson + "]}," + + "\"pagination\":" + paginationJson + "}"; } private String buildMultiKeyResponse(List keyGuids, UUID serviceInstanceGuid, List keyNames, String serviceName) { - StringBuilder resources = new StringBuilder(); + String keyResourcesJson = buildMultipleServiceKeyResourcesJson(keyGuids, keyNames, serviceInstanceGuid); + String serviceInstanceJson = buildServiceInstanceJson(serviceInstanceGuid, serviceName); + return assembleResponseJson(keyResourcesJson, serviceInstanceJson, buildPaginationJson(null)); + } + + private String buildMultipleServiceKeyResourcesJson(List keyGuids, List keyNames, UUID serviceInstanceGuid) { + StringBuilder sb = new StringBuilder(); for (int i = 0; i < keyGuids.size(); i++) { if (i > 0) { - resources.append(","); + sb.append(","); } - resources.append("{") - .append("\"guid\":\"") - .append(keyGuids.get(i)) - .append("\",") - .append("\"name\":\"") - .append(keyNames.get(i)) - .append("\",") - .append("\"type\":\"key\",") - .append("\"created_at\":\"2024-01-01T00:00:00Z\",") - .append("\"updated_at\":\"2024-01-01T00:00:00Z\",") - .append("\"metadata\":{\"labels\":{},\"annotations\":{}},") - .append("\"relationships\":{") - .append(" \"service_instance\":{\"data\":{\"guid\":\"") - .append(serviceInstanceGuid) - .append("\"}}") - .append("}") - .append("}"); + sb.append(buildServiceKeyResourceJson(keyGuids.get(i), keyNames.get(i), serviceInstanceGuid)); } - - return "{" + "\"resources\":[" + resources + "]," + "\"included\":{" + " \"service_instances\":[" + " {" + " \"guid\":\"" - + serviceInstanceGuid + "\"," + " \"name\":\"" + serviceName + "\"," + " \"type\":\"managed\"," - + " \"created_at\":\"2024-01-01T00:00:00Z\"," + " \"updated_at\":\"2024-01-01T00:00:00Z\"," - + " \"metadata\":{\"labels\":{},\"annotations\":{}}" + " }" + " ]" + "}," + "\"pagination\":{\"next\":null}" + "}"; + return sb.toString(); } } diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetterTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetterTest.java index db84a62158..0d4f72b55d 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetterTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/ServiceInstanceRoutesGetterTest.java @@ -4,41 +4,22 @@ import java.util.Collections; import java.util.List; import java.util.UUID; -import java.util.function.Consumer; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.client.lib.domain.ServiceRouteBinding; -import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -class ServiceInstanceRoutesGetterTest { +class ServiceInstanceRoutesGetterTest extends CustomControllerClientBaseTest { private static final String CORRELATION_ID = "test-correlation-id"; - @Mock - private WebClientFactory webClientFactory; - @Mock - private ApplicationConfiguration applicationConfiguration; - @Mock - private WebClient webClient; - - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); - @SuppressWarnings("rawtypes") - private final WebClient.RequestHeadersSpec requestHeadersSpec = Mockito.mock(WebClient.RequestHeadersSpec.class); - @Mock - private WebClient.ResponseSpec responseSpec; - private ServiceInstanceRoutesGetter client; @BeforeEach @@ -63,7 +44,7 @@ void testGetServiceRouteBindingsWithEmptyGuidsReturnsEmptyList() { @Test void testGetServiceRouteBindingsBuildsCorrectUri() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String routeGuid = UUID.randomUUID() .toString(); @@ -82,7 +63,7 @@ void testGetServiceRouteBindingsBuildsCorrectUri() { @Test void testGetServiceRouteBindingsJoinsMultipleGuidsWithComma() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String routeGuid1 = UUID.randomUUID() .toString(); @@ -106,8 +87,8 @@ void testGetServiceRouteBindingsReturnsMappedBindings() { String serviceInstanceGuid = UUID.randomUUID() .toString(); - String responseJson = buildServiceRouteBindingsResponse(routeGuid, serviceInstanceGuid); - stubWebClientToReturn(responseJson); + String responseJson = buildServiceRouteBindingsResponse(routeGuid, serviceInstanceGuid, null); + stubWebClientToReturnResponse(responseJson); List result = client.getServiceRouteBindings(List.of(routeGuid)); @@ -119,7 +100,7 @@ void testGetServiceRouteBindingsReturnsMappedBindings() { @Test void testGetServiceRouteBindingsWithEmptyResponseReturnsEmptyList() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); String routeGuid = UUID.randomUUID() .toString(); @@ -141,7 +122,7 @@ void testGetServiceRouteBindingsReturnsMultipleBindings() { String responseJson = buildMultipleServiceRouteBindingsResponse( List.of(routeGuid1, routeGuid2), List.of(serviceInstanceGuid1, serviceInstanceGuid2)); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); List result = client.getServiceRouteBindings(List.of(routeGuid1, routeGuid2)); @@ -168,7 +149,7 @@ void testGetServiceRouteBindingsMultipleBindingsForSameRoute() { String responseJson = buildMultipleServiceRouteBindingsResponse( List.of(routeGuid, routeGuid), List.of(serviceInstanceGuid1, serviceInstanceGuid2)); - stubWebClientToReturn(responseJson); + stubWebClientToReturnResponse(responseJson); List result = client.getServiceRouteBindings(List.of(routeGuid)); @@ -192,11 +173,11 @@ void testGetServiceRouteBindingsPaginatedResponseFollowsAllPages() { String serviceInstanceGuid2 = UUID.randomUUID() .toString(); - String page1Json = buildServiceRouteBindingsResponseWithPagination(routeGuid, serviceInstanceGuid1, - "/v3/service_route_bindings?page=2"); - String page2Json = buildServiceRouteBindingsResponse(routeGuid, serviceInstanceGuid2); + String page1Json = buildServiceRouteBindingsResponse(routeGuid, serviceInstanceGuid1, + "/v3/service_route_bindings?page=2"); + String page2Json = buildServiceRouteBindingsResponse(routeGuid, serviceInstanceGuid2, null); - stubWebClientToReturnSequentially(page1Json, page2Json); + stubWebClientToReturnResponse(page1Json, page2Json); List result = client.getServiceRouteBindings(List.of(routeGuid)); @@ -209,7 +190,7 @@ void testGetServiceRouteBindingsPaginatedResponseFollowsAllPages() { @Test void testBatchingTriggeredWithManyGuids() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); List manyGuids = generateRandomGuids(200); @@ -221,7 +202,7 @@ void testBatchingTriggeredWithManyGuids() { @Test void testSingleBatchWithFewGuids() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); List fewGuids = generateRandomGuids(3); @@ -233,7 +214,7 @@ void testSingleBatchWithFewGuids() { @Test void testBatchedUrisNeverExceedMaxLength() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); List manyGuids = generateRandomGuids(800); client.getServiceRouteBindings(manyGuids); @@ -251,7 +232,7 @@ void testBatchedUrisNeverExceedMaxLength() { @Test void testBatchedRequestsContainAllGuidsInOrder() { - stubWebClientToReturnEmptyPage(); + stubWebClientToReturnResponse(); List manyGuids = generateRandomGuids(200); client.getServiceRouteBindings(manyGuids); @@ -280,8 +261,8 @@ void testBatchedRequestsAggregateResultsFromAllBatches() { String serviceInstanceGuid1 = UUID.randomUUID() .toString(); - String response = buildServiceRouteBindingsResponse(routeGuid1, serviceInstanceGuid1); - stubWebClientToReturn(response); + String response = buildServiceRouteBindingsResponse(routeGuid1, serviceInstanceGuid1, null); + stubWebClientToReturnResponse(response); List result = client.getServiceRouteBindings(List.of(routeGuid1)); @@ -292,58 +273,6 @@ void testBatchedRequestsAggregateResultsFromAllBatches() { .getServiceInstanceId()); } - @SuppressWarnings("unchecked") - private void stubWebClientToReturnEmptyPage() { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just("{\"resources\":[],\"pagination\":{\"next\":null}}")); - } - - @SuppressWarnings("unchecked") - private void stubWebClientToReturn(String responseJson) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responseJson)); - } - - @SuppressWarnings("unchecked") - private void stubWebClientToReturnSequentially(String... responses) { - Mockito.when(webClient.get()) - .thenReturn(requestHeadersUriSpec); - Mockito.when(requestHeadersUriSpec.uri(Mockito.anyString())) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.headers(Mockito.any(Consumer.class))) - .thenReturn(requestHeadersSpec); - Mockito.when(requestHeadersSpec.retrieve()) - .thenReturn(responseSpec); - - if (responses.length == 1) { - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0])); - } else { - @SuppressWarnings("rawtypes") Mono[] remaining = new Mono[responses.length - 1]; - for (int i = 1; i < responses.length; i++) { - remaining[i - 1] = Mono.just(responses[i]); - } - Mockito.when(responseSpec.bodyToMono(String.class)) - .thenReturn(Mono.just(responses[0]), remaining); - } - } - private List generateRandomGuids(int count) { List guids = new ArrayList<>(); for (int i = 0; i < count; i++) { @@ -353,55 +282,45 @@ private List generateRandomGuids(int count) { return guids; } - private String buildServiceRouteBindingsResponse(String routeGuid, String serviceInstanceGuid) { - return buildServiceRouteBindingsResponseWithPagination(routeGuid, serviceInstanceGuid, null); + private String buildServiceRouteBindingsResponse(String routeGuid, String serviceInstanceGuid, String nextPageHref) { + String resourceJson = buildRouteBindingResourceJson(routeGuid, serviceInstanceGuid); + return assembleRouteBindingsResponseJson(resourceJson, buildPaginationJson(nextPageHref)); + } + + private String buildRouteBindingResourceJson(String routeGuid, String serviceInstanceGuid) { + return "{\"guid\":\"" + UUID.randomUUID() + "\"," + + "\"created_at\":\"2024-01-01T00:00:00Z\"," + + "\"updated_at\":\"2024-01-01T00:00:00Z\"," + + "\"relationships\":{" + + "\"route\":{\"data\":{\"guid\":\"" + routeGuid + "\"}}," + + "\"service_instance\":{\"data\":{\"guid\":\"" + serviceInstanceGuid + "\"}}" + + "}}"; } - private String buildServiceRouteBindingsResponseWithPagination(String routeGuid, String serviceInstanceGuid, String nextPageHref) { + private String buildPaginationJson(String nextPageHref) { String nextPage = nextPageHref == null ? "null" : "{\"href\":\"" + nextPageHref + "\"}"; - return "{" - + "\"resources\":[" - + " {" - + " \"guid\":\"" + UUID.randomUUID() + "\"," - + " \"created_at\":\"2024-01-01T00:00:00Z\"," - + " \"updated_at\":\"2024-01-01T00:00:00Z\"," - + " \"relationships\":{" - + " \"route\":{\"data\":{\"guid\":\"" + routeGuid + "\"}}," - + " \"service_instance\":{\"data\":{\"guid\":\"" + serviceInstanceGuid + "\"}}" - + " }" - + " }" - + "]," - + "\"pagination\":{\"next\":" + nextPage + "}" - + "}"; + return "{\"next\":" + nextPage + "}"; + } + + private String assembleRouteBindingsResponseJson(String resourcesJson, String paginationJson) { + return "{\"resources\":[" + resourcesJson + "]," + + "\"pagination\":" + paginationJson + "}"; } private String buildMultipleServiceRouteBindingsResponse(List routeGuids, List serviceInstanceGuids) { - StringBuilder resources = new StringBuilder(); + String resourcesJson = buildMultipleRouteBindingResourcesJson(routeGuids, serviceInstanceGuids); + return assembleRouteBindingsResponseJson(resourcesJson, buildPaginationJson(null)); + } + + private String buildMultipleRouteBindingResourcesJson(List routeGuids, List serviceInstanceGuids) { + StringBuilder sb = new StringBuilder(); for (int i = 0; i < routeGuids.size(); i++) { if (i > 0) { - resources.append(","); + sb.append(","); } - resources.append("{") - .append("\"guid\":\"") - .append(UUID.randomUUID()) - .append("\",") - .append("\"created_at\":\"2024-01-01T00:00:00Z\",") - .append("\"updated_at\":\"2024-01-01T00:00:00Z\",") - .append("\"relationships\":{") - .append(" \"route\":{\"data\":{\"guid\":\"") - .append(routeGuids.get(i)) - .append("\"}},") - .append(" \"service_instance\":{\"data\":{\"guid\":\"") - .append(serviceInstanceGuids.get(i)) - .append("\"}}") - .append("}") - .append("}"); + sb.append(buildRouteBindingResourceJson(routeGuids.get(i), serviceInstanceGuids.get(i))); } - - return "{" - + "\"resources\":[" + resources + "]," - + "\"pagination\":{\"next\":null}" - + "}"; + return sb.toString(); } }