diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a78875f..d27c3d0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [10.6.2] +### Changed +- Updated stream-investment to be able to seed to investment caboose + ## [10.6.1] ### Changed - Updated plan manager ingestion logic to attempt to retrieve plans if they don't exist in the pre-filled map. diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentClientConfig.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentClientConfig.java index 019fa5bdd..d267bee6f 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentClientConfig.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentClientConfig.java @@ -25,7 +25,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.web.reactive.function.client.WebClient; @@ -41,9 +43,11 @@ * {@link InvestmentWebClientConfiguration} which should be imported alongside this class. * The WebClient connection pool prevents resource exhaustion and 503 errors by limiting: *
All values can be overridden via {@code application.yml} / {@code application.properties} - * using the prefix {@code backbase.communication.services.investment.http-client}. + *
Bound via {@code backbase.communication.services.investment.http-client} and + * {@code backbase.communication.services.investment-caboose.http-client}. * - *
Example: + *
Example (values shown are illustrative; defaults are in field JavaDoc below): *
* backbase: * communication: * services: * investment: * http-client: - * max-connections: 20 + * max-connections: 50 * max-idle-time-minutes: 5 - * max-pending-acquires: 100 - * pending-acquire-timeout-millis: 45000 + * max-life-time-minutes: 30 + * max-pending-acquires: -1 + * pending-acquire-timeout-millis: 90000 + * evict-in-background-seconds: 120 * connect-timeout-seconds: 10 * read-timeout-seconds: 30 * write-timeout-seconds: 30 + * investment-caboose: + * http-client: + * max-connections: 50 **/ @Data -@ConfigurationProperties(prefix = "backbase.communication.services.investment.http-client") public class InvestmentWebClientProperties { /** * Maximum number of open TCP connections to the Investment service. * Limiting this prevents the service from being overwhelmed (which causes 503 responses). */ - private int maxConnections = 20; + private int maxConnections = 50; /** * Maximum time (in minutes) that a connection can remain idle in the pool before being evicted. @@ -46,15 +49,24 @@ public class InvestmentWebClientProperties { private long maxLifeTimeMinutes = 30; /** - * Maximum number of requests that can be queued waiting for a connection. - * Bounds the in-memory queue so callers receive a fast failure rather than an unbounded backlog. + * Maximum number of requests that can wait for a free connection. + * + *
{@code -1} means no queue limit — callers block up to + * {@link #pendingAcquireTimeoutMillis} instead of failing fast with + * "Pending acquire queue has reached its maximum size". + * + *
Pair with application-level {@code flatMap} concurrency limits in the ingestion + * services so the queue does not grow without bound. */ - private int maxPendingAcquires = 100; + private int maxPendingAcquires = -1; /** * Maximum time (in milliseconds) a request will wait to acquire a connection from the pool. + * + *
When all {@link #maxConnections} are in use, new requests wait here rather than
+ * opening additional connections or failing immediately.
*/
- private long pendingAcquireTimeoutMillis = 45_000;
+ private long pendingAcquireTimeoutMillis = 90_000;
/**
* Background eviction interval (in seconds) for idle/expired connections in the pool.
diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/AsyncTaskService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/AsyncTaskService.java
index 32c3c7cba..6f2b9c5b6 100644
--- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/AsyncTaskService.java
+++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/AsyncTaskService.java
@@ -22,20 +22,23 @@ public Mono Each {@code @Bean} factory method is invoked directly (no Spring context) to verify
+ * that it returns a non-null, correctly typed bean.
+ */
+@DisplayName("InvestmentWebClientConfiguration")
+class InvestmentWebClientConfigurationTest {
+
+ private InvestmentWebClientConfiguration config;
+
+ @BeforeEach
+ void setUp() {
+ config = new InvestmentWebClientConfiguration();
+ }
+
+ @Test
+ @DisplayName("investmentWebClientProperties — returns non-null properties with defaults")
+ void investmentWebClientProperties_returnsNonNullProperties() {
+ InvestmentWebClientProperties properties = config.investmentWebClientProperties();
+
+ assertThat(properties).isNotNull();
+ assertThat(properties.getMaxConnections()).isPositive();
+ assertThat(properties.getConnectTimeoutSeconds()).isPositive();
+ }
+
+ @Test
+ @DisplayName("investmentConnectionProvider — returns configured connection provider")
+ void investmentConnectionProvider_returnsConfiguredProvider() {
+ InvestmentWebClientProperties properties = config.investmentWebClientProperties();
+
+ ConnectionProvider provider = config.investmentConnectionProvider(properties);
+
+ assertThat(provider).isNotNull();
+ }
+
+ @Test
+ @DisplayName("investmentHttpClient — returns configured HttpClient")
+ void investmentHttpClient_returnsConfiguredHttpClient() {
+ InvestmentWebClientProperties properties = config.investmentWebClientProperties();
+ ConnectionProvider provider = config.investmentConnectionProvider(properties);
+
+ HttpClient httpClient = config.investmentHttpClient(provider, properties);
+
+ assertThat(httpClient).isNotNull();
+ }
+}
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/AsyncTaskServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/AsyncTaskServiceTest.java
new file mode 100644
index 000000000..841194e47
--- /dev/null
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/AsyncTaskServiceTest.java
@@ -0,0 +1,149 @@
+package com.backbase.stream.investment.service;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.backbase.investment.api.service.v1.AsyncBulkGroupsApi;
+import com.backbase.investment.api.service.v1.model.GroupResult;
+import java.time.Duration;
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+@DisplayName("AsyncTaskService")
+class AsyncTaskServiceTest {
+
+ private AsyncBulkGroupsApi asyncBulkGroupsApi;
+ private AsyncTaskService service;
+
+ @BeforeEach
+ void setUp() {
+ asyncBulkGroupsApi = mock(AsyncBulkGroupsApi.class);
+ service = new AsyncTaskService(asyncBulkGroupsApi);
+ }
+
+ @Nested
+ @DisplayName("checkPriceAsyncTasksFinished")
+ class CheckPriceAsyncTasksFinishedTests {
+
+ @Test
+ @DisplayName("empty task list — returns empty list immediately")
+ void emptyTaskList_returnsEmptyList() {
+ StepVerifier.create(service.checkPriceAsyncTasksFinished(List.of()))
+ .expectNext(List.of())
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("all tasks completed on first poll — returns polled results")
+ void allTasksCompletedOnFirstPoll_returnsPolledResults() {
+ UUID uuid = UUID.randomUUID();
+ GroupResult inputTask = new GroupResult(uuid, "PENDING", List.of());
+ GroupResult completed = new GroupResult(uuid, "COMPLETED", List.of());
+
+ when(asyncBulkGroupsApi.getBulkGroup(uuid.toString())).thenReturn(Mono.just(completed));
+
+ StepVerifier.withVirtualTime(() -> service.checkPriceAsyncTasksFinished(List.of(inputTask)))
+ .thenAwait(Duration.ofSeconds(5))
+ .expectNextMatches(results -> results.size() == 1
+ && "COMPLETED".equalsIgnoreCase(results.getFirst().getStatus()))
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("tasks transition from pending to completed — polls until finished")
+ void tasksTransitionFromPendingToCompleted_pollsUntilFinished() {
+ UUID uuid = UUID.randomUUID();
+ GroupResult inputTask = new GroupResult(uuid, "PENDING", List.of());
+ GroupResult completed = new GroupResult(uuid, "COMPLETED", List.of());
+
+ when(asyncBulkGroupsApi.getBulkGroup(uuid.toString()))
+ .thenReturn(Mono.just(new GroupResult(uuid, "PENDING", List.of())))
+ .thenReturn(Mono.just(completed));
+
+ StepVerifier.withVirtualTime(() -> service.checkPriceAsyncTasksFinished(List.of(inputTask)))
+ .thenAwait(Duration.ofSeconds(10))
+ .expectNextMatches(results -> results.size() == 1
+ && "COMPLETED".equalsIgnoreCase(results.getFirst().getStatus()))
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("pending status is case-insensitive — completes when status is lowercase")
+ void pendingStatusCaseInsensitive_completesWhenNoLongerPending() {
+ UUID uuid = UUID.randomUUID();
+ GroupResult inputTask = new GroupResult(uuid, "PENDING", List.of());
+ GroupResult completed = new GroupResult(uuid, "completed", List.of());
+
+ when(asyncBulkGroupsApi.getBulkGroup(uuid.toString()))
+ .thenReturn(Mono.just(new GroupResult(uuid, "pending", List.of())))
+ .thenReturn(Mono.just(completed));
+
+ StepVerifier.withVirtualTime(() -> service.checkPriceAsyncTasksFinished(List.of(inputTask)))
+ .thenAwait(Duration.ofSeconds(10))
+ .expectNextMatches(results -> results.size() == 1
+ && "completed".equals(results.getFirst().getStatus()))
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("timeout waiting for tasks — returns original task list")
+ void timeoutWaitingForTasks_returnsOriginalTaskList() {
+ UUID uuid = UUID.randomUUID();
+ GroupResult inputTask = new GroupResult(uuid, "PENDING", List.of());
+
+ when(asyncBulkGroupsApi.getBulkGroup(uuid.toString()))
+ .thenReturn(Mono.just(new GroupResult(uuid, "PENDING", List.of())));
+
+ StepVerifier.withVirtualTime(() -> service.checkPriceAsyncTasksFinished(List.of(inputTask)))
+ .thenAwait(Duration.ofMinutes(11))
+ .expectNextMatches(results -> results.size() == 1
+ && uuid.equals(results.getFirst().getUuid()))
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("API error while polling — returns original task list")
+ void apiErrorWhilePolling_returnsOriginalTaskList() {
+ UUID uuid = UUID.randomUUID();
+ GroupResult inputTask = new GroupResult(uuid, "PENDING", List.of());
+
+ when(asyncBulkGroupsApi.getBulkGroup(uuid.toString()))
+ .thenReturn(Mono.error(new RuntimeException("bulk group lookup failed")));
+
+ StepVerifier.withVirtualTime(() -> service.checkPriceAsyncTasksFinished(List.of(inputTask)))
+ .thenAwait(Duration.ofSeconds(5))
+ .expectNextMatches(results -> results.size() == 1
+ && uuid.equals(results.getFirst().getUuid()))
+ .verifyComplete();
+ }
+ }
+
+ @Nested
+ @DisplayName("groupResultStatus")
+ class GroupResultStatusTests {
+
+ @Test
+ @DisplayName("delegates to AsyncBulkGroupsApi")
+ void delegatesToAsyncBulkGroupsApi() {
+ UUID uuid = UUID.randomUUID();
+ GroupResult groupResult = new GroupResult(uuid, "COMPLETED", List.of());
+
+ when(asyncBulkGroupsApi.getBulkGroup(uuid.toString())).thenReturn(Mono.just(groupResult));
+
+ StepVerifier.create(service.groupResultStatus(uuid))
+ .expectNext(groupResult)
+ .verifyComplete();
+
+ verify(asyncBulkGroupsApi, times(1)).getBulkGroup(eq(uuid.toString()));
+ }
+ }
+}
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentClientServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentClientServiceTest.java
index a391ee1ab..874cc0c7e 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentClientServiceTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentClientServiceTest.java
@@ -1,77 +1,245 @@
package com.backbase.stream.investment.service;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import com.backbase.investment.api.service.v1.ClientApi;
import com.backbase.investment.api.service.v1.model.ClientCreate;
-import com.backbase.investment.api.service.v1.model.ClientCreateRequest;
import com.backbase.investment.api.service.v1.model.OASClient;
import com.backbase.investment.api.service.v1.model.OASClientUpdateRequest;
+import com.backbase.investment.api.service.v1.model.PaginatedOASClientList;
import com.backbase.investment.api.service.v1.model.PatchedOASClientUpdateRequest;
+import com.backbase.stream.investment.ClientUser;
import java.nio.charset.StandardCharsets;
+import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.when;
-
+@DisplayName("InvestmentClientService")
class InvestmentClientServiceTest {
- ClientApi clientApi;
- InvestmentClientService service;
+ private ClientApi clientApi;
+ private InvestmentClientService service;
@BeforeEach
void setUp() {
- clientApi = Mockito.mock(ClientApi.class);
+ clientApi = mock(ClientApi.class);
service = new InvestmentClientService(clientApi);
}
- @Test
- void createClient_success() {
- ClientCreateRequest request = new ClientCreateRequest();
- ClientCreate created = new ClientCreate(UUID.randomUUID());
- when(clientApi.createClient(any())).thenReturn(Mono.just(created));
+ @Nested
+ @DisplayName("upsertClients")
+ class UpsertClientsTests {
+
+ @Test
+ @DisplayName("no existing client — creates new client")
+ void noExistingClient_createsNewClient() {
+ String internalUserId = "internal-1";
+ String externalUserId = "external-1";
+ String legalEntityId = "le-1";
+ UUID clientUuid = UUID.randomUUID();
+
+ ClientUser clientUser = ClientUser.builder()
+ .internalUserId(internalUserId)
+ .externalUserId(externalUserId)
+ .legalEntityId(legalEntityId)
+ .build();
+
+ stubEmptyClientList(internalUserId);
+ when(clientApi.createClient(any())).thenReturn(Mono.just(createdClient(clientUuid, internalUserId)));
+
+ StepVerifier.create(service.upsertClients(List.of(clientUser)))
+ .expectNextMatches(results -> results.size() == 1
+ && clientUuid.equals(results.getFirst().getInvestmentClientId())
+ && internalUserId.equals(results.getFirst().getInternalUserId())
+ && externalUserId.equals(results.getFirst().getExternalUserId())
+ && legalEntityId.equals(results.getFirst().getLegalEntityId()))
+ .verifyComplete();
+
+ verify(clientApi).createClient(any());
+ verify(clientApi, never()).patchClient(any(), any());
+ }
+
+ @Test
+ @DisplayName("duplicate internalUserId — deduplicates and processes once")
+ void duplicateInternalUserId_deduplicatesClients() {
+ String internalUserId = "duplicate-id";
+ UUID clientUuid = UUID.randomUUID();
+
+ ClientUser first = ClientUser.builder()
+ .internalUserId(internalUserId)
+ .externalUserId("external-a")
+ .legalEntityId("le-a")
+ .build();
+ ClientUser duplicate = ClientUser.builder()
+ .internalUserId(internalUserId)
+ .externalUserId("external-b")
+ .legalEntityId("le-b")
+ .build();
+
+ stubEmptyClientList(internalUserId);
+ when(clientApi.createClient(any())).thenReturn(Mono.just(createdClient(clientUuid, internalUserId)));
+
+ StepVerifier.create(service.upsertClients(List.of(first, duplicate)))
+ .expectNextMatches(results -> results.size() == 1)
+ .verifyComplete();
+
+ verify(clientApi, times(1)).listClients(any(), any(), any(), any(), any(),
+ eq(internalUserId), any(), any(), any(), any(), any(), any());
+ }
+
+ @Test
+ @DisplayName("client upsert fails — skips client and returns empty list")
+ void clientUpsertFails_skipsClient() {
+ ClientUser clientUser = ClientUser.builder()
+ .internalUserId("failing-id")
+ .externalUserId("external-fail")
+ .legalEntityId("le-fail")
+ .build();
+
+ stubEmptyClientList("failing-id");
+ when(clientApi.createClient(any()))
+ .thenReturn(Mono.error(new RuntimeException("create failed")));
+
+ StepVerifier.create(service.upsertClients(List.of(clientUser)))
+ .expectNextMatches(List::isEmpty)
+ .verifyComplete();
+ }
+ }
+
+ @Nested
+ @DisplayName("getClient")
+ class GetClientTests {
+
+ @Test
+ @DisplayName("client found — returns client")
+ void clientFound_returnsClient() {
+ UUID uuid = UUID.randomUUID();
+ OASClient client = new OASClient();
+ when(clientApi.getClient(eq(uuid), anyList(), isNull(), isNull())).thenReturn(Mono.just(client));
+
+ StepVerifier.create(service.getClient(uuid))
+ .expectNext(client)
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("client not found — returns empty Mono")
+ void clientNotFound_returnsEmpty() {
+ UUID uuid = UUID.randomUUID();
+ WebClientResponseException notFound = new WebClientResponseException(
+ 404, "Not Found", new HttpHeaders(), new byte[0], StandardCharsets.UTF_8);
+ when(clientApi.getClient(eq(uuid), anyList(), isNull(), isNull())).thenReturn(Mono.error(notFound));
+
+ StepVerifier.create(service.getClient(uuid))
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("non-404 error — propagates error")
+ void nonNotFoundError_propagatesError() {
+ UUID uuid = UUID.randomUUID();
+ when(clientApi.getClient(eq(uuid), anyList(), isNull(), isNull()))
+ .thenReturn(Mono.error(new RuntimeException("service unavailable")));
+ StepVerifier.create(service.getClient(uuid))
+ .expectErrorMatches(e -> e instanceof RuntimeException
+ && "service unavailable".equals(e.getMessage()))
+ .verify();
+ }
}
- @Test
- void getClient_notFoundReturnsEmpty() {
- UUID uuid = UUID.randomUUID();
- WebClientResponseException notFound = new WebClientResponseException(
- 404, "Not Found", new HttpHeaders(), new byte[0], StandardCharsets.UTF_8);
- when(clientApi.getClient(eq(uuid), anyList(), any(), any())).thenReturn(Mono.error(notFound));
+ @Nested
+ @DisplayName("patchClient")
+ class PatchClientTests {
- StepVerifier.create(service.getClient(uuid))
- .verifyComplete();
+ @Test
+ @DisplayName("patch succeeds — returns updated client")
+ void patchClient_success() {
+ UUID uuid = UUID.randomUUID();
+ PatchedOASClientUpdateRequest patch = new PatchedOASClientUpdateRequest();
+ OASClient updated = new OASClient();
+ when(clientApi.patchClient(eq(uuid), any())).thenReturn(Mono.just(updated));
+
+ StepVerifier.create(service.patchClient(uuid, patch))
+ .expectNext(updated)
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("patchClient propagates WebClientResponseException errors")
+ void patchClient_webClientError_propagatesError() {
+ UUID uuid = UUID.randomUUID();
+ PatchedOASClientUpdateRequest patch = new PatchedOASClientUpdateRequest();
+ when(clientApi.patchClient(eq(uuid), any()))
+ .thenReturn(Mono.error(WebClientResponseException.create(
+ HttpStatus.BAD_REQUEST.value(), "Bad Request",
+ HttpHeaders.EMPTY, "invalid patch".getBytes(StandardCharsets.UTF_8),
+ StandardCharsets.UTF_8)));
+
+ StepVerifier.create(service.patchClient(uuid, patch))
+ .expectError(WebClientResponseException.class)
+ .verify();
+ }
}
- @Test
- void patchClient_success() {
- UUID uuid = UUID.randomUUID();
- PatchedOASClientUpdateRequest patch = new PatchedOASClientUpdateRequest();
- OASClient updated = new OASClient();
- when(clientApi.patchClient(eq(uuid), any())).thenReturn(Mono.just(updated));
+ @Nested
+ @DisplayName("updateClient")
+ class UpdateClientTests {
+
+ @Test
+ @DisplayName("update succeeds — returns updated client")
+ void updateClient_success() {
+ UUID uuid = UUID.randomUUID();
+ OASClientUpdateRequest update = new OASClientUpdateRequest();
+ OASClient updated = new OASClient();
+ when(clientApi.updateClient(eq(uuid), any())).thenReturn(Mono.just(updated));
- StepVerifier.create(service.patchClient(uuid, patch))
- .expectNext(updated)
- .verifyComplete();
+ StepVerifier.create(service.updateClient(uuid, update))
+ .expectNext(updated)
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("update propagates WebClientResponseException errors")
+ void updateClient_webClientError_propagatesError() {
+ UUID uuid = UUID.randomUUID();
+ OASClientUpdateRequest update = new OASClientUpdateRequest();
+ when(clientApi.updateClient(eq(uuid), any()))
+ .thenReturn(Mono.error(WebClientResponseException.create(
+ HttpStatus.BAD_REQUEST.value(), "Bad Request",
+ HttpHeaders.EMPTY, "invalid update".getBytes(StandardCharsets.UTF_8),
+ StandardCharsets.UTF_8)));
+
+ StepVerifier.create(service.updateClient(uuid, update))
+ .expectError(WebClientResponseException.class)
+ .verify();
+ }
}
- @Test
- void updateClient_success() {
- UUID uuid = UUID.randomUUID();
- OASClientUpdateRequest update = new OASClientUpdateRequest();
- OASClient updated = new OASClient();
- when(clientApi.updateClient(eq(uuid), any())).thenReturn(Mono.just(updated));
+ private void stubEmptyClientList(String internalUserId) {
+ lenient().when(clientApi.listClients(any(), any(), any(), any(), any(),
+ eq(internalUserId), any(), any(), any(), any(), any(), any()))
+ .thenAnswer(invocation -> Mono.just(new PaginatedOASClientList().results(List.of())));
+ }
- StepVerifier.create(service.updateClient(uuid, update))
- .expectNext(updated)
- .verifyComplete();
+ private ClientCreate createdClient(UUID clientUuid, String internalUserId) {
+ return new ClientCreate(clientUuid).internalUserId(internalUserId);
}
}
-
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioServiceTest.java
index 2276346a3..06a925af4 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioServiceTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioServiceTest.java
@@ -740,6 +740,80 @@ void upsertPortfolios_emptyArrangements_returnsEmptyList() {
verifyNoInteractions(portfolioApi);
}
+
+ @Test
+ @DisplayName("single arrangement fails — skips failed arrangement and returns successful ones")
+ void upsertPortfolios_singleFailure_skipsFailedArrangement() {
+ UUID portfolioUuid = UUID.randomUUID();
+ UUID productId = UUID.randomUUID();
+ String successExternalId = "EXT-BATCH-OK";
+ String failExternalId = "EXT-BATCH-FAIL";
+ String leExternalId = "LE-BATCH";
+ UUID clientUuid = UUID.randomUUID();
+
+ InvestmentArrangement successArrangement = buildArrangement(
+ successExternalId, "Success Portfolio", productId, leExternalId);
+ InvestmentArrangement failArrangement = buildArrangement(
+ failExternalId, "Fail Portfolio", productId, leExternalId);
+
+ PaginatedPortfolioListList emptyList = Mockito.mock(PaginatedPortfolioListList.class);
+ when(emptyList.getResults()).thenReturn(List.of());
+ when(portfolioApi.listPortfolios(isNull(), isNull(), isNull(),
+ isNull(), eq(successExternalId), isNull(), isNull(), eq(1),
+ isNull(), isNull(), isNull(), isNull()))
+ .thenReturn(Mono.just(emptyList));
+ when(portfolioApi.listPortfolios(isNull(), isNull(), isNull(),
+ isNull(), eq(failExternalId), isNull(), isNull(), eq(1),
+ isNull(), isNull(), isNull(), isNull()))
+ .thenReturn(Mono.error(new RuntimeException("list portfolios failed")));
+
+ PortfolioList created = buildPortfolioList(portfolioUuid, successExternalId,
+ OffsetDateTime.now().minusMonths(6));
+ when(portfolioApi.createPortfolio(any(), isNull(), isNull(), isNull()))
+ .thenReturn(Mono.just(created));
+
+ Map> checkPriceAsyncTasksFinished(List
> upsertClients(List
> upsertPortfolios(List