FINERACT-2380: move retrofit client based test to feign

This commit is contained in:
Adam Saghy
2025-11-26 10:47:29 +01:00
parent 5d2a9c5390
commit f28382b70f
22 changed files with 98 additions and 158 deletions
@@ -40,9 +40,6 @@ public class LoanProductResolver {
log.debug("Resolving loan product by name [{}]", loanProductName);
List<GetLoanProductsResponse> loanProductsResponses = ok(() -> fineractClient.loanProducts().retrieveAllLoanProducts(Map.of()));
log.info("Retrieved {} loan products from API", loanProductsResponses.size());
log.info("Available loan products: {}", loanProductsResponses.stream().map(GetLoanProductsResponse::getName).toList());
GetLoanProductsResponse foundLpr = loanProductsResponses.stream().filter(lpr -> loanProductName.equals(lpr.getName())).findAny()
.orElseThrow(() -> new IllegalArgumentException("Loan product [%s] not found".formatted(loanProductName)));
return foundLpr.getId();
@@ -41,11 +41,10 @@ public class FineractInitializer implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
// ALWAYS log initializer counts at INFO level for debugging
log.info("=== FineractInitializer.afterPropertiesSet() called ===");
log.info("Global initializers count: {}", globalInitializerSteps.size());
log.info("Suite initializers count: {}", suiteInitializerSteps.size());
log.info("Scenario initializers count: {}", scenarioInitializerSteps.size());
log.debug("=== FineractInitializer.afterPropertiesSet() called ===");
log.debug("Global initializers count: {}", globalInitializerSteps.size());
log.debug("Suite initializers count: {}", suiteInitializerSteps.size());
log.debug("Scenario initializers count: {}", scenarioInitializerSteps.size());
if (log.isDebugEnabled()) {
String globalInitializers = globalInitializerSteps.stream().map(Object::getClass).map(Class::getName)
@@ -64,7 +63,7 @@ public class FineractInitializer implements InitializingBean {
// Always log the suite initializers at INFO since this is critical
String suiteInitializers = suiteInitializerSteps.stream().map(Object::getClass).map(Class::getName)
.collect(Collectors.joining(", "));
log.info("Suite initializers: [{}]", suiteInitializers);
log.debug("Suite initializers: [{}]", suiteInitializers);
}
}
@@ -76,9 +75,9 @@ public class FineractInitializer implements InitializingBean {
}
public void setupDefaultsForSuite() throws Exception {
log.info("=== setupDefaultsForSuite() called - {} suite initializers to execute ===", suiteInitializerSteps.size());
log.debug("=== setupDefaultsForSuite() called - {} suite initializers to execute ===", suiteInitializerSteps.size());
for (FineractSuiteInitializerStep initializerStep : suiteInitializerSteps) {
log.info("Executing suite initializer: {}", initializerStep.getClass().getName());
log.debug("Executing suite initializer: {}", initializerStep.getClass().getName());
initializerStep.initializeForSuite();
}
businessDateHelper.setBusinessDateToday();
@@ -271,7 +271,7 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
try {
executeVoid(() -> fineractClient.codeValues().createCodeValue(codeId, postCodeValuesDataRequest, Map.of()));
log.info("Code value '{}' created successfully", name);
log.debug("Code value '{}' created successfully", name);
} catch (CallFailedRuntimeException e) {
if (e.getStatus() == 403 && e.getDeveloperMessage() != null && e.getDeveloperMessage().contains("already exists")) {
log.debug("Code value '{}' already exists, skipping creation", name);
@@ -45,7 +45,7 @@ public class FinancialActivityMappingGlobalInitializerStep implements FineractGl
try {
executeVoid(() -> fineractClient.mappingFinancialActivitiesToAccounts().createGLAccount(request, Map.of()));
log.info("Financial activity mapping created successfully");
log.debug("Financial activity mapping created successfully");
} catch (CallFailedRuntimeException e) {
if (e.getStatus() == 403 && e.getDeveloperMessage() != null && e.getDeveloperMessage().contains("already exists")) {
log.debug("Financial activity mapping already exists, skipping creation");
@@ -4329,14 +4329,14 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
private PostLoanProductsResponse createLoanProductIdempotent(PostLoanProductsRequest loanProductRequest) {
String productName = loanProductRequest.getName();
log.info("Attempting to create loan product: {}", productName);
log.debug("Attempting to create loan product: {}", productName);
try {
List<GetLoanProductsResponse> existingProducts = fineractClient.loanProducts().retrieveAllLoanProducts(Map.of());
GetLoanProductsResponse existingProduct = existingProducts.stream().filter(p -> productName.equals(p.getName())).findFirst()
.orElse(null);
if (existingProduct != null) {
log.info("Loan product '{}' already exists with ID: {}", productName, existingProduct.getId());
log.debug("Loan product '{}' already exists with ID: {}", productName, existingProduct.getId());
PostLoanProductsResponse response = new PostLoanProductsResponse();
response.setResourceId(existingProduct.getId());
return response;
@@ -4345,10 +4345,10 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
log.warn("Error checking if loan product '{}' exists", productName, e);
}
log.info("Creating new loan product: {}", productName);
log.debug("Creating new loan product: {}", productName);
try {
PostLoanProductsResponse response = ok(() -> fineractClient.loanProducts().createLoanProduct(loanProductRequest, Map.of()));
log.info("Successfully created loan product '{}' with ID: {}", productName, response.getResourceId());
log.debug("Successfully created loan product '{}' with ID: {}", productName, response.getResourceId());
return response;
} catch (Exception e) {
log.error("FAILED to create loan product '{}'", productName, e);
@@ -55,7 +55,7 @@ public class ExternalEventSuiteInitializerStep implements FineractSuiteInitializ
@Override
public void initializeForSuite() throws InterruptedException {
log.info("=== ExternalEventSuiteInitializerStep.initializeForSuite() - START ===");
log.debug("=== ExternalEventSuiteInitializerStep.initializeForSuite() - START ===");
// Step 1: Enable all external events
Map<String, Boolean> eventConfigMap = new HashMap<>();
@@ -72,7 +72,7 @@ public class ExternalEventSuiteInitializerStep implements FineractSuiteInitializ
.externalEventConfigurations(eventConfigMap);
executeVoid(() -> fineractClient.externalEventConfiguration().updateExternalEventConfigurations(null, request, Map.of()));
log.info("=== External event configuration updated - all events enabled ===");
log.debug("=== External event configuration updated - all events enabled ===");
// Step 2: Wait for JMS Listener to be ready before proceeding
if (eventProperties != null && eventProperties.isEventVerificationEnabled()) {
@@ -93,6 +93,6 @@ public class ExternalEventSuiteInitializerStep implements FineractSuiteInitializ
}
}
log.info("=== ExternalEventSuiteInitializerStep.initializeForSuite() - COMPLETED ===");
log.debug("=== ExternalEventSuiteInitializerStep.initializeForSuite() - COMPLETED ===");
}
}
@@ -40,44 +40,44 @@ public class JobSuiteInitializerStep implements FineractSuiteInitializerStep {
private final FineractFeignClient fineractClient;
public JobSuiteInitializerStep(FineractFeignClient fineractClient) {
log.info("=== JobSuiteInitializerStep: Constructor called - bean is being created ===");
log.debug("=== JobSuiteInitializerStep: Constructor called - bean is being created ===");
this.fineractClient = fineractClient;
log.info("=== JobSuiteInitializerStep: FineractFeignClient injected successfully ===");
log.debug("=== JobSuiteInitializerStep: FineractFeignClient injected successfully ===");
}
@Override
public void initializeForSuite() throws InterruptedException {
log.info("=== JobSuiteInitializerStep.initializeForSuite() - START ===");
log.debug("=== JobSuiteInitializerStep.initializeForSuite() - START ===");
enableAndExecuteEventJob();
log.info("=== JobSuiteInitializerStep.initializeForSuite() - COMPLETED successfully ===");
log.debug("=== JobSuiteInitializerStep.initializeForSuite() - COMPLETED successfully ===");
}
private void enableAndExecuteEventJob() throws InterruptedException {
log.info("=== Initializing Send Asynchronous Events job ===");
log.debug("=== Initializing Send Asynchronous Events job ===");
Long jobId = updateExternalEventJobFrequency(EVERY_1_SECONDS);
log.info("=== Updated cron expression to EVERY_1_SECONDS ===");
log.debug("=== Updated cron expression to EVERY_1_SECONDS ===");
// CRITICAL: SchedulerGlobalInitializerStep stops the scheduler globally
// Solution: START the scheduler so the job runs every 1 second automatically
log.info("Starting scheduler to enable automatic job execution every 1 second...");
log.debug("Starting scheduler to enable automatic job execution every 1 second...");
executeVoid(() -> fineractClient.scheduler().changeSchedulerStatus("start", Map.of()));
log.info("Scheduler started successfully");
log.debug("Scheduler started successfully");
// Manually execute once immediately to publish any queued events from initialization
log.info("Manually executing '{}' job once to publish queued events...", SEND_ASYNCHRONOUS_EVENTS_JOB_NAME);
log.debug("Manually executing '{}' job once to publish queued events...", SEND_ASYNCHRONOUS_EVENTS_JOB_NAME);
executeVoid(() -> fineractClient.schedulerJob().executeJob(jobId, new ExecuteJobRequest(), Map.of("command", "executeJob")));
// Poll job history to confirm it ran
log.info("Polling job history to confirm initial execution...");
log.debug("Polling job history to confirm initial execution...");
Long initialRunCount = getJobRunCount(jobId);
log.info("Initial job run count: {}", initialRunCount);
log.debug("Initial job run count: {}", initialRunCount);
boolean jobRan = false;
for (int i = 0; i < 30; i++) {
Thread.sleep(200);
Long currentRunCount = getJobRunCount(jobId);
if (currentRunCount > initialRunCount) {
log.info("Job execution confirmed! Run count increased from {} to {}", initialRunCount, currentRunCount);
log.debug("Job execution confirmed! Run count increased from {} to {}", initialRunCount, currentRunCount);
jobRan = true;
break;
}
@@ -88,9 +88,9 @@ public class JobSuiteInitializerStep implements FineractSuiteInitializerStep {
}
// Wait for events to propagate to ActiveMQ
log.info("Waiting 1 second for event propagation to ActiveMQ...");
log.debug("Waiting 1 second for event propagation to ActiveMQ...");
Thread.sleep(1000);
log.info("Scheduler is now running - job will execute every 1 second automatically");
log.debug("Scheduler is now running - job will execute every 1 second automatically");
}
private Long getJobRunCount(Long jobId) {
@@ -105,20 +105,20 @@ public class JobSuiteInitializerStep implements FineractSuiteInitializerStep {
@Override
public void resetAfterSuite() {
log.info("=== JobSuiteInitializerStep.resetAfterSuite() - START ===");
log.debug("=== JobSuiteInitializerStep.resetAfterSuite() - START ===");
// Stop the scheduler to prevent jobs from running between test suites
log.info("Stopping scheduler...");
log.debug("Stopping scheduler...");
try {
executeVoid(() -> fineractClient.scheduler().changeSchedulerStatus(Map.of("command", "stop")));
log.info("Scheduler stopped successfully");
log.debug("Scheduler stopped successfully");
} catch (Exception e) {
log.warn("Failed to stop scheduler: {}", e.getMessage());
}
// Reset cron expression to default
updateExternalEventJobFrequency(EVERY_60_SECONDS);
log.info("=== JobSuiteInitializerStep.resetAfterSuite() - COMPLETED ===");
log.debug("=== JobSuiteInitializerStep.resetAfterSuite() - COMPLETED ===");
}
private Long updateExternalEventJobFrequency(String cronExpression) {
@@ -89,7 +89,7 @@ import org.springframework.stereotype.Component;
public class EventCheckHelper {
private static final DateTimeFormatter FORMATTER_EVENTS = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final long TRANSACTION_COMMIT_DELAY_MS = 500L;
private static final long TRANSACTION_COMMIT_DELAY_MS = 100L;
@Autowired
private FineractFeignClient fineractClient;
@@ -46,7 +46,6 @@ import java.io.IOException;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -54,7 +53,6 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.feign.services.ExternalAssetOwnerLoanProductAttributesApi;
import org.apache.fineract.client.feign.services.ExternalAssetOwnersApi;
import org.apache.fineract.client.feign.services.ExternalAssetOwnersApiExtension;
import org.apache.fineract.client.feign.services.LoanProductsApi;
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
import org.apache.fineract.client.models.ExternalAssetOwnerRequest;
@@ -104,10 +102,6 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
return fineractFeignClient.externalAssetOwners();
}
private ExternalAssetOwnersApiExtension externalAssetOwnersApiExtension() {
return fineractFeignClient.externalAssetOwnersExtension();
}
private LoanProductsApi loanProductsApi() {
return fineractFeignClient.loanProducts();
}
@@ -152,8 +146,8 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
.dateFormat(DATE_FORMAT_ASSET_EXT)//
.locale(DEFAULT_LOCALE);//
PostInitiateTransferResponse response = externalAssetOwnersApi().transferRequestWithLoanId(loanId, request, transferData.get(0),
Collections.emptyMap());
PostInitiateTransferResponse response = externalAssetOwnersApi().transferRequestWithLoanId(loanId, request,
Map.of("command", transferData.get(0)));
testContext().set(TestContextKey.ASSET_EXTERNALIZATION_RESPONSE, response);
testContext().set(TestContextKey.ASSET_EXTERNALIZATION_BUYBACK_TRANSFER_EXTERNAL_ID_FROM_RESPONSE,
response.getResourceExternalId());
@@ -809,9 +803,7 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
String transferExternalId = testContext()
.get(TestContextKey.ASSET_EXTERNALIZATION_TRANSFER_EXTERNAL_ID_USER_GENERATED + "_" + type);
ExternalAssetOwnerRequest request = new ExternalAssetOwnerRequest().dateFormat(DATE_FORMAT_ASSET_EXT).locale(DEFAULT_LOCALE);
externalAssetOwnersApiExtension().transferRequestWithId1WithBody(transferExternalId, request, Map.of("command", command));
externalAssetOwnersApi().transferRequestWithId1(transferExternalId, Map.of("command", command));
}
@When("Admin send {string} command to the transaction type {string} will throw error")
@@ -819,10 +811,8 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
String transferExternalId = testContext()
.get(TestContextKey.ASSET_EXTERNALIZATION_TRANSFER_EXTERNAL_ID_USER_GENERATED + "_" + type);
ExternalAssetOwnerRequest request = new ExternalAssetOwnerRequest().dateFormat(DATE_FORMAT_ASSET_EXT).locale(DEFAULT_LOCALE);
CallFailedRuntimeException exception = fail(() -> externalAssetOwnersApiExtension()
.transferRequestWithId1WithBody(transferExternalId, request, Map.of("command", command)));
CallFailedRuntimeException exception = fail(
() -> externalAssetOwnersApi().transferRequestWithId1(transferExternalId, Map.of("command", command)));
assertThat(exception.getStatus()).as("Expected status code: 403").isEqualTo(403);
}
@@ -883,10 +873,8 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
transferExternalId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_SALES_TRANSFER_EXTERNAL_ID_FROM_RESPONSE);
}
ExternalAssetOwnerRequest request = new ExternalAssetOwnerRequest().dateFormat(DATE_FORMAT_ASSET_EXT).locale(DEFAULT_LOCALE);
CallFailedRuntimeException exception = fail(() -> externalAssetOwnersApiExtension()
.transferRequestWithId1WithBody(transferExternalId, request, Map.of("command", command)));
CallFailedRuntimeException exception = fail(
() -> externalAssetOwnersApi().transferRequestWithId1(transferExternalId, Map.of("command", command)));
assertThat(exception.getStatus()).as("Expected status code: 403").isEqualTo(403);
}
@@ -900,9 +888,7 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
transferExternalId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_SALES_TRANSFER_EXTERNAL_ID_FROM_RESPONSE);
}
ExternalAssetOwnerRequest request = new ExternalAssetOwnerRequest().dateFormat(DATE_FORMAT_ASSET_EXT).locale(DEFAULT_LOCALE);
externalAssetOwnersApiExtension().transferRequestWithId1WithBody(transferExternalId, request, Map.of("command", command));
externalAssetOwnersApi().transferRequestWithId1(transferExternalId, Map.of("command", command));
}
@When("Admin set external asset owner loan product attribute {string} value {string} for loan product {string}")
@@ -27,6 +27,7 @@ import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@@ -39,6 +40,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.fineract.avro.loan.v1.LoanSchedulePeriodDataV1;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.feign.services.BatchApiApi;
@@ -50,6 +52,7 @@ import org.apache.fineract.client.models.GetClientsClientIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdStatus;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetUsersUserIdResponse;
import org.apache.fineract.client.models.Header;
import org.apache.fineract.client.models.InlineJobRequest;
import org.apache.fineract.client.models.PostClientsRequest;
@@ -61,6 +64,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
import org.apache.fineract.client.models.PostUsersResponse;
import org.apache.fineract.test.data.ChargeProductType;
import org.apache.fineract.test.data.LoanRescheduleErrorMessage;
import org.apache.fineract.test.data.LoanStatus;
@@ -543,10 +547,18 @@ public class BatchApiStepDef extends AbstractStepDef {
// Feign throws exceptions on errors instead of returning error in response body
ErrorResponse errorResponse = null;
Map<String, String> headerMap = new HashMap<>();
// Create new user which cannot bypass loan COB execution
PostUsersResponse createUserResponse = testContext().get(TestContextKey.CREATED_SIMPLE_USER_RESPONSE);
Long createdUserId = createUserResponse.getResourceId();
GetUsersUserIdResponse user = fineractFeignClient.users().retrieveOne31(createdUserId);
String authorizationString = user.getUsername() + ":" + PWD_USER_WITH_ROLE;
Base64 base64 = new Base64();
headerMap.put("Authorization",
"Basic " + new String(base64.encode(authorizationString.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8));
try {
// TODO: Feign doesn't support per-request headers - need to use RequestInterceptor
batchApiApi().handleBatchRequests(requestList, queryParams);
throw new IllegalStateException("Expected Feign exception but call succeeded");
batchApiApi().handleBatchRequests(requestList, queryParams, headerMap);
} catch (org.apache.fineract.client.feign.FeignException e) {
errorResponse = fromJson(e.responseBodyAsString(), ErrorResponse.class);
}
@@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.models.LoanAccountLock;
import org.apache.fineract.client.models.LoanAccountLockResponseDTO;
import org.apache.fineract.client.models.LockRequest;
import org.apache.fineract.client.models.OldestCOBProcessedLoanDTO;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.test.helper.ErrorMessageHelper;
@@ -90,7 +91,8 @@ public class LoanCOBStepDef extends AbstractStepDef {
PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanResponse.getLoanId();
executeVoid(() -> fineractClient.defaultApi().placeLockOnLoanAccount(loanId, "LOAN_COB_CHUNK_PROCESSING", "TestError"));
executeVoid(() -> fineractClient.defaultApi().placeLockOnLoanAccount(loanId, "LOAN_COB_CHUNK_PROCESSING",
new LockRequest().error("ERROR")));
}
@When("Admin places a lock on loan account WITHOUT an error message")
@@ -98,6 +100,6 @@ public class LoanCOBStepDef extends AbstractStepDef {
PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanResponse.getLoanId();
executeVoid(() -> fineractClient.defaultApi().placeLockOnLoanAccount(loanId, "LOAN_COB_CHUNK_PROCESSING", ""));
executeVoid(() -> fineractClient.defaultApi().placeLockOnLoanAccount(loanId, "LOAN_COB_CHUNK_PROCESSING", new LockRequest()));
}
}
@@ -75,6 +75,7 @@ import org.apache.fineract.client.models.ApiResponse;
import org.apache.fineract.client.models.BusinessDateResponse;
import org.apache.fineract.client.models.BuyDownFeeAmortizationDetails;
import org.apache.fineract.client.models.CapitalizedIncomeDetails;
import org.apache.fineract.client.models.CommandProcessingResult;
import org.apache.fineract.client.models.DeleteLoansLoanIdResponse;
import org.apache.fineract.client.models.DisbursementDetail;
import org.apache.fineract.client.models.GetCodeValuesDataResponse;
@@ -1626,8 +1627,8 @@ public class LoanStepDef extends AbstractStepDef {
PostAddAndDeleteDisbursementDetailRequest disbursementDetailRequest = LoanRequestFactory
.defaultLoanDisbursementDetailRequest(disbursementData);
PostAddAndDeleteDisbursementDetailRequest loanDisburseResponse = ok(
() -> fineractClient.loanDisbursementDetailsExtension().addAndDeleteDisbursementDetail(loanId, disbursementDetailRequest));
CommandProcessingResult loanDisburseResponse = ok(
() -> fineractClient.loanDisbursementDetails().addAndDeleteDisbursementDetail(loanId, disbursementDetailRequest));
testContext().set(TestContextKey.LOAN_DISBURSEMENT_DETAIL_RESPONSE, loanDisburseResponse);
}