FINERACT-2380: create feign client
This commit is contained in:
@@ -44,6 +44,7 @@ buildscript {
|
||||
'twofactor-tests',
|
||||
'oauth2-tests',
|
||||
'fineract-client',
|
||||
'fineract-client-feign',
|
||||
'fineract-avro-schemas',
|
||||
'fineract-e2e-tests-core',
|
||||
'fineract-e2e-tests-runner',
|
||||
@@ -55,6 +56,7 @@ buildscript {
|
||||
[
|
||||
'fineract-avro-schemas',
|
||||
'fineract-client',
|
||||
'fineract-client-feign',
|
||||
'fineract-core',
|
||||
'fineract-cob',
|
||||
'fineract-validation',
|
||||
@@ -558,6 +560,9 @@ configure(project.fineractJavaProjects) {
|
||||
if (project.path == ':fineract-client') {
|
||||
excludedPaths = '.*/build/generated/java/src/main/java/.*'
|
||||
}
|
||||
if (project.path == ':fineract-client-feign') {
|
||||
excludedPaths = '.*/build/generated/java/src/main/java/.*'
|
||||
}
|
||||
disable(
|
||||
// TODO Remove disabled checks from this list, by fixing remaining usages
|
||||
"UnusedVariable",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
**/README.md
|
||||
**/pom.xml
|
||||
**/build.sbt
|
||||
**/*.gradle
|
||||
**/.gitignore
|
||||
**/git_push.sh
|
||||
**/api/*
|
||||
**/gradle*
|
||||
**/gradle
|
||||
**/src/main/AndroidManifest.xml
|
||||
|
||||
# https://issues.apache.org/jira/browse/FINERACT-1231
|
||||
**/feign/*.java
|
||||
!**/feign/CollectionFormats.java
|
||||
!**/feign/StringUtil.java
|
||||
|
||||
# Manual API overrides - do not regenerate
|
||||
# https://issues.apache.org/jira/browse/FINERACT-1263
|
||||
**/services/RunReportsApi.java
|
||||
**/services/ImagesApi.java
|
||||
**/services/DocumentsApiFixed.java
|
||||
|
||||
# Utility classes - do not regenerate
|
||||
**/util/*.java
|
||||
**/adapter/*.java
|
||||
|
||||
# Feign configuration - do not regenerate
|
||||
**/feign/FineractFeignClient.java
|
||||
**/feign/FineractFeignClientConfig.java
|
||||
**/feign/BasicAuthRequestInterceptor.java
|
||||
**/feign/ObjectMapperFactory.java
|
||||
**/feign/FeignException.java
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
apply plugin: 'org.openapi.generator'
|
||||
apply plugin: 'jacoco'
|
||||
description = 'Fineract Client with Feign'
|
||||
|
||||
apply from: 'dependencies.gradle'
|
||||
|
||||
openApiMeta {
|
||||
generatorName = 'Fineract-Feign'
|
||||
packageName = 'org.apache.fineract.client.feign'
|
||||
outputFolder = "$buildDir/meta".toString()
|
||||
}
|
||||
|
||||
openApiValidate {
|
||||
inputSpec = "file:///$swaggerFile"
|
||||
recommend = true
|
||||
}
|
||||
|
||||
tasks.register('buildJavaSdk', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) {
|
||||
generatorName = 'java'
|
||||
library = 'feign'
|
||||
verbose = false
|
||||
validateSpec = false
|
||||
skipValidateSpec = true
|
||||
inputSpec = "file:///$swaggerFile"
|
||||
outputDir = "$buildDir/generated/temp-java".toString()
|
||||
templateDir = "$projectDir/src/main/resources/templates/java"
|
||||
groupId = 'org.apache.fineract'
|
||||
apiPackage = 'org.apache.fineract.client.feign.services'
|
||||
invokerPackage = 'org.apache.fineract.client.feign'
|
||||
modelPackage = 'org.apache.fineract.client.models'
|
||||
generateModelTests = false
|
||||
generateApiTests = false
|
||||
ignoreFileOverride = "$projectDir/.openapi-generator-ignore"
|
||||
configOptions = [
|
||||
dateLibrary : 'java8',
|
||||
library : 'feign',
|
||||
feignVersion : '13.6',
|
||||
feignApacheHttpClient : 'true',
|
||||
useFeign13 : 'true',
|
||||
useFeignApacheHttpClient : 'true',
|
||||
hideGenerationTimestamp : 'true',
|
||||
containerDefaultToNull : 'true',
|
||||
oauth2Implementation : 'none',
|
||||
useJakartaEe : 'true'
|
||||
|
||||
]
|
||||
dependsOn(':fineract-provider:resolve')
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDirs = [
|
||||
new File(buildDir, "generated/java/src/main/java"),
|
||||
"$projectDir/src/main/java"
|
||||
]
|
||||
destinationDirectory = layout.buildDirectory.dir('classes/java/main').get().asFile
|
||||
}
|
||||
output.resourcesDir = layout.buildDirectory.dir('resources/main').get().asFile
|
||||
}
|
||||
test {
|
||||
java {
|
||||
destinationDirectory = layout.buildDirectory.dir('classes/java/test').get().asFile
|
||||
}
|
||||
output.resourcesDir = layout.buildDirectory.dir('resources/test').get().asFile
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Jar).configureEach {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
task cleanupGeneratedJavaFiles() {
|
||||
def tempDir = file("$buildDir/generated/temp-java")
|
||||
def targetDir = file("$buildDir/generated/java")
|
||||
|
||||
inputs.dir(tempDir)
|
||||
outputs.dir(targetDir)
|
||||
|
||||
doLast {
|
||||
copy {
|
||||
from tempDir
|
||||
into targetDir
|
||||
filter { line ->
|
||||
line
|
||||
.replaceAll(", \\)", ")")
|
||||
.replaceAll(", , @HeaderMap", ", @HeaderMap")
|
||||
.replaceAll("\\(, ", "(")
|
||||
}
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
}
|
||||
dependsOn("buildJavaSdk")
|
||||
}
|
||||
|
||||
tasks.named('compileJava') {
|
||||
outputs.cacheIf { true }
|
||||
dependsOn(buildJavaSdk, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply)
|
||||
mustRunAfter(licenseFormatMain, cleanupGeneratedJavaFiles)
|
||||
}
|
||||
|
||||
tasks.named('sourcesJar') {
|
||||
dependsOn(cleanupGeneratedJavaFiles)
|
||||
mustRunAfter(cleanupGeneratedJavaFiles)
|
||||
|
||||
from(sourceSets.main.java.srcDirs) {
|
||||
include "**/*.java"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('licenseFormatMain') {
|
||||
dependsOn(cleanupGeneratedJavaFiles)
|
||||
mustRunAfter(cleanupGeneratedJavaFiles)
|
||||
source = sourceSets.main.java.srcDirs
|
||||
}
|
||||
|
||||
tasks.named('licenseMain') {
|
||||
dependsOn(licenseFormatMain)
|
||||
mustRunAfter(licenseFormatMain)
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs << '-parameters'
|
||||
options.errorprone {
|
||||
excludedPaths = '.*/build/generated/java/src/main/java/.*'
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
configurations {
|
||||
generatedCompileClasspath.extendsFrom implementation
|
||||
generatedRuntimeClasspath.extendsFrom runtimeClasspath
|
||||
}
|
||||
|
||||
javadoc {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
spotbugsMain {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
spotbugsTest {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.11"
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test
|
||||
reports {
|
||||
xml.required = true
|
||||
html.required = true
|
||||
csv.required = false
|
||||
}
|
||||
afterEvaluate {
|
||||
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||
fileTree(dir: it, exclude: [
|
||||
'**/build/generated/**',
|
||||
'**/org/apache/fineract/client/models/**',
|
||||
'**/org/apache/fineract/client/services/**Api.class',
|
||||
'**/org/apache/fineract/client/auth/**'
|
||||
])
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
dependencies {
|
||||
// Feign dependencies
|
||||
implementation(
|
||||
'io.github.openfeign:feign-core:13.6',
|
||||
'io.github.openfeign:feign-jackson:13.6',
|
||||
'io.github.openfeign:feign-slf4j:13.6',
|
||||
'io.github.openfeign:feign-hc5:13.6',
|
||||
'io.github.openfeign:feign-okhttp:13.6',
|
||||
'io.github.openfeign.form:feign-form:3.8.0',
|
||||
'org.apache.httpcomponents.client5:httpclient5:5.2.1',
|
||||
'com.squareup.okhttp3:okhttp:4.12.0',
|
||||
'com.fasterxml.jackson.core:jackson-databind',
|
||||
'com.fasterxml.jackson.datatype:jackson-datatype-jsr310',
|
||||
'com.fasterxml.jackson.datatype:jackson-datatype-jdk8',
|
||||
'jakarta.annotation:jakarta.annotation-api:3.0.0',
|
||||
'io.swagger.core.v3:swagger-annotations-jakarta:2.2.15',
|
||||
'org.apache.commons:commons-lang3:3.12.0',
|
||||
'org.slf4j:slf4j-api:1.7.36',
|
||||
'org.projectlombok:lombok'
|
||||
)
|
||||
|
||||
// Test dependencies
|
||||
testImplementation(
|
||||
'org.junit.jupiter:junit-jupiter-api:5.11.3',
|
||||
'org.junit.jupiter:junit-jupiter-engine:5.11.3',
|
||||
'org.mockito:mockito-core:5.14.2',
|
||||
'org.assertj:assertj-core:3.26.3',
|
||||
'org.slf4j:slf4j-simple:1.7.36',
|
||||
'org.wiremock:wiremock-standalone'
|
||||
)
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.adapter;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import java.io.IOException;
|
||||
import org.apache.fineract.client.models.ExternalId;
|
||||
|
||||
/**
|
||||
* Custom Jackson adapter for ExternalId type serialization and deserialization. This adapter ensures that ExternalId
|
||||
* objects are properly serialized to their string value and deserialized from string values.
|
||||
*/
|
||||
public final class ExternalIdAdapter {
|
||||
|
||||
private ExternalIdAdapter() {}
|
||||
|
||||
/**
|
||||
* Jackson Serializer for ExternalId. Serializes an ExternalId object to its string value, or null if the ExternalId
|
||||
* or its value is null.
|
||||
*/
|
||||
public static class Serializer extends JsonSerializer<ExternalId> {
|
||||
|
||||
@Override
|
||||
public void serialize(ExternalId value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
if (value == null || value.getValue() == null) {
|
||||
gen.writeNull();
|
||||
} else {
|
||||
gen.writeString(value.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jackson Deserializer for ExternalId. Deserializes a string value to an ExternalId object, or null if the input is
|
||||
* null.
|
||||
*/
|
||||
public static class Deserializer extends JsonDeserializer<ExternalId> {
|
||||
|
||||
@Override
|
||||
public ExternalId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
String value = p.getValueAsString();
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
ExternalId externalId = new ExternalId();
|
||||
externalId.setValue(value);
|
||||
return externalId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Jackson SimpleModule configured with the ExternalId serializer and deserializer.
|
||||
*
|
||||
* @return A configured SimpleModule ready to be registered with an ObjectMapper
|
||||
*/
|
||||
public static SimpleModule createModule() {
|
||||
SimpleModule module = new SimpleModule("ExternalIdModule");
|
||||
module.addSerializer(ExternalId.class, new Serializer());
|
||||
module.addDeserializer(ExternalId.class, new Deserializer());
|
||||
return module;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Request interceptor that adds Basic Authentication header to requests.
|
||||
*/
|
||||
public class BasicAuthRequestInterceptor implements RequestInterceptor {
|
||||
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String BASIC_AUTH_PREFIX = "Basic ";
|
||||
|
||||
private final String credentials;
|
||||
|
||||
/**
|
||||
* Creates a new BasicAuthRequestInterceptor with the specified credentials.
|
||||
*
|
||||
* @param username
|
||||
* the username for authentication
|
||||
* @param password
|
||||
* the password for authentication
|
||||
*/
|
||||
public BasicAuthRequestInterceptor(String username, String password) {
|
||||
if (username == null || password == null) {
|
||||
throw new IllegalArgumentException("Username and password cannot be null");
|
||||
}
|
||||
String auth = username + ":" + password;
|
||||
this.credentials = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
template.header(AUTHORIZATION_HEADER, BASIC_AUTH_PREFIX + credentials);
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import feign.Request;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Base exception class for Feign client exceptions.
|
||||
*/
|
||||
public class FeignException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final int status;
|
||||
private final Request request;
|
||||
private final byte[] responseBody;
|
||||
private final String developerMessage;
|
||||
private final String userMessage;
|
||||
|
||||
protected FeignException(int status, String message, Request request) {
|
||||
this(status, message, request, (byte[]) null);
|
||||
}
|
||||
|
||||
protected FeignException(int status, String message, Request request, Throwable cause) {
|
||||
this(status, message, request, null, cause);
|
||||
}
|
||||
|
||||
protected FeignException(int status, String message, Request request, byte[] responseBody) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.request = request;
|
||||
this.responseBody = responseBody;
|
||||
this.developerMessage = null;
|
||||
this.userMessage = null;
|
||||
}
|
||||
|
||||
protected FeignException(int status, String message, Request request, byte[] responseBody, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.status = status;
|
||||
this.request = request;
|
||||
this.responseBody = responseBody;
|
||||
this.developerMessage = null;
|
||||
this.userMessage = null;
|
||||
}
|
||||
|
||||
public FeignException(int status, String message, Request request, byte[] responseBody, String developerMessage, String userMessage) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.request = request;
|
||||
this.responseBody = responseBody;
|
||||
this.developerMessage = developerMessage;
|
||||
this.userMessage = userMessage;
|
||||
}
|
||||
|
||||
public int status() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public Request request() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public byte[] responseBody() {
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
public String responseBodyAsString() {
|
||||
return responseBody != null ? new String(responseBody, Charset.defaultCharset()) : null;
|
||||
}
|
||||
|
||||
public String getDeveloperMessage() {
|
||||
return developerMessage;
|
||||
}
|
||||
|
||||
public String getUserMessage() {
|
||||
return userMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("status ").append(status);
|
||||
|
||||
if (userMessage != null) {
|
||||
sb.append(": ").append(userMessage);
|
||||
}
|
||||
|
||||
if (developerMessage != null) {
|
||||
sb.append(" (").append(developerMessage).append(")");
|
||||
}
|
||||
|
||||
if (super.getMessage() != null && userMessage == null && developerMessage == null) {
|
||||
sb.append(": ").append(super.getMessage());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.Response;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class FineractErrorDecoder implements ErrorDecoder {
|
||||
|
||||
private final ErrorDecoder defaultDecoder = new Default();
|
||||
private final ObjectMapper objectMapper = ObjectMapperFactory.getShared();
|
||||
|
||||
@Override
|
||||
public Exception decode(String methodKey, Response response) {
|
||||
try {
|
||||
if (response.body() != null) {
|
||||
byte[] bodyData = readResponseBody(response);
|
||||
|
||||
try {
|
||||
JsonNode rootNode = objectMapper.readTree(bodyData);
|
||||
|
||||
String developerMessage = extractField(rootNode, "developerMessage");
|
||||
String userMessage = extractField(rootNode, "userMessage");
|
||||
String validationErrors = extractValidationErrors(rootNode);
|
||||
|
||||
if (developerMessage != null || userMessage != null || validationErrors != null) {
|
||||
String enhancedDeveloperMessage = developerMessage;
|
||||
if (validationErrors != null) {
|
||||
enhancedDeveloperMessage = validationErrors;
|
||||
}
|
||||
return new FeignException(response.status(), userMessage != null ? userMessage : enhancedDeveloperMessage,
|
||||
response.request(), bodyData, enhancedDeveloperMessage, userMessage);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return defaultDecoder.decode(methodKey, response);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return defaultDecoder.decode(methodKey, response);
|
||||
}
|
||||
|
||||
return defaultDecoder.decode(methodKey, response);
|
||||
}
|
||||
|
||||
private byte[] readResponseBody(Response response) throws IOException {
|
||||
if (response.body() == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
try (InputStream inputStream = response.body().asInputStream()) {
|
||||
return inputStream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private String extractField(JsonNode node, String fieldName) {
|
||||
JsonNode fieldNode = node.get(fieldName);
|
||||
return fieldNode != null && !fieldNode.isNull() ? fieldNode.asText() : null;
|
||||
}
|
||||
|
||||
private String extractValidationErrors(JsonNode rootNode) {
|
||||
JsonNode errorsNode = rootNode.get("errors");
|
||||
if (errorsNode != null && errorsNode.isArray() && errorsNode.size() > 0) {
|
||||
StringBuilder errors = new StringBuilder("Validation errors: ");
|
||||
for (JsonNode error : errorsNode) {
|
||||
String parameterName = extractField(error, "parameterName");
|
||||
String defaultUserMessage = extractField(error, "defaultUserMessage");
|
||||
String developerMessage = extractField(error, "developerMessage");
|
||||
|
||||
if (errors.length() > "Validation errors: ".length()) {
|
||||
errors.append("; ");
|
||||
}
|
||||
|
||||
if (parameterName != null) {
|
||||
errors.append("[").append(parameterName).append("] ");
|
||||
}
|
||||
|
||||
if (defaultUserMessage != null) {
|
||||
errors.append(defaultUserMessage);
|
||||
} else if (developerMessage != null) {
|
||||
errors.append(developerMessage);
|
||||
}
|
||||
}
|
||||
return errors.length() > "Validation errors: ".length() ? errors.toString() : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+1002
File diff suppressed because it is too large
Load Diff
+293
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
import feign.Request;
|
||||
import feign.Retryer;
|
||||
import feign.codec.Encoder;
|
||||
import feign.hc5.ApacheHttp5Client;
|
||||
import feign.jackson.JacksonDecoder;
|
||||
import feign.jackson.JacksonEncoder;
|
||||
import feign.slf4j.Slf4jLogger;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
|
||||
import org.apache.hc.core5.util.TimeValue;
|
||||
import org.apache.hc.core5.util.Timeout;
|
||||
|
||||
/**
|
||||
* Configuration class for Feign client.
|
||||
*/
|
||||
public final class FineractFeignClientConfig {
|
||||
|
||||
public enum HttpClientType {
|
||||
APACHE, OKHTTP
|
||||
}
|
||||
|
||||
private final String baseUrl;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String tenantId;
|
||||
private final int connectTimeout;
|
||||
private final int readTimeout;
|
||||
private final boolean debugEnabled;
|
||||
private final long connectionTimeToLive;
|
||||
private final TimeUnit connectionTimeToLiveUnit;
|
||||
private final boolean disableSslVerification;
|
||||
private final int maxConnTotal;
|
||||
private final int maxConnPerRoute;
|
||||
private final long idleConnectionEvictionTime;
|
||||
private final TimeUnit idleConnectionEvictionTimeUnit;
|
||||
private final HttpClientType clientType;
|
||||
private volatile Client cachedHttpClient;
|
||||
|
||||
private FineractFeignClientConfig(Builder builder) {
|
||||
this.baseUrl = builder.baseUrl;
|
||||
this.username = builder.username;
|
||||
this.password = builder.password;
|
||||
this.tenantId = builder.tenantId;
|
||||
this.connectTimeout = builder.connectTimeout;
|
||||
this.readTimeout = builder.readTimeout;
|
||||
this.debugEnabled = builder.debugEnabled;
|
||||
this.connectionTimeToLive = builder.connectionTimeToLive;
|
||||
this.connectionTimeToLiveUnit = builder.connectionTimeToLiveUnit;
|
||||
this.disableSslVerification = builder.disableSslVerification;
|
||||
this.maxConnTotal = builder.maxConnTotal;
|
||||
this.maxConnPerRoute = builder.maxConnPerRoute;
|
||||
this.idleConnectionEvictionTime = builder.idleConnectionEvictionTime;
|
||||
this.idleConnectionEvictionTimeUnit = builder.idleConnectionEvictionTimeUnit;
|
||||
this.clientType = builder.clientType;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public <T> T createClient(Class<T> apiType) {
|
||||
JacksonEncoder jacksonEncoder = new JacksonEncoder(ObjectMapperFactory.getShared());
|
||||
Encoder multipartEncoder = new FineractMultipartEncoder(jacksonEncoder);
|
||||
|
||||
return Feign.builder().client(getOrCreateHttpClient()).encoder(multipartEncoder)
|
||||
.decoder(new JacksonDecoder(ObjectMapperFactory.getShared())).errorDecoder(new FineractErrorDecoder())
|
||||
.options(new Request.Options(connectTimeout, TimeUnit.MILLISECONDS, readTimeout, TimeUnit.MILLISECONDS, true))
|
||||
.retryer(Retryer.NEVER_RETRY).requestInterceptor(new BasicAuthRequestInterceptor(username, password))
|
||||
.requestInterceptor(new TenantIdRequestInterceptor(tenantId)).logger(new Slf4jLogger(apiType))
|
||||
.logLevel(debugEnabled ? feign.Logger.Level.FULL : feign.Logger.Level.BASIC).target(apiType, baseUrl);
|
||||
}
|
||||
|
||||
private Client getOrCreateHttpClient() {
|
||||
if (cachedHttpClient == null) {
|
||||
synchronized (this) {
|
||||
if (cachedHttpClient == null) {
|
||||
cachedHttpClient = createHttpClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
return cachedHttpClient;
|
||||
}
|
||||
|
||||
private Client createHttpClient() {
|
||||
switch (clientType) {
|
||||
case APACHE:
|
||||
return createApacheHttpClient();
|
||||
case OKHTTP:
|
||||
return createOkHttpClient();
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported HTTP client type: " + clientType);
|
||||
}
|
||||
}
|
||||
|
||||
private Client createApacheHttpClient() {
|
||||
try {
|
||||
PoolingHttpClientConnectionManagerBuilder connManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setMaxConnTotal(maxConnTotal).setMaxConnPerRoute(maxConnPerRoute);
|
||||
|
||||
if (disableSslVerification) {
|
||||
SSLContext sslContext = createTrustAllSslContext();
|
||||
SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create().setSslContext(sslContext).build();
|
||||
connManagerBuilder.setSSLSocketFactory(sslSocketFactory);
|
||||
}
|
||||
|
||||
if (connectionTimeToLive > 0) {
|
||||
connManagerBuilder.setConnectionTimeToLive(TimeValue.of(connectionTimeToLive, connectionTimeToLiveUnit));
|
||||
}
|
||||
|
||||
PoolingHttpClientConnectionManager connectionManager = connManagerBuilder.build();
|
||||
|
||||
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager)
|
||||
.setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(Timeout.ofMilliseconds(connectTimeout))
|
||||
.setResponseTimeout(Timeout.ofMilliseconds(readTimeout)).build())
|
||||
.evictIdleConnections(TimeValue.of(idleConnectionEvictionTime, idleConnectionEvictionTimeUnit))
|
||||
.evictExpiredConnections().build();
|
||||
|
||||
return new ApacheHttp5Client(httpClient);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to create Apache HTTP client", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Client createOkHttpClient() {
|
||||
try {
|
||||
okhttp3.OkHttpClient.Builder builder = new okhttp3.OkHttpClient.Builder().connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
|
||||
.connectionPool(new okhttp3.ConnectionPool(maxConnTotal, connectionTimeToLive > 0 ? connectionTimeToLive : 5,
|
||||
connectionTimeToLive > 0 ? connectionTimeToLiveUnit : TimeUnit.MINUTES));
|
||||
|
||||
if (disableSslVerification) {
|
||||
SSLContext sslContext = createTrustAllSslContext();
|
||||
builder.sslSocketFactory(sslContext.getSocketFactory(), createTrustAllManager());
|
||||
builder.hostnameVerifier((hostname, session) -> true);
|
||||
}
|
||||
|
||||
return new feign.okhttp.OkHttpClient(builder.build());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to create OkHttp client", e);
|
||||
}
|
||||
}
|
||||
|
||||
private X509TrustManager createTrustAllManager() {
|
||||
return new X509TrustManager() {
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||
};
|
||||
}
|
||||
|
||||
private SSLContext createTrustAllSslContext() throws Exception {
|
||||
TrustManager[] trustAllCerts = new TrustManager[] { createTrustAllManager() };
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private String baseUrl;
|
||||
private String username;
|
||||
private String password;
|
||||
private String tenantId = "default";
|
||||
private int connectTimeout = 30000; // 30 seconds
|
||||
private int readTimeout = 60000; // 60 seconds
|
||||
private boolean debugEnabled = false;
|
||||
private long connectionTimeToLive = -1;
|
||||
private TimeUnit connectionTimeToLiveUnit = TimeUnit.MILLISECONDS;
|
||||
private boolean disableSslVerification = false;
|
||||
private int maxConnTotal = 200;
|
||||
private int maxConnPerRoute = 20;
|
||||
private long idleConnectionEvictionTime = 30;
|
||||
private TimeUnit idleConnectionEvictionTimeUnit = TimeUnit.SECONDS;
|
||||
private HttpClientType clientType = HttpClientType.APACHE;
|
||||
|
||||
public Builder baseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder credentials(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder tenantId(String tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder connectTimeout(int timeout, TimeUnit unit) {
|
||||
this.connectTimeout = (int) unit.toMillis(timeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder readTimeout(int timeout, TimeUnit unit) {
|
||||
this.readTimeout = (int) unit.toMillis(timeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder connectionTimeToLive(long ttl, TimeUnit unit) {
|
||||
this.connectionTimeToLive = ttl;
|
||||
this.connectionTimeToLiveUnit = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder debugEnabled(boolean debugEnabled) {
|
||||
this.debugEnabled = debugEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder disableSslVerification(boolean disableSslVerification) {
|
||||
this.disableSslVerification = disableSslVerification;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxConnTotal(int maxConnTotal) {
|
||||
this.maxConnTotal = maxConnTotal;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxConnPerRoute(int maxConnPerRoute) {
|
||||
this.maxConnPerRoute = maxConnPerRoute;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder idleConnectionEvictionTime(long time, TimeUnit unit) {
|
||||
this.idleConnectionEvictionTime = time;
|
||||
this.idleConnectionEvictionTimeUnit = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder httpClientType(HttpClientType clientType) {
|
||||
this.clientType = clientType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FineractFeignClientConfig build() {
|
||||
if (baseUrl == null || baseUrl.trim().isEmpty()) {
|
||||
throw new IllegalStateException("baseUrl is required");
|
||||
}
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
throw new IllegalStateException("username is required");
|
||||
}
|
||||
if (password == null) {
|
||||
throw new IllegalStateException("password is required");
|
||||
}
|
||||
return new FineractFeignClientConfig(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import feign.RequestTemplate;
|
||||
import feign.codec.EncodeException;
|
||||
import feign.codec.Encoder;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
|
||||
import org.apache.hc.core5.http.HttpEntity;
|
||||
|
||||
/**
|
||||
* Custom multipart encoder for Fineract Feign clients. Uses Apache HttpClient's MultipartEntityBuilder to properly
|
||||
* construct multipart/form-data requests.
|
||||
*/
|
||||
public class FineractMultipartEncoder implements Encoder {
|
||||
|
||||
private final Encoder delegate;
|
||||
|
||||
public FineractMultipartEncoder(Encoder delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
|
||||
if (object instanceof MultipartData) {
|
||||
encodeMultipart((MultipartData) object, template);
|
||||
} else if (object instanceof File) {
|
||||
encodeFileAsMultipart((File) object, template);
|
||||
} else if (object instanceof String && template.headers().containsKey("Content-Type")) {
|
||||
String contentType = template.headers().get("Content-Type").iterator().next();
|
||||
if (contentType.startsWith("text/html") || contentType.startsWith("text/plain")) {
|
||||
byte[] bodyBytes = ((String) object).getBytes(StandardCharsets.UTF_8);
|
||||
template.body(bodyBytes, StandardCharsets.UTF_8);
|
||||
} else {
|
||||
delegate.encode(object, bodyType, template);
|
||||
}
|
||||
} else {
|
||||
delegate.encode(object, bodyType, template);
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeFileAsMultipart(File file, RequestTemplate template) throws EncodeException {
|
||||
try {
|
||||
byte[] fileData = Files.readAllBytes(file.toPath());
|
||||
String contentType = detectContentType(file);
|
||||
MultipartData multipartData = new MultipartData().addFile("file", file.getName(), fileData, contentType);
|
||||
encodeMultipart(multipartData, template);
|
||||
} catch (IOException e) {
|
||||
throw new EncodeException("Failed to encode File as multipart: " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String detectContentType(File file) {
|
||||
try {
|
||||
String contentType = Files.probeContentType(file.toPath());
|
||||
return contentType != null ? contentType : "application/octet-stream";
|
||||
} catch (IOException e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeMultipart(MultipartData data, RequestTemplate template) throws EncodeException {
|
||||
try {
|
||||
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
|
||||
builder.setMode(org.apache.hc.client5.http.entity.mime.HttpMultipartMode.STRICT);
|
||||
|
||||
for (MultipartData.Part part : data.getParts()) {
|
||||
if (part.getFileData() != null) {
|
||||
org.apache.hc.core5.http.ContentType ct = org.apache.hc.core5.http.ContentType.create(part.getContentType());
|
||||
builder.addBinaryBody(part.getName(), part.getFileData(), ct, part.getFileName());
|
||||
} else if (part.getTextValue() != null) {
|
||||
builder.addTextBody(part.getName(), part.getTextValue(), org.apache.hc.core5.http.ContentType.TEXT_PLAIN);
|
||||
}
|
||||
}
|
||||
|
||||
HttpEntity entity = builder.build();
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
entity.writeTo(outputStream);
|
||||
byte[] body = outputStream.toByteArray();
|
||||
|
||||
template.body(body, null);
|
||||
template.removeHeader("Content-Type");
|
||||
String contentTypeValue = entity.getContentType();
|
||||
String cleanContentType = contentTypeValue;
|
||||
if (contentTypeValue.contains(";")) {
|
||||
int firstSemicolon = contentTypeValue.indexOf(';');
|
||||
String mainType = contentTypeValue.substring(0, firstSemicolon).trim();
|
||||
String paramsSection = contentTypeValue.substring(firstSemicolon + 1);
|
||||
|
||||
String boundary = null;
|
||||
int boundaryIndex = paramsSection.indexOf("boundary=");
|
||||
if (boundaryIndex != -1) {
|
||||
int boundaryStart = boundaryIndex;
|
||||
int boundaryEnd = paramsSection.indexOf(';', boundaryStart);
|
||||
if (boundaryEnd == -1) {
|
||||
boundary = paramsSection.substring(boundaryStart).trim();
|
||||
} else {
|
||||
boundary = paramsSection.substring(boundaryStart, boundaryEnd).trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (boundary != null) {
|
||||
cleanContentType = mainType + "; " + boundary;
|
||||
}
|
||||
}
|
||||
template.header("Content-Type", cleanContentType);
|
||||
} catch (IOException e) {
|
||||
throw new EncodeException("Failed to encode multipart request", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MultipartData {
|
||||
|
||||
private final java.util.List<Part> parts = new java.util.ArrayList<>();
|
||||
|
||||
public MultipartData addFile(String name, String fileName, byte[] data, String contentType) {
|
||||
parts.add(new Part(name, fileName, data, contentType));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MultipartData addText(String name, String value) {
|
||||
parts.add(new Part(name, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public java.util.List<Part> getParts() {
|
||||
return parts;
|
||||
}
|
||||
|
||||
public static class Part {
|
||||
|
||||
private final String name;
|
||||
private final String fileName;
|
||||
private final byte[] fileData;
|
||||
private final String contentType;
|
||||
private final String textValue;
|
||||
|
||||
public Part(String name, String fileName, byte[] fileData, String contentType) {
|
||||
this.name = name;
|
||||
this.fileName = fileName;
|
||||
this.fileData = fileData;
|
||||
this.contentType = contentType;
|
||||
this.textValue = null;
|
||||
}
|
||||
|
||||
public Part(String name, String textValue) {
|
||||
this.name = name;
|
||||
this.textValue = textValue;
|
||||
this.fileName = null;
|
||||
this.fileData = null;
|
||||
this.contentType = null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public byte[] getFileData() {
|
||||
return fileData;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public String getTextValue() {
|
||||
return textValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.apache.fineract.client.adapter.ExternalIdAdapter;
|
||||
|
||||
/**
|
||||
* Factory for creating and configuring Jackson ObjectMapper instances.
|
||||
*/
|
||||
public final class ObjectMapperFactory {
|
||||
|
||||
private static final ObjectMapper INSTANCE = createObjectMapper();
|
||||
|
||||
private ObjectMapperFactory() {
|
||||
// Private constructor to prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and configures a new ObjectMapper instance.
|
||||
*
|
||||
* @return A new configured ObjectMapper instance
|
||||
*/
|
||||
public static ObjectMapper createObjectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
// Configure the ObjectMapper
|
||||
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
|
||||
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
||||
|
||||
// Register Java 8 date/time support
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
|
||||
// Register ExternalId adapter
|
||||
mapper.registerModule(ExternalIdAdapter.createModule());
|
||||
|
||||
// Disable FAIL_ON_EMPTY_BEANS for empty responses
|
||||
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
|
||||
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shared, pre-configured ObjectMapper instance.
|
||||
*
|
||||
* @return A shared ObjectMapper instance
|
||||
*/
|
||||
public static ObjectMapper getShared() {
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
|
||||
public class TenantIdRequestInterceptor implements RequestInterceptor {
|
||||
|
||||
private final String tenantId;
|
||||
|
||||
public TenantIdRequestInterceptor(String tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
template.header("Fineract-Platform-TenantId", tenantId);
|
||||
if (!template.headers().containsKey("Content-Type")) {
|
||||
template.header("Content-Type", "application/json");
|
||||
}
|
||||
if (!template.headers().containsKey("Accept")) {
|
||||
template.header("Accept", "application/json");
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign.services;
|
||||
|
||||
import feign.Param;
|
||||
import feign.RequestLine;
|
||||
import feign.Response;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.DocumentData;
|
||||
import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse;
|
||||
|
||||
/**
|
||||
* This class was originally generated by OpenAPI Generator (https://openapi-generator.tech), but then had to be
|
||||
* manually edited to manually fix https://issues.apache.org/jira/browse/FINERACT-1227. If we could fix our OpenAPI /
|
||||
* Swagger YAML generation from the JAX RS and OpenAPI annotation to have the correct notation for binary document files
|
||||
* and images, then this could be removed again.
|
||||
*/
|
||||
public interface DocumentsApiFixed {
|
||||
|
||||
/**
|
||||
* Create a Document Note: A document is created using a Multi-part form upload Body Parts name : Name or summary of
|
||||
* the document description : Description of the document file : The file to be uploaded Mandatory Fields : file and
|
||||
* description
|
||||
*
|
||||
* @param entityType
|
||||
* entityType (required)
|
||||
* @param entityId
|
||||
* entityId (required)
|
||||
* @param file
|
||||
* (required)
|
||||
* @param name
|
||||
* name (optional)
|
||||
* @param description
|
||||
* description (optional)
|
||||
* @return PostEntityTypeEntityIdDocumentsResponse
|
||||
*/
|
||||
@RequestLine("POST /v1/{entityType}/{entityId}/documents")
|
||||
PostEntityTypeEntityIdDocumentsResponse createDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
|
||||
|
||||
/**
|
||||
* Remove a Document
|
||||
*
|
||||
* @param entityType
|
||||
* entityType (required)
|
||||
* @param entityId
|
||||
* entityId (required)
|
||||
* @param documentId
|
||||
* documentId (required)
|
||||
* @return DeleteEntityTypeEntityIdDocumentsResponse
|
||||
*/
|
||||
@RequestLine("DELETE /v1/{entityType}/{entityId}/documents/{documentId}")
|
||||
DeleteEntityTypeEntityIdDocumentsResponse deleteDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
@Param("documentId") Long documentId);
|
||||
|
||||
/**
|
||||
* Retrieve Binary File associated with Document Request used to download the file associated with the document
|
||||
* Example Requests: clients/1/documents/1/attachment loans/1/documents/1/attachment
|
||||
*
|
||||
* @param entityType
|
||||
* entityType (required)
|
||||
* @param entityId
|
||||
* entityId (required)
|
||||
* @param documentId
|
||||
* documentId (required)
|
||||
* @return Response
|
||||
*/
|
||||
@RequestLine("GET /v1/{entityType}/{entityId}/documents/{documentId}/attachment")
|
||||
@feign.Headers("Accept: */*")
|
||||
Response downloadFile(@Param("entityType") String entityType, @Param("entityId") Long entityId, @Param("documentId") Long documentId);
|
||||
|
||||
/**
|
||||
* Retrieve a Document Example Requests: clients/1/documents/1 loans/1/documents/1
|
||||
* client_identifiers/1/documents/1?fields=name,description
|
||||
*
|
||||
* @param entityType
|
||||
* entityType (required)
|
||||
* @param entityId
|
||||
* entityId (required)
|
||||
* @param documentId
|
||||
* documentId (required)
|
||||
* @return DocumentData
|
||||
*/
|
||||
@RequestLine("GET /v1/{entityType}/{entityId}/documents/{documentId}")
|
||||
DocumentData getDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
@Param("documentId") Long documentId);
|
||||
|
||||
/**
|
||||
* List documents Example Requests: clients/1/documents client_identifiers/1/documents
|
||||
* loans/1/documents?fields=name,description
|
||||
*
|
||||
* @param entityType
|
||||
* entityType (required)
|
||||
* @param entityId
|
||||
* entityId (required)
|
||||
* @return List<DocumentData>
|
||||
*/
|
||||
@RequestLine("GET /v1/{entityType}/{entityId}/documents")
|
||||
List<DocumentData> retrieveAllDocuments(@Param("entityType") String entityType, @Param("entityId") Long entityId);
|
||||
|
||||
/**
|
||||
* Update a Document Note: A document is updated using a Multi-part form upload Body Parts name Name or summary of
|
||||
* the document description Description of the document file The file to be uploaded
|
||||
*
|
||||
* @param entityType
|
||||
* entityType (required)
|
||||
* @param entityId
|
||||
* entityId (required)
|
||||
* @param documentId
|
||||
* documentId (required)
|
||||
* @param file
|
||||
* (optional)
|
||||
* @param name
|
||||
* name (optional)
|
||||
* @param description
|
||||
* description (optional)
|
||||
* @return PutEntityTypeEntityIdDocumentsResponse
|
||||
*/
|
||||
@RequestLine("PUT /v1/{entityType}/{entityId}/documents/{documentId}")
|
||||
PutEntityTypeEntityIdDocumentsResponse updateDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
|
||||
@Param("documentId") Long documentId, org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign.services;
|
||||
|
||||
import feign.Headers;
|
||||
import feign.Param;
|
||||
import feign.QueryMap;
|
||||
import feign.RequestLine;
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Client API (Feign) for /images.
|
||||
*
|
||||
* This class is entirely hand-written, inspired by DocumentsApiFixed, and from /images methods which currently end up
|
||||
* in DefaultApi (see <a href="https://issues.apache.org/jira/browse/FINERACT-1222">FINERACT-1222</a>), but fixed for
|
||||
* bugs in the code generation (see <a href="https://issues.apache.org/jira/browse/FINERACT-1227">FINERACT-1227</a>).
|
||||
*
|
||||
* Note: For image uploads, use {@link #prepareFileUpload(File)} to prepare file data as a Data URL.
|
||||
*/
|
||||
public interface ImagesApi {
|
||||
|
||||
@RequestLine("POST /v1/{entityType}/{entityId}/images")
|
||||
@Headers("Content-Type: text/html")
|
||||
Response create(@Param("entityType") String entityType, @Param("entityId") Long entityId, String dataUrl);
|
||||
|
||||
@RequestLine("GET /v1/{entityType}/{entityId}/images")
|
||||
Response get(@Param("entityType") String entityType, @Param("entityId") Long entityId, @QueryMap Map<String, Object> queryParams);
|
||||
|
||||
@RequestLine("PUT /v1/{entityType}/{entityId}/images")
|
||||
@Headers("Content-Type: text/html")
|
||||
Response update(@Param("entityType") String entityType, @Param("entityId") Long entityId, String dataUrl);
|
||||
|
||||
@RequestLine("DELETE /v1/{entityType}/{entityId}/images")
|
||||
Response delete(@Param("entityType") String entityType, @Param("entityId") Long entityId);
|
||||
|
||||
static String prepareFileUpload(File file) {
|
||||
try {
|
||||
byte[] fileData = java.nio.file.Files.readAllBytes(file.toPath());
|
||||
String contentType = detectMediaType(file.getName());
|
||||
String dataUrlPrefix = getDataUrlPrefix(contentType);
|
||||
String base64Data = java.util.Base64.getEncoder().encodeToString(fileData);
|
||||
return dataUrlPrefix + base64Data;
|
||||
} catch (java.io.IOException e) {
|
||||
throw new RuntimeException("Failed to prepare file for upload: " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getDataUrlPrefix(String contentType) {
|
||||
return "data:" + contentType + ";base64,";
|
||||
}
|
||||
|
||||
private static String detectMediaType(String fileName) {
|
||||
if (fileName == null) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
int dotPos = fileName.lastIndexOf('.');
|
||||
if (dotPos == -1) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
String ext = fileName.substring(dotPos + 1).toLowerCase();
|
||||
|
||||
switch (ext) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return "image/jpeg";
|
||||
case "png":
|
||||
return "image/png";
|
||||
case "gif":
|
||||
return "image/gif";
|
||||
case "tif":
|
||||
case "tiff":
|
||||
return "image/tiff";
|
||||
case "pdf":
|
||||
return "application/pdf";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign.services;
|
||||
|
||||
import feign.Param;
|
||||
import feign.QueryMap;
|
||||
import feign.RequestLine;
|
||||
import feign.Response;
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.client.models.RunReportsResponse;
|
||||
|
||||
public interface RunReportsApi {
|
||||
|
||||
/**
|
||||
* Running a Report This resource allows you to run and receive output from pre-defined Apache Fineract reports.
|
||||
* Reports can also be used to provide data for searching and workflow functionality. The default output is a JSON
|
||||
* formatted "Generic Resultset". The Generic Resultset contains Column Heading as well as Data information.
|
||||
* However, you can export to CSV format by simply adding "&exportCSV=true" to the end of your URL. If Pentaho
|
||||
* reports have been pre-defined, they can also be run through this resource. Pentaho reports can return HTML, PDF
|
||||
* or CSV formats. The Apache Fineract reference application uses a JQuery plugin called stretchy reporting which,
|
||||
* itself, uses this reports resource to provide a pretty flexible reporting User Interface (UI). Example Requests:
|
||||
* runreports/Client%20Listing?R_officeId=1 runreports/Client%20Listing?R_officeId=1&exportCSV=true
|
||||
* runreports/OfficeIdSelectOne?R_officeId=1&parameterType=true
|
||||
* runreports/OfficeIdSelectOne?R_officeId=1&parameterType=true&exportCSV=true
|
||||
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&R_loanOfficerId=-1&R_officeId=1&R_startDate=2013-04-16&output-type=HTML&R_officeId=1
|
||||
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&R_loanOfficerId=-1&R_officeId=1&R_startDate=2013-04-16&output-type=XLS&R_officeId=1
|
||||
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&R_loanOfficerId=-1&R_officeId=1&R_startDate=2013-04-16&output-type=CSV&R_officeId=1
|
||||
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&R_loanOfficerId=-1&R_officeId=1&R_startDate=2013-04-16&output-type=PDF&R_officeId=1
|
||||
*
|
||||
* @param reportName
|
||||
* reportName (required)
|
||||
* @param parameters
|
||||
* Dynamic query parameters for the report (required)
|
||||
* @return RunReportsResponse
|
||||
*/
|
||||
@RequestLine("GET /v1/runreports/{reportName}")
|
||||
RunReportsResponse runReportGetData(@Param("reportName") String reportName, @QueryMap Map<String, String> parameters);
|
||||
|
||||
/**
|
||||
* Run Report which returns a response such as a PDF, CSV, XLS or XLSX file.
|
||||
*
|
||||
* @param reportName
|
||||
* reportName (required)
|
||||
* @param parameters
|
||||
* Dynamic query parameters for the report (required)
|
||||
* @return Response containing the file content
|
||||
*/
|
||||
@RequestLine("GET /v1/runreports/{reportName}")
|
||||
Response runReportGetFile(@Param("reportName") String reportName, @QueryMap Map<String, String> parameters);
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Exception thrown by {@link FeignCalls} utility when Feign calls fail.
|
||||
*/
|
||||
@Slf4j
|
||||
@Getter
|
||||
public class CallFailedRuntimeException extends RuntimeException {
|
||||
|
||||
private final int status;
|
||||
private final String developerMessage;
|
||||
|
||||
public CallFailedRuntimeException(FeignException cause) {
|
||||
super(createMessage(cause), cause);
|
||||
this.status = cause.status();
|
||||
this.developerMessage = extractDeveloperMessage(cause);
|
||||
}
|
||||
|
||||
private static String createMessage(FeignException e) {
|
||||
StringBuilder sb = new StringBuilder("HTTP failed: status=").append(e.status());
|
||||
|
||||
if (e.request() != null) {
|
||||
sb.append(", request=").append(e.request().url());
|
||||
}
|
||||
|
||||
String contentString = e.contentUTF8();
|
||||
if (contentString != null && !contentString.isEmpty()) {
|
||||
sb.append(", errorBody=").append(contentString);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String extractDeveloperMessage(FeignException e) {
|
||||
try {
|
||||
byte[] content = e.content();
|
||||
if (content == null || content.length == 0) {
|
||||
return e.getMessage();
|
||||
}
|
||||
|
||||
String contentString = new String(content, StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode root = mapper.readTree(contentString);
|
||||
|
||||
if (root.has("developerMessage")) {
|
||||
return root.get("developerMessage").asText();
|
||||
}
|
||||
|
||||
if (root.has("errors")) {
|
||||
JsonNode errors = root.get("errors");
|
||||
if (errors.isArray() && errors.size() > 0) {
|
||||
JsonNode firstError = errors.get(0);
|
||||
if (firstError.has("developerMessage")) {
|
||||
return firstError.get("developerMessage").asText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return contentString;
|
||||
} catch (IOException ex) {
|
||||
log.warn("Failed to extract developer message from error response", ex);
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign.util;
|
||||
|
||||
import feign.FeignException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Extension methods for Feign calls. This class is recommended to be statically imported.
|
||||
*/
|
||||
public final class FeignCalls {
|
||||
|
||||
private FeignCalls() {}
|
||||
|
||||
/**
|
||||
* Execute a Feign call, expecting success, returning strongly typed body. This covers the most typical use and is
|
||||
* thus most used.
|
||||
*
|
||||
* @param feignCall
|
||||
* the Feign call to execute
|
||||
* @return the result of the successful call (never null)
|
||||
* @throws CallFailedRuntimeException
|
||||
* thrown if a problem occurred talking to the server, or the HTTP response code was not [200..300)
|
||||
* successful
|
||||
*/
|
||||
public static <T> T ok(Supplier<T> feignCall) throws CallFailedRuntimeException {
|
||||
try {
|
||||
return feignCall.get();
|
||||
} catch (FeignException e) {
|
||||
throw new CallFailedRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Feign call that returns void. This is useful for operations that don't return a response body.
|
||||
*
|
||||
* @param feignCall
|
||||
* the Feign call to execute
|
||||
* @throws CallFailedRuntimeException
|
||||
* thrown if a problem occurred talking to the server, or the HTTP response code was not [200..300)
|
||||
* successful
|
||||
*/
|
||||
public static void executeVoid(Runnable feignCall) throws CallFailedRuntimeException {
|
||||
try {
|
||||
feignCall.run();
|
||||
} catch (FeignException e) {
|
||||
throw new CallFailedRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Feign call, returning the result without throwing CallFailedRuntimeException. This is useful when you
|
||||
* want to handle FeignException yourself.
|
||||
*
|
||||
* @param feignCall
|
||||
* the Feign call to execute
|
||||
* @return the result of the call
|
||||
* @throws FeignException
|
||||
* thrown if a problem occurred talking to the server
|
||||
*/
|
||||
public static <T> T execute(Supplier<T> feignCall) throws FeignException {
|
||||
return feignCall.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.util;
|
||||
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.client.feign.services.DocumentsApiFixed;
|
||||
import org.apache.fineract.client.feign.services.ImagesApi;
|
||||
|
||||
/**
|
||||
* Convenience utilities for file handling in Feign API calls.
|
||||
*
|
||||
* Provides helper methods for: - Media type detection from file extensions - File name extraction from HTTP response
|
||||
* headers - Content type probing
|
||||
*
|
||||
* Used in conjunction with {@link DocumentsApiFixed} and {@link ImagesApi} which use Data URL format for file uploads
|
||||
* (base64-encoded strings with data URI scheme).
|
||||
*/
|
||||
public final class FeignParts {
|
||||
|
||||
private FeignParts() {}
|
||||
|
||||
/**
|
||||
* Determine the media type based on file extension.
|
||||
*
|
||||
* @param fileName
|
||||
* the name of the file
|
||||
* @return the media type string, or null if not recognized
|
||||
*/
|
||||
public static String mediaType(String fileName) {
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
int dotPos = fileName.lastIndexOf('.');
|
||||
if (dotPos == -1) {
|
||||
return null;
|
||||
}
|
||||
String ext = fileName.substring(dotPos + 1).toLowerCase();
|
||||
|
||||
switch (ext) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return "image/jpeg";
|
||||
case "png":
|
||||
return "image/png";
|
||||
case "tif":
|
||||
case "tiff":
|
||||
return "image/tiff";
|
||||
case "gif":
|
||||
return "image/gif";
|
||||
case "pdf":
|
||||
return "application/pdf";
|
||||
case "docx":
|
||||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
case "doc":
|
||||
return "application/msword";
|
||||
case "xlsx":
|
||||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
case "xls":
|
||||
return "application/vnd.ms-excel";
|
||||
case "odt":
|
||||
return "application/vnd.oasis.opendocument.text";
|
||||
case "ods":
|
||||
return "application/vnd.oasis.opendocument.spreadsheet";
|
||||
case "txt":
|
||||
return "text/plain";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the file name from the Content-Disposition header of a response.
|
||||
*
|
||||
* @param response
|
||||
* the HTTP response
|
||||
* @return Optional containing the file name if present
|
||||
*/
|
||||
public static Optional<String> fileName(Response response) {
|
||||
if (response.headers() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
java.util.Collection<String> contentDispositionHeaders = response.headers().get("Content-Disposition");
|
||||
if (contentDispositionHeaders == null || contentDispositionHeaders.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String contentDisposition = contentDispositionHeaders.iterator().next();
|
||||
if (contentDisposition == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
int i = contentDisposition.indexOf("; filename=\"");
|
||||
if (i == -1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(contentDisposition.substring(i + "; filename=\"".length(), contentDisposition.length() - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe the content type of a file using Files.probeContentType.
|
||||
*
|
||||
* @param file
|
||||
* the file to probe
|
||||
* @return the content type, or application/octet-stream as fallback
|
||||
* @throws IOException
|
||||
* if an I/O error occurs
|
||||
*/
|
||||
public static String probeContentType(File file) throws IOException {
|
||||
String contentType = Files.probeContentType(file.toPath());
|
||||
if (contentType == null) {
|
||||
contentType = mediaType(file.getName());
|
||||
}
|
||||
return contentType != null ? contentType : "application/octet-stream";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package {{package}};
|
||||
|
||||
import feign.*;
|
||||
import feign.Param;
|
||||
import feign.RequestLine;
|
||||
|
||||
{{#imports}}import {{import}};
|
||||
{{/imports}}
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface {{classname}} {
|
||||
|
||||
{{#operations}}
|
||||
{{#operation}}
|
||||
/**
|
||||
* {{summary}}
|
||||
* {{notes}}
|
||||
{{#allParams}}
|
||||
* @param {{paramName}} {{description}}
|
||||
{{/allParams}}
|
||||
* @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}
|
||||
*/
|
||||
@RequestLine("{{httpMethod}} {{path}}")
|
||||
{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} {{operationId}}(
|
||||
{{#pathParams}}
|
||||
@Param("{{baseName}}") {{{dataType}}} {{paramName}}{{^-last}},{{/-last}}
|
||||
{{/pathParams}}
|
||||
{{#bodyParam}}
|
||||
{{#hasPathParams}},{{/hasPathParams}}{{{dataType}}} {{paramName}}
|
||||
{{/bodyParam}}
|
||||
{{#hasQueryParams}}
|
||||
{{#hasPathParams}}{{^bodyParam}},{{/bodyParam}}{{/hasPathParams}}{{#bodyParam}},{{/bodyParam}}
|
||||
@QueryMap Map<String, Object> queryParams
|
||||
{{/hasQueryParams}}
|
||||
{{#hasHeaderParams}}
|
||||
{{#hasPathParams}}{{^bodyParam}}{{^hasQueryParams}},{{/hasQueryParams}}{{/bodyParam}}{{/hasPathParams}}
|
||||
{{#bodyParam}}{{^hasQueryParams}},{{/hasQueryParams}}{{/bodyParam}}
|
||||
{{#hasQueryParams}},{{/hasQueryParams}}
|
||||
@HeaderMap Map<String, Object> headerParams
|
||||
{{/hasHeaderParams}}
|
||||
);
|
||||
|
||||
{{/operation}}
|
||||
{{/operations}}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.adapter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.fineract.client.feign.ObjectMapperFactory;
|
||||
import org.apache.fineract.client.models.ExternalId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ExternalIdAdapterTest {
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mapper = ObjectMapperFactory.getShared();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalId() throws Exception {
|
||||
ExternalId externalId = new ExternalId().value("EXT-12345");
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("\"EXT-12345\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalIdWithSpecialCharacters() throws Exception {
|
||||
ExternalId externalId = new ExternalId().value("EXT-ID-2024/01/15");
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("\"EXT-ID-2024/01/15\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeNullExternalId() throws Exception {
|
||||
ExternalId externalId = new ExternalId().value(null);
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalIdObjectNull() throws Exception {
|
||||
ExternalId externalId = null;
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeExternalId() throws Exception {
|
||||
String json = "\"EXT-67890\"";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNotNull();
|
||||
assertThat(externalId.getValue()).isEqualTo("EXT-67890");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeExternalIdWithSpecialCharacters() throws Exception {
|
||||
String json = "\"CLIENT-2024-001/ABC\"";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNotNull();
|
||||
assertThat(externalId.getValue()).isEqualTo("CLIENT-2024-001/ABC");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeNullExternalId() throws Exception {
|
||||
String json = "null";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundTripSerialization() throws Exception {
|
||||
ExternalId original = new ExternalId().value("TEST-ID-001");
|
||||
|
||||
String json = mapper.writeValueAsString(original);
|
||||
ExternalId deserialized = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(deserialized.getValue()).isEqualTo(original.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundTripSerializationWithUUID() throws Exception {
|
||||
ExternalId original = new ExternalId().value("550e8400-e29b-41d4-a716-446655440000");
|
||||
|
||||
String json = mapper.writeValueAsString(original);
|
||||
ExternalId deserialized = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(deserialized.getValue()).isEqualTo(original.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundTripSerializationWithNumericString() throws Exception {
|
||||
ExternalId original = new ExternalId().value("123456789");
|
||||
|
||||
String json = mapper.writeValueAsString(original);
|
||||
ExternalId deserialized = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(deserialized.getValue()).isEqualTo(original.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeEmptyString() throws Exception {
|
||||
String json = "\"\"";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNotNull();
|
||||
assertThat(externalId.getValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalIdWithEmptyValue() throws Exception {
|
||||
ExternalId externalId = new ExternalId().value("");
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("\"\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testModuleCreation() {
|
||||
assertThat(ExternalIdAdapter.createModule()).isNotNull();
|
||||
assertThat(ExternalIdAdapter.createModule().getModuleName()).isEqualTo("ExternalIdModule");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeExternalIdWithWhitespace() throws Exception {
|
||||
String json = "\" EXT-001 \"";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNotNull();
|
||||
assertThat(externalId.getValue()).isEqualTo(" EXT-001 ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalIdInObject() throws Exception {
|
||||
TestObject obj = new TestObject();
|
||||
obj.externalId = new ExternalId().value("TEST-123");
|
||||
|
||||
String json = mapper.writeValueAsString(obj);
|
||||
|
||||
assertThat(json).contains("\"externalId\":\"TEST-123\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeExternalIdInObject() throws Exception {
|
||||
String json = "{\"externalId\":\"TEST-456\"}";
|
||||
|
||||
TestObject obj = mapper.readValue(json, TestObject.class);
|
||||
|
||||
assertThat(obj.externalId).isNotNull();
|
||||
assertThat(obj.externalId.getValue()).isEqualTo("TEST-456");
|
||||
}
|
||||
|
||||
static class TestObject {
|
||||
|
||||
public ExternalId externalId;
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import feign.Request;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FeignExceptionTest {
|
||||
|
||||
private Request request;
|
||||
private byte[] responseBody;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
request = Request.create(Request.HttpMethod.GET, "/api/test", Collections.emptyMap(), null, StandardCharsets.UTF_8, null);
|
||||
responseBody = "Error response body".getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithErrorDetails() {
|
||||
String developerMessage = "Technical error details";
|
||||
String userMessage = "User-friendly error message";
|
||||
|
||||
FeignException exception = new FeignException(400, "Error message", request, responseBody, developerMessage, userMessage);
|
||||
|
||||
assertNotNull(exception);
|
||||
assertEquals(400, exception.status());
|
||||
assertEquals(developerMessage, exception.getDeveloperMessage());
|
||||
assertEquals(userMessage, exception.getUserMessage());
|
||||
assertEquals(request, exception.request());
|
||||
assertEquals(responseBody, exception.responseBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessage() {
|
||||
String developerMessage = "Developer error";
|
||||
String userMessage = "User error";
|
||||
|
||||
FeignException exception = new FeignException(500, "Base message", request, responseBody, developerMessage, userMessage);
|
||||
|
||||
String message = exception.getMessage();
|
||||
assertNotNull(message);
|
||||
assertTrue(message.contains("500"));
|
||||
assertTrue(message.contains(userMessage));
|
||||
assertTrue(message.contains(developerMessage));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDeveloperMessage() {
|
||||
String developerMessage = "This is a technical error";
|
||||
|
||||
FeignException exception = new FeignException(400, "Error", request, responseBody, developerMessage, null);
|
||||
|
||||
assertEquals(developerMessage, exception.getDeveloperMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserMessage() {
|
||||
String userMessage = "This is a user-friendly error";
|
||||
|
||||
FeignException exception = new FeignException(400, "Error", request, responseBody, null, userMessage);
|
||||
|
||||
assertEquals(userMessage, exception.getUserMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullErrorMessages() {
|
||||
FeignException exception = new FeignException(404, "Not found", request, responseBody, null, null);
|
||||
|
||||
assertNull(exception.getDeveloperMessage());
|
||||
assertNull(exception.getUserMessage());
|
||||
String message = exception.getMessage();
|
||||
assertNotNull(message);
|
||||
assertTrue(message.contains("404"));
|
||||
assertTrue(message.contains("Not found"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessageWithOnlyUserMessage() {
|
||||
String userMessage = "User error only";
|
||||
|
||||
FeignException exception = new FeignException(400, "Base message", request, responseBody, null, userMessage);
|
||||
|
||||
String message = exception.getMessage();
|
||||
assertTrue(message.contains("400"));
|
||||
assertTrue(message.contains(userMessage));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessageWithOnlyDeveloperMessage() {
|
||||
String developerMessage = "Developer error only";
|
||||
|
||||
FeignException exception = new FeignException(400, "Base message", request, responseBody, developerMessage, null);
|
||||
|
||||
String message = exception.getMessage();
|
||||
assertTrue(message.contains("400"));
|
||||
assertTrue(message.contains(developerMessage));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResponseBodyAsString() {
|
||||
FeignException exception = new FeignException(400, "Error", request, responseBody, null, null);
|
||||
|
||||
String bodyString = exception.responseBodyAsString();
|
||||
assertNotNull(bodyString);
|
||||
assertEquals("Error response body", bodyString);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResponseBodyAsStringWithNullBody() {
|
||||
FeignException exception = new FeignException(400, "Error", request, null, null, null);
|
||||
|
||||
String bodyString = exception.responseBodyAsString();
|
||||
assertNull(bodyString);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FineractErrorDecoderTest {
|
||||
|
||||
private FineractErrorDecoder decoder;
|
||||
private Request request;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
decoder = new FineractErrorDecoder();
|
||||
request = Request.create(Request.HttpMethod.GET, "/api/test", Collections.emptyMap(), null, StandardCharsets.UTF_8, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeValidFineractError() {
|
||||
String jsonError = "{\"developerMessage\":\"Developer error message\",\"userMessage\":\"User error message\"}";
|
||||
Response response = createResponse(400, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals(400, feignException.status());
|
||||
assertEquals("User error message", feignException.getUserMessage());
|
||||
assertEquals("Developer error message", feignException.getDeveloperMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeInvalidJson() {
|
||||
String invalidJson = "This is not valid JSON";
|
||||
Response response = createResponse(400, invalidJson);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertNotNull(exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeNullBody() {
|
||||
Response response = createResponse(404, null);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertNotNull(exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecode400Error() {
|
||||
String jsonError = "{\"developerMessage\":\"Bad request details\",\"userMessage\":\"Invalid input\"}";
|
||||
Response response = createResponse(400, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals(400, feignException.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecode404Error() {
|
||||
String jsonError = "{\"developerMessage\":\"Resource not found details\",\"userMessage\":\"Not found\"}";
|
||||
Response response = createResponse(404, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals(404, feignException.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecode500Error() {
|
||||
String jsonError = "{\"developerMessage\":\"Internal server error details\",\"userMessage\":\"Server error\"}";
|
||||
Response response = createResponse(500, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals(500, feignException.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractDeveloperMessage() {
|
||||
String jsonError = "{\"developerMessage\":\"Technical details here\"}";
|
||||
Response response = createResponse(400, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals("Technical details here", feignException.getDeveloperMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractUserMessage() {
|
||||
String jsonError = "{\"userMessage\":\"User-friendly message\"}";
|
||||
Response response = createResponse(400, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals("User-friendly message", feignException.getUserMessage());
|
||||
}
|
||||
|
||||
private Response createResponse(int status, String body) {
|
||||
Map<String, Collection<String>> headers = new HashMap<>();
|
||||
|
||||
Response.Body responseBody = null;
|
||||
if (body != null) {
|
||||
byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
responseBody = new Response.Body() {
|
||||
|
||||
@Override
|
||||
public Integer length() {
|
||||
return bodyBytes.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRepeatable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteArrayInputStream asInputStream() {
|
||||
return new ByteArrayInputStream(bodyBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.Reader asReader() {
|
||||
return new java.io.InputStreamReader(asInputStream(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.Reader asReader(java.nio.charset.Charset charset) {
|
||||
return new java.io.InputStreamReader(asInputStream(), charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
};
|
||||
}
|
||||
|
||||
return Response.builder().status(status).reason("Test").request(request).headers(headers).body(responseBody).build();
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import feign.RequestLine;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FineractFeignClientConfigTest {
|
||||
|
||||
@Test
|
||||
void testBuilderDefaults() {
|
||||
FineractFeignClientConfig.Builder builder = FineractFeignClientConfig.builder();
|
||||
FineractFeignClientConfig config = builder.baseUrl("http://localhost:8080").credentials("admin", "password").build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuilderConfiguration() {
|
||||
String baseUrl = "http://example.com:8080";
|
||||
String username = "testuser";
|
||||
String password = "testpass";
|
||||
int connectTimeoutSeconds = 10;
|
||||
int readTimeoutSeconds = 30;
|
||||
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials(username, password)
|
||||
.connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS).readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
|
||||
.debugEnabled(true).build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionTimeToLiveConfiguration() {
|
||||
long ttl = 5;
|
||||
TimeUnit ttlUnit = TimeUnit.MINUTES;
|
||||
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
|
||||
.credentials("admin", "password").connectionTimeToLive(ttl, ttlUnit).build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncoderDecoderConfiguration() {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
|
||||
.credentials("admin", "password").build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorDecoderConfiguration() {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
|
||||
.credentials("admin", "password").build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClientCreation() {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
|
||||
.credentials("admin", "password").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
interface TestApi {
|
||||
|
||||
@RequestLine("GET /test")
|
||||
String test();
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ObjectMapperFactoryTest {
|
||||
|
||||
@Test
|
||||
void testSharedObjectMapperNotNull() {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
assertNotNull(mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSharedObjectMapperSingleton() {
|
||||
ObjectMapper mapper1 = ObjectMapperFactory.getShared();
|
||||
ObjectMapper mapper2 = ObjectMapperFactory.getShared();
|
||||
assertSame(mapper1, mapper2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJava8DateTimeSerialization() throws JsonProcessingException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
LocalDate date = LocalDate.of(2024, 1, 15);
|
||||
|
||||
String json = mapper.writeValueAsString(new TestObject(date));
|
||||
assertNotNull(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJava8DateTimeDeserialization() throws IOException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
String json = "{\"date\":\"2024-01-15\"}";
|
||||
|
||||
TestObject obj = mapper.readValue(json, TestObject.class);
|
||||
assertNotNull(obj);
|
||||
assertNotNull(obj.getDate());
|
||||
assertEquals(LocalDate.of(2024, 1, 15), obj.getDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullHandling() throws JsonProcessingException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
TestObject obj = new TestObject(null);
|
||||
|
||||
String json = mapper.writeValueAsString(obj);
|
||||
assertNotNull(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeNullValue() throws IOException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
String json = "{\"date\":null}";
|
||||
|
||||
TestObject obj = mapper.readValue(json, TestObject.class);
|
||||
assertNotNull(obj);
|
||||
assertNull(obj.getDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnknownPropertiesIgnored() throws IOException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
String json = "{\"date\":\"2024-01-15\",\"unknownField\":\"value\"}";
|
||||
|
||||
TestObject obj = mapper.readValue(json, TestObject.class);
|
||||
assertNotNull(obj);
|
||||
assertEquals(LocalDate.of(2024, 1, 15), obj.getDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateObjectMapperNotNull() {
|
||||
ObjectMapper mapper = ObjectMapperFactory.createObjectMapper();
|
||||
assertNotNull(mapper);
|
||||
}
|
||||
|
||||
static class TestObject {
|
||||
|
||||
private LocalDate date;
|
||||
|
||||
TestObject() {}
|
||||
|
||||
TestObject(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
}
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign.integration;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.RequestLine;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Tag("integration")
|
||||
class EncoderDecoderIntegrationTest {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private String baseUrl;
|
||||
private FineractFeignClientConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
baseUrl = "http://localhost:" + wireMockServer.port();
|
||||
|
||||
config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexObjectSerialization() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(aResponse().withStatus(201)
|
||||
.withHeader("Content-Type", "application/json").withBody("{\"id\":100,\"status\":\"created\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("complex-test");
|
||||
request.setAmount(new BigDecimal("1234.56"));
|
||||
request.setCreatedDate(LocalDate.of(2024, 1, 15));
|
||||
request.setItems(Arrays.asList("item1", "item2", "item3"));
|
||||
|
||||
Map<String, String> metadata = new HashMap<>();
|
||||
metadata.put("key1", "value1");
|
||||
metadata.put("key2", "value2");
|
||||
request.setMetadata(metadata);
|
||||
|
||||
NestedObject nested = new NestedObject();
|
||||
nested.setNestedName("nested-value");
|
||||
nested.setNestedDate(LocalDate.of(2024, 2, 20));
|
||||
request.setNested(nested);
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(100L);
|
||||
assertThat(response.getStatus()).isEqualTo("created");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullValueHandling() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":null}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("null-test");
|
||||
request.setAmount(null);
|
||||
request.setCreatedDate(null);
|
||||
request.setItems(null);
|
||||
request.setMetadata(null);
|
||||
request.setNested(null);
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(1L);
|
||||
assertThat(response.getStatus()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDateFormats() {
|
||||
String responseJson = "{\"id\":1,\"date\":\"2024-03-15\",\"dateTime\":\"2024-03-15T14:30:00\",\"timestamp\":\"2024-03-15T14:30:00.123\"}";
|
||||
|
||||
wireMockServer.stubFor(post(urlEqualTo("/date-test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseJson)));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("date-test");
|
||||
request.setCreatedDate(LocalDate.of(2024, 3, 15));
|
||||
|
||||
DateResponse response = client.testDates(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getDate()).isEqualTo(LocalDate.of(2024, 3, 15));
|
||||
assertThat(response.getDateTime()).isEqualTo(LocalDateTime.of(2024, 3, 15, 14, 30, 0));
|
||||
assertThat(response.getTimestamp()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLargePayload() {
|
||||
StringBuilder largeString = new StringBuilder();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
largeString.append("This is line ").append(i).append(". ");
|
||||
}
|
||||
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(aResponse().withStatus(200)
|
||||
.withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"processed\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName(largeString.toString());
|
||||
request.setAmount(new BigDecimal("999999.99"));
|
||||
request.setCreatedDate(LocalDate.now(ZoneId.systemDefault()));
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo("processed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyCollections() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"ok\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("empty-test");
|
||||
request.setItems(Arrays.asList());
|
||||
request.setMetadata(new HashMap<>());
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBigDecimalPrecision() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"precise\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("precision-test");
|
||||
request.setAmount(new BigDecimal("123456789.123456789"));
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo("precise");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNestedObjectDeserialization() {
|
||||
String responseJson = "{\"id\":1,\"nested\":{\"nestedName\":\"deep-value\",\"nestedDate\":\"2024-04-01\"},\"items\":[\"a\",\"b\",\"c\"]}";
|
||||
|
||||
wireMockServer.stubFor(post(urlEqualTo("/nested"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseJson)));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("nested-test");
|
||||
|
||||
NestedResponse response = client.testNested(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(1L);
|
||||
assertThat(response.getNested()).isNotNull();
|
||||
assertThat(response.getNested().getNestedName()).isEqualTo("deep-value");
|
||||
assertThat(response.getNested().getNestedDate()).isEqualTo(LocalDate.of(2024, 4, 1));
|
||||
assertThat(response.getItems()).containsExactly("a", "b", "c");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSpecialCharacterHandling() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"special\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("Test with special chars: <>\"'&\n\t");
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo("special");
|
||||
}
|
||||
|
||||
interface ComplexApi {
|
||||
|
||||
@RequestLine("POST /complex")
|
||||
SimpleResponse createComplex(ComplexRequest request);
|
||||
|
||||
@RequestLine("POST /date-test")
|
||||
DateResponse testDates(ComplexRequest request);
|
||||
|
||||
@RequestLine("POST /nested")
|
||||
NestedResponse testNested(ComplexRequest request);
|
||||
}
|
||||
|
||||
static class ComplexRequest {
|
||||
|
||||
private String name;
|
||||
private BigDecimal amount;
|
||||
private LocalDate createdDate;
|
||||
private List<String> items;
|
||||
private Map<String, String> metadata;
|
||||
private NestedObject nested;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(BigDecimal amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public LocalDate getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
public void setCreatedDate(LocalDate createdDate) {
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public List<String> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<String> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public void setMetadata(Map<String, String> metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public NestedObject getNested() {
|
||||
return nested;
|
||||
}
|
||||
|
||||
public void setNested(NestedObject nested) {
|
||||
this.nested = nested;
|
||||
}
|
||||
}
|
||||
|
||||
static class NestedObject {
|
||||
|
||||
private String nestedName;
|
||||
private LocalDate nestedDate;
|
||||
|
||||
public String getNestedName() {
|
||||
return nestedName;
|
||||
}
|
||||
|
||||
public void setNestedName(String nestedName) {
|
||||
this.nestedName = nestedName;
|
||||
}
|
||||
|
||||
public LocalDate getNestedDate() {
|
||||
return nestedDate;
|
||||
}
|
||||
|
||||
public void setNestedDate(LocalDate nestedDate) {
|
||||
this.nestedDate = nestedDate;
|
||||
}
|
||||
}
|
||||
|
||||
static class SimpleResponse {
|
||||
|
||||
private Long id;
|
||||
private String status;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
static class DateResponse {
|
||||
|
||||
private Long id;
|
||||
private LocalDate date;
|
||||
private LocalDateTime dateTime;
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public LocalDateTime getDateTime() {
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
public void setDateTime(LocalDateTime dateTime) {
|
||||
this.dateTime = dateTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(LocalDateTime timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
static class NestedResponse {
|
||||
|
||||
private Long id;
|
||||
private NestedObject nested;
|
||||
private List<String> items;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public NestedObject getNested() {
|
||||
return nested;
|
||||
}
|
||||
|
||||
public void setNested(NestedObject nested) {
|
||||
this.nested = nested;
|
||||
}
|
||||
|
||||
public List<String> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<String> items) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign.integration;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.RequestLine;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FeignException;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Tag("integration")
|
||||
class FineractFeignClientIntegrationTest {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private String baseUrl;
|
||||
private FineractFeignClientConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
baseUrl = "http://localhost:" + wireMockServer.port();
|
||||
|
||||
config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJsonEncoding() {
|
||||
wireMockServer
|
||||
.stubFor(post(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":1,\"message\":\"success\",\"timestamp\":\"2024-01-15T10:30:00\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestRequest request = new TestRequest();
|
||||
request.setName("test-name");
|
||||
request.setDate(LocalDate.of(2024, 1, 15));
|
||||
|
||||
TestResponse response = client.createTest(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(1L);
|
||||
assertThat(response.getMessage()).isEqualTo("success");
|
||||
assertThat(response.getTimestamp()).isEqualTo(LocalDateTime.of(2024, 1, 15, 10, 30, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJsonDecoding() {
|
||||
wireMockServer
|
||||
.stubFor(get(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":42,\"message\":\"decoded-message\",\"timestamp\":\"2024-12-25T18:45:30\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestResponse response = client.getTest();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(42L);
|
||||
assertThat(response.getMessage()).isEqualTo("decoded-message");
|
||||
assertThat(response.getTimestamp()).isEqualTo(LocalDateTime.of(2024, 12, 25, 18, 45, 30));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJava8DateTimeSerialization() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/test")).withRequestBody(containing("2024-06-30"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":99,\"message\":\"date-test\",\"timestamp\":\"2024-06-30T12:00:00\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestRequest request = new TestRequest();
|
||||
request.setName("date-test");
|
||||
request.setDate(LocalDate.of(2024, 6, 30));
|
||||
|
||||
TestResponse response = client.createTest(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(99L);
|
||||
assertThat(response.getTimestamp()).isNotNull();
|
||||
assertThat(response.getTimestamp().toLocalDate()).isEqualTo(LocalDate.of(2024, 6, 30));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorDecoder() {
|
||||
wireMockServer.stubFor(
|
||||
get(urlEqualTo("/error")).willReturn(aResponse().withStatus(404).withHeader("Content-Type", "application/json").withBody(
|
||||
"{\"developerMessage\":\"Resource not found\",\"httpStatusCode\":\"404\",\"defaultUserMessage\":\"Not Found\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
|
||||
assertThatThrownBy(client::getError).isInstanceOf(FeignException.class).hasMessageContaining("Resource not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorDecoderWithServerError() {
|
||||
wireMockServer.stubFor(
|
||||
get(urlEqualTo("/error")).willReturn(aResponse().withStatus(500).withHeader("Content-Type", "application/json").withBody(
|
||||
"{\"developerMessage\":\"Internal server error occurred\",\"httpStatusCode\":\"500\",\"defaultUserMessage\":\"Server Error\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
|
||||
assertThatThrownBy(client::getError).isInstanceOf(FeignException.class).hasMessageContaining("Internal server error occurred");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionTimeToLiveConfiguration() {
|
||||
FineractFeignClientConfig ttlConfig = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
|
||||
.connectionTimeToLive(5, TimeUnit.MINUTES).build();
|
||||
|
||||
wireMockServer
|
||||
.stubFor(get(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":1,\"message\":\"ttl-test\",\"timestamp\":\"2024-01-01T00:00:00\"}")));
|
||||
|
||||
TestApi client = ttlConfig.createClient(TestApi.class);
|
||||
TestResponse response = client.getTest();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getMessage()).isEqualTo("ttl-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBasicAuthentication() {
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test")).withHeader("Authorization", equalTo("Basic dGVzdHVzZXI6dGVzdHBhc3M="))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":1,\"message\":\"authenticated\",\"timestamp\":\"2024-01-01T00:00:00\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestResponse response = client.getTest();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getMessage()).isEqualTo("authenticated");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullValueHandling() {
|
||||
wireMockServer
|
||||
.stubFor(post(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":1,\"message\":null,\"timestamp\":\"2024-01-01T00:00:00\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestRequest request = new TestRequest();
|
||||
request.setName("null-test");
|
||||
request.setDate(null);
|
||||
|
||||
TestResponse response = client.createTest(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getMessage()).isNull();
|
||||
}
|
||||
|
||||
interface TestApi {
|
||||
|
||||
@RequestLine("GET /test")
|
||||
TestResponse getTest();
|
||||
|
||||
@RequestLine("POST /test")
|
||||
TestResponse createTest(TestRequest request);
|
||||
|
||||
@RequestLine("GET /error")
|
||||
TestResponse getError();
|
||||
}
|
||||
|
||||
static class TestRequest {
|
||||
|
||||
private String name;
|
||||
private LocalDate date;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
|
||||
static class TestResponse {
|
||||
|
||||
private Long id;
|
||||
private String message;
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public LocalDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(LocalDateTime timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.feign.performance;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.RequestLine;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Tag("performance")
|
||||
class ConnectionPoolPerformanceTest {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionPoolTTL() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectionTimeToLive(2, TimeUnit.SECONDS).connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
String response1 = api.getTest();
|
||||
assertThat(response1).isEqualTo("OK");
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
String response2 = api.getTest();
|
||||
assertThat(response2).isEqualTo("OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionPoolTTLWithMultipleRequests() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectionTimeToLive(1, TimeUnit.SECONDS).connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
String response = api.getTest();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
Thread.sleep(1500);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentRequests() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"").withFixedDelay(100)));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
int numRequests = 50;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(10);
|
||||
List<Future<String>> futures = new ArrayList<>();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (int i = 0; i < numRequests; i++) {
|
||||
futures.add(executor.submit(api::getTest));
|
||||
}
|
||||
|
||||
for (Future<String> future : futures) {
|
||||
assertThat(future.get()).isEqualTo("OK");
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(duration).isLessThan(2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentRequestsWithHighLoad() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"").withFixedDelay(50)));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
int numRequests = 100;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(20);
|
||||
List<Future<String>> futures = new ArrayList<>();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (int i = 0; i < numRequests; i++) {
|
||||
futures.add(executor.submit(api::getTest));
|
||||
}
|
||||
|
||||
int successCount = 0;
|
||||
for (Future<String> future : futures) {
|
||||
if ("OK".equals(future.get())) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(successCount).isEqualTo(numRequests);
|
||||
assertThat(duration).isLessThan(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionRecycling() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectionTimeToLive(500, TimeUnit.MILLISECONDS).connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String response = api.getTest();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
Thread.sleep(600);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultConnectionTimeToLive() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
String response = api.getTest();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSequentialRequests() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
String response = api.getTest();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
assertThat(duration).isLessThan(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionPoolWithDifferentEndpoints() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test2"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK2\"")));
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test3"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK3\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(5);
|
||||
List<Future<String>> futures = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int endpoint = i % 3;
|
||||
futures.add(executor.submit(() -> {
|
||||
switch (endpoint) {
|
||||
case 0:
|
||||
return api.getTest();
|
||||
case 1:
|
||||
return api.getTest2();
|
||||
default:
|
||||
return api.getTest3();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for (Future<String> future : futures) {
|
||||
assertThat(future.get()).isIn("OK", "OK2", "OK3");
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
interface TestApi {
|
||||
|
||||
@RequestLine("GET /test")
|
||||
String getTest();
|
||||
|
||||
@RequestLine("GET /test2")
|
||||
String getTest2();
|
||||
|
||||
@RequestLine("GET /test3")
|
||||
String getTest3();
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.services;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.apache.fineract.client.feign.services.DocumentsApiFixed;
|
||||
import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.DocumentData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@Tag("integration")
|
||||
class DocumentsApiFixedIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private FineractFeignClientConfig config;
|
||||
private File testDocumentFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
testDocumentFile = new File(tempDir, "test-doc.pdf");
|
||||
Files.write(testDocumentFile.toPath(), "%PDF-1.4 test content".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDocumentsForClient() {
|
||||
String responseBody = "[{\"id\":1,\"name\":\"Document1\",\"description\":\"Test document\"}]";
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
List<DocumentData> documents = api.retrieveAllDocuments("clients", 123L);
|
||||
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).hasSize(1);
|
||||
assertThat(documents.get(0).getId()).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDocumentsForLoan() {
|
||||
String responseBody = "[{\"id\":2,\"name\":\"LoanDocument\",\"description\":\"Loan agreement\"}]";
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/loans/456/documents"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
List<DocumentData> documents = api.retrieveAllDocuments("loans", 456L);
|
||||
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).hasSize(1);
|
||||
assertThat(documents.get(0).getName()).isEqualTo("LoanDocument");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDocumentsEmpty() {
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/999/documents"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("[]")));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
List<DocumentData> documents = api.retrieveAllDocuments("clients", 999L);
|
||||
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSingleDocument() {
|
||||
String responseBody = "{\"id\":1,\"name\":\"Document1\",\"description\":\"Test document\",\"fileName\":\"doc.pdf\"}";
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/1"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
DocumentData document = api.getDocument("clients", 123L, 1L);
|
||||
|
||||
assertThat(document).isNotNull();
|
||||
assertThat(document.getId()).isEqualTo(1L);
|
||||
assertThat(document.getName()).isEqualTo("Document1");
|
||||
assertThat(document.getFileName()).isEqualTo("doc.pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadDocument() throws IOException {
|
||||
byte[] docContent = "PDF document content here".getBytes(StandardCharsets.UTF_8);
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/456/attachment"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/pdf")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"document.pdf\"").withBody(docContent)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
Response response = api.downloadFile("clients", 123L, 456L);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.headers().get("Content-Type")).contains("application/pdf");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(docContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteDocument() {
|
||||
String responseBody = "{\"resourceId\":456}";
|
||||
wireMockServer.stubFor(delete(urlEqualTo("/v1/clients/123/documents/456"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("clients", 123L, 456L);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getResourceId()).isEqualTo(456L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteDocumentForLoan() {
|
||||
String responseBody = "{\"resourceId\":999}";
|
||||
wireMockServer.stubFor(delete(urlEqualTo("/v1/loans/789/documents/999"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("loans", 789L, 999L);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getResourceId()).isEqualTo(999L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleEntityTypes() {
|
||||
String[] entityTypes = { "clients", "loans", "savings", "groups" };
|
||||
Long[] entityIds = { 100L, 200L, 300L, 400L };
|
||||
|
||||
for (int i = 0; i < entityTypes.length; i++) {
|
||||
String entityType = entityTypes[i];
|
||||
Long entityId = entityIds[i];
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/" + entityType + "/" + entityId + "/documents"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("[]")));
|
||||
}
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
|
||||
for (int i = 0; i < entityTypes.length; i++) {
|
||||
List<DocumentData> documents = api.retrieveAllDocuments(entityTypes[i], entityIds[i]);
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadTextDocument() throws IOException {
|
||||
String textContent = "Plain text document content";
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/999/attachment"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/plain")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"readme.txt\"").withBody(textContent)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
Response response = api.downloadFile("clients", 123L, 999L);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers().get("Content-Type")).contains("text/plain");
|
||||
String content = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo(textContent);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.services;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.apache.fineract.client.feign.services.ImagesApi;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@Tag("integration")
|
||||
class ImagesApiIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private FineractFeignClientConfig config;
|
||||
private File testImageFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
testImageFile = new File(tempDir, "test-image.jpg");
|
||||
Files.write(testImageFile.toPath(), new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x01, 0x02, 0x03 });
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveClientImage() throws IOException {
|
||||
byte[] imageData = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x10, 0x11, 0x12 };
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(imageData)));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Response response = api.get("clients", 123L, new HashMap<>());
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.headers().get("Content-Type")).contains("image/jpeg");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(imageData);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveClientImageWithMaxDimensions() throws IOException {
|
||||
byte[] resizedImage = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x20, 0x21 };
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/clients/123/images")).withQueryParam("maxWidth", equalTo("100"))
|
||||
.withQueryParam("maxHeight", equalTo("100"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(resizedImage)));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("maxWidth", 100);
|
||||
params.put("maxHeight", 100);
|
||||
|
||||
Response response = api.get("clients", 123L, params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(resizedImage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveClientImageWithMaxWidthOnly() throws IOException {
|
||||
byte[] resizedImage = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x30 };
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/clients/123/images")).withQueryParam("maxWidth", equalTo("200"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(resizedImage)));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("maxWidth", 200);
|
||||
|
||||
Response response = api.get("clients", 123L, params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(resizedImage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteClientImage() {
|
||||
wireMockServer.stubFor(delete(urlEqualTo("/v1/clients/123/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"resourceId\":123}")));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Response response = api.delete("clients", 123L);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteStaffImage() {
|
||||
wireMockServer.stubFor(delete(urlEqualTo("/v1/staff/456/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"resourceId\":456}")));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Response response = api.delete("staff", 456L);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveImageWithPngFormat() throws IOException {
|
||||
byte[] pngImage = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A };
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/png").withBody(pngImage)));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Response response = api.get("clients", 123L, new HashMap<>());
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers().get("Content-Type")).contains("image/png");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(pngImage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleEntityTypes() {
|
||||
String[] entityTypes = { "clients", "staff" };
|
||||
Long[] entityIds = { 100L, 200L };
|
||||
|
||||
for (int i = 0; i < entityTypes.length; i++) {
|
||||
String entityType = entityTypes[i];
|
||||
Long entityId = entityIds[i];
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/" + entityType + "/" + entityId + "/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(new byte[] { 0x01 })));
|
||||
}
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
|
||||
for (int i = 0; i < entityTypes.length; i++) {
|
||||
Response response = api.get(entityTypes[i], entityIds[i], new HashMap<>());
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.services;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.Response;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.apache.fineract.client.feign.services.RunReportsApi;
|
||||
import org.apache.fineract.client.models.RunReportsResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Tag("integration")
|
||||
class RunReportsApiIntegrationTest {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private FineractFeignClientConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetDataWithMultipleParameters() {
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/DateRangeReport")).withQueryParam("officeId", equalTo("1"))
|
||||
.withQueryParam("fromDate", equalTo("2024-01-01")).withQueryParam("toDate", equalTo("2024-12-31")).willReturn(aResponse()
|
||||
.withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("officeId", "1");
|
||||
params.put("fromDate", "2024-01-01");
|
||||
params.put("toDate", "2024-12-31");
|
||||
|
||||
RunReportsResponse response = api.runReportGetData("DateRangeReport", params);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetDataWithEmptyResult() {
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/EmptyReport")).withQueryParam("officeId", equalTo("1")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("officeId", "1");
|
||||
|
||||
RunReportsResponse response = api.runReportGetData("EmptyReport", params);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getColumnHeaders()).isEmpty();
|
||||
assertThat(response.getData()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetFileAsPdf() throws IOException {
|
||||
byte[] pdfContent = "%PDF-1.4 test content".getBytes(StandardCharsets.UTF_8);
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/LoanSummary")).withQueryParam("output-type", equalTo("PDF"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/pdf")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"report.pdf\"").withBody(pdfContent)));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("output-type", "PDF");
|
||||
|
||||
Response response = api.runReportGetFile("LoanSummary", params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.headers().get("Content-Type")).contains("application/pdf");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(pdfContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetFileAsCsv() throws IOException {
|
||||
String csvContent = "id,name,balance\n1,Client1,1000.00\n2,Client2,2000.00";
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/ClientBalance")).withQueryParam("exportCSV", equalTo("true"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/csv")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"report.csv\"").withBody(csvContent)));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("exportCSV", "true");
|
||||
|
||||
Response response = api.runReportGetFile("ClientBalance", params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.headers().get("Content-Type")).contains("text/csv");
|
||||
String content = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo(csvContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetFileAsExcel() throws IOException {
|
||||
byte[] excelContent = "fake excel data".getBytes(StandardCharsets.UTF_8);
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/FinancialReport")).withQueryParam("output-type", equalTo("XLS"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/vnd.ms-excel")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"report.xls\"").withBody(excelContent)));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("output-type", "XLS");
|
||||
|
||||
Response response = api.runReportGetFile("FinancialReport", params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(excelContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetDataWithNoParameters() {
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/AllClients")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
|
||||
RunReportsResponse response = api.runReportGetData("AllClients", new HashMap<>());
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.client.util;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class FeignPartsTest {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
@Test
|
||||
void testMediaTypeForJpeg() {
|
||||
String mediaType = FeignParts.mediaType("photo.jpg");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForJpegUppercase() {
|
||||
String mediaType = FeignParts.mediaType("photo.JPEG");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForPng() {
|
||||
String mediaType = FeignParts.mediaType("image.png");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/png");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForPdf() {
|
||||
String mediaType = FeignParts.mediaType("document.pdf");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForDocx() {
|
||||
String mediaType = FeignParts.mediaType("report.docx");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForDoc() {
|
||||
String mediaType = FeignParts.mediaType("report.doc");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/msword");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForXlsx() {
|
||||
String mediaType = FeignParts.mediaType("spreadsheet.xlsx");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForXls() {
|
||||
String mediaType = FeignParts.mediaType("spreadsheet.xls");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.ms-excel");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForTiff() {
|
||||
String mediaType = FeignParts.mediaType("scan.tiff");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/tiff");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForGif() {
|
||||
String mediaType = FeignParts.mediaType("animation.gif");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/gif");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForOdt() {
|
||||
String mediaType = FeignParts.mediaType("document.odt");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.oasis.opendocument.text");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForOds() {
|
||||
String mediaType = FeignParts.mediaType("spreadsheet.ods");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.oasis.opendocument.spreadsheet");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForTxt() {
|
||||
String mediaType = FeignParts.mediaType("readme.txt");
|
||||
|
||||
assertThat(mediaType).isEqualTo("text/plain");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForUnknownExtension() {
|
||||
String mediaType = FeignParts.mediaType("file.xyz");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/octet-stream");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeCaseInsensitive() {
|
||||
assertThat(FeignParts.mediaType("FILE.JPG")).isEqualTo("image/jpeg");
|
||||
assertThat(FeignParts.mediaType("file.PDF")).isEqualTo("application/pdf");
|
||||
assertThat(FeignParts.mediaType("Document.DOCX"))
|
||||
.isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForNullFilename() {
|
||||
String mediaType = FeignParts.mediaType(null);
|
||||
|
||||
assertThat(mediaType).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForFilenameWithoutExtension() {
|
||||
String mediaType = FeignParts.mediaType("filename");
|
||||
|
||||
assertThat(mediaType).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameExtractionFromContentDisposition() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
|
||||
.headers(Map.of("Content-Disposition", List.of("attachment; filename=\"test.pdf\""))).body(new byte[0]).build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isPresent();
|
||||
assertThat(filename.get()).isEqualTo("test.pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameExtractionWithComplexFilename() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
|
||||
.headers(Map.of("Content-Disposition", List.of("attachment; filename=\"report-2024-01-15.xlsx\""))).body(new byte[0])
|
||||
.build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isPresent();
|
||||
assertThat(filename.get()).isEqualTo("report-2024-01-15.xlsx");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameWithoutContentDisposition() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null)).headers(Map.of()).body(new byte[0])
|
||||
.build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameWithNullHeaders() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null)).headers(null).body(new byte[0])
|
||||
.build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameWithEmptyContentDisposition() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
|
||||
.headers(Map.of("Content-Disposition", Collections.emptyList())).body(new byte[0]).build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameWithInvalidContentDisposition() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
|
||||
.headers(Map.of("Content-Disposition", List.of("inline"))).body(new byte[0]).build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProbeContentTypeForJpegFile() throws IOException {
|
||||
File jpegFile = new File(tempDir, "test.jpg");
|
||||
Files.write(jpegFile.toPath(), new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF });
|
||||
|
||||
String contentType = FeignParts.probeContentType(jpegFile);
|
||||
|
||||
assertThat(contentType).isIn("image/jpeg", "image/jpg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProbeContentTypeForPdfFile() throws IOException {
|
||||
File pdfFile = new File(tempDir, "test.pdf");
|
||||
Files.write(pdfFile.toPath(), "%PDF-1.4".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String contentType = FeignParts.probeContentType(pdfFile);
|
||||
|
||||
assertThat(contentType).isEqualTo("application/pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProbeContentTypeFallsBackToMediaType() throws IOException {
|
||||
File unknownFile = new File(tempDir, "test.docx");
|
||||
Files.write(unknownFile.toPath(), "test content".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String contentType = FeignParts.probeContentType(unknownFile);
|
||||
|
||||
assertThat(contentType).isNotNull();
|
||||
assertThat(contentType).isNotEqualTo("application/octet-stream");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProbeContentTypeForUnknownFile() throws IOException {
|
||||
File unknownFile = new File(tempDir, "test.xyz");
|
||||
Files.write(unknownFile.toPath(), "unknown content".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String contentType = FeignParts.probeContentType(unknownFile);
|
||||
|
||||
assertThat(contentType).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForAllSupportedImageFormats() {
|
||||
assertThat(FeignParts.mediaType("image.jpg")).isEqualTo("image/jpeg");
|
||||
assertThat(FeignParts.mediaType("image.jpeg")).isEqualTo("image/jpeg");
|
||||
assertThat(FeignParts.mediaType("image.png")).isEqualTo("image/png");
|
||||
assertThat(FeignParts.mediaType("image.gif")).isEqualTo("image/gif");
|
||||
assertThat(FeignParts.mediaType("image.tif")).isEqualTo("image/tiff");
|
||||
assertThat(FeignParts.mediaType("image.tiff")).isEqualTo("image/tiff");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForAllSupportedDocumentFormats() {
|
||||
assertThat(FeignParts.mediaType("doc.pdf")).isEqualTo("application/pdf");
|
||||
assertThat(FeignParts.mediaType("doc.doc")).isEqualTo("application/msword");
|
||||
assertThat(FeignParts.mediaType("doc.docx")).isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
assertThat(FeignParts.mediaType("doc.odt")).isEqualTo("application/vnd.oasis.opendocument.text");
|
||||
assertThat(FeignParts.mediaType("doc.txt")).isEqualTo("text/plain");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForAllSupportedSpreadsheetFormats() {
|
||||
assertThat(FeignParts.mediaType("sheet.xls")).isEqualTo("application/vnd.ms-excel");
|
||||
assertThat(FeignParts.mediaType("sheet.xlsx")).isEqualTo("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
assertThat(FeignParts.mediaType("sheet.ods")).isEqualTo("application/vnd.oasis.opendocument.spreadsheet");
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -32,11 +32,19 @@ public class BasicPasswordEncodablePlatformUser implements PlatformUser {
|
||||
|
||||
@Getter
|
||||
private Long id;
|
||||
@Getter(onMethod_ = @Override)
|
||||
private String username;
|
||||
@Getter(onMethod_ = @Override)
|
||||
private String password;
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<GrantedAuthority> getAuthorities() {
|
||||
return null;
|
||||
|
||||
@@ -88,15 +88,69 @@ cargo {
|
||||
|
||||
cargoRunLocal.dependsOn ':fineract-war:war'
|
||||
cargoStartLocal.dependsOn ':fineract-war:war'
|
||||
cargoStartLocal.mustRunAfter 'testClasses'
|
||||
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
tasks.register('waitForFineract') {
|
||||
doLast {
|
||||
int timeoutSeconds = (project.findProperty('waitForFineractTimeoutSeconds') ?: '600') as int
|
||||
int waited = 0
|
||||
int interval = 5
|
||||
|
||||
TrustManager[] trustAllCerts = [
|
||||
new X509TrustManager() {
|
||||
java.security.cert.X509Certificate[] getAcceptedIssuers() { return null }
|
||||
void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
|
||||
void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
|
||||
}
|
||||
] as TrustManager[]
|
||||
|
||||
SSLContext sc = SSLContext.getInstance("SSL")
|
||||
sc.init(null, trustAllCerts, new java.security.SecureRandom())
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory())
|
||||
HttpsURLConnection.setDefaultHostnameVerifier({ hostname, session -> true })
|
||||
|
||||
URL url = new URL("https://localhost:8443/fineract-provider/actuator/health")
|
||||
println "Waiting for Fineract startup (timeout: ${timeoutSeconds}s)..."
|
||||
|
||||
while (waited < timeoutSeconds) {
|
||||
try {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection()
|
||||
connection.setConnectTimeout(2000)
|
||||
connection.setReadTimeout(2000)
|
||||
connection.setRequestMethod("GET")
|
||||
int responseCode = connection.getResponseCode()
|
||||
if (responseCode == 200) {
|
||||
println "Fineract is up!"
|
||||
return
|
||||
}
|
||||
} catch (Exception ignored) { }
|
||||
sleep(interval * 1000)
|
||||
waited += interval
|
||||
println "Still waiting..."
|
||||
}
|
||||
|
||||
throw new GradleException("Fineract did not start within ${timeoutSeconds} seconds")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
if (!project.hasProperty('cargoDisabled')) {
|
||||
test {
|
||||
dependsOn(cargoStartLocal)
|
||||
dependsOn cargoStartLocal, waitForFineract
|
||||
finalizedBy cargoStopLocal
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.hasProperty('cargoDisabled')) {
|
||||
cargoStartLocal.mustRunAfter testClasses
|
||||
waitForFineract.mustRunAfter cargoStartLocal
|
||||
}
|
||||
|
||||
// Configure proper test output directories
|
||||
sourceSets {
|
||||
test {
|
||||
|
||||
@@ -21,7 +21,8 @@ dependencies {
|
||||
// Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly!
|
||||
//
|
||||
tomcat 'org.apache.tomcat:tomcat:10.1.42@zip'
|
||||
testImplementation( files("$rootDir/fineract-provider/build/classes/java/main/"),
|
||||
def providerMainOutput = project(':fineract-provider').extensions.getByType(SourceSetContainer).named('main').get().output
|
||||
testImplementation( providerMainOutput,
|
||||
project(path: ':fineract-core', configuration: 'runtimeElements'),
|
||||
project(path: ':fineract-accounting', configuration: 'runtimeElements'),
|
||||
project(path: ':fineract-investor', configuration: 'runtimeElements'),
|
||||
@@ -32,9 +33,17 @@ dependencies {
|
||||
project(path: ':fineract-savings', configuration: 'runtimeElements'),
|
||||
project(path: ':fineract-provider', configuration: 'runtimeElements'),
|
||||
project(path: ':fineract-avro-schemas', configuration: 'runtimeElements'),
|
||||
project(path: ':fineract-client', configuration: 'runtimeElements'),
|
||||
project(path: ':fineract-progressive-loan', configuration: 'runtimeElements')
|
||||
)
|
||||
|
||||
// Retrofit client for existing tests
|
||||
testImplementation project(path: ':fineract-client', configuration: 'runtimeElements')
|
||||
|
||||
// Feign client - exclude fineract-client module to avoid Retrofit/Feign model conflicts
|
||||
// Both generated and hand-written Feign services are available in feign.services package
|
||||
testImplementation(project(path: ':fineract-client-feign', configuration: 'runtimeElements')) {
|
||||
exclude group: 'org.apache.fineract', module: 'fineract-client'
|
||||
}
|
||||
testImplementation ('org.mock-server:mockserver-junit-jupiter') {
|
||||
exclude group: 'com.sun.mail', module: 'mailapi'
|
||||
exclude group: 'javax.servlet', module: 'javax.servlet-api'
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client;
|
||||
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.apache.fineract.integrationtests.ConfigProperties;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Simple smoke tests for Feign-based Fineract client
|
||||
*
|
||||
*/
|
||||
public class FeignClientSmokeTest extends FeignIntegrationTest {
|
||||
|
||||
private String getTestUrl() {
|
||||
return System.getProperty("fineract.it.url", ConfigProperties.Backend.PROTOCOL + "://" + ConfigProperties.Backend.HOST + ":"
|
||||
+ ConfigProperties.Backend.PORT + "/fineract-provider/api");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testFeignClientBuilder() {
|
||||
FineractFeignClient.Builder builder = FineractFeignClient.builder().baseUrl(getTestUrl())
|
||||
.credentials(ConfigProperties.Backend.USERNAME, ConfigProperties.Backend.PASSWORD);
|
||||
|
||||
assertThat(builder).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testFeignClientConfig() {
|
||||
FineractFeignClientConfig.Builder configBuilder = FineractFeignClientConfig.builder().baseUrl(getTestUrl())
|
||||
.credentials(ConfigProperties.Backend.USERNAME, ConfigProperties.Backend.PASSWORD).debugEnabled(true);
|
||||
|
||||
FineractFeignClientConfig config = configBuilder.build();
|
||||
|
||||
assertThat(config).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
public void testFeignClientCanInstantiateApis() {
|
||||
assertThat(fineractClient()).isNotNull();
|
||||
assertThat(fineractClient().offices()).isNotNull();
|
||||
assertThat(fineractClient().clients()).isNotNull();
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import org.apache.fineract.client.feign.FeignException;
|
||||
import org.apache.fineract.client.feign.FineractMultipartEncoder;
|
||||
import org.apache.fineract.client.models.DocumentData;
|
||||
import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class FeignDocumentTest extends FeignIntegrationTest {
|
||||
|
||||
final File testFile = new File(getClass().getResource("/michael.vorburger-crepes.jpg").getFile());
|
||||
|
||||
Long clientId;
|
||||
Long documentId;
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void setupClient() {
|
||||
FeignClientHelper clientHelper = new FeignClientHelper(fineractClient());
|
||||
clientId = clientHelper.createClient("Feign", "Test");
|
||||
assertThat(clientId).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void testCreateDocument() throws IOException {
|
||||
String name = "Feign Test Document";
|
||||
String description = "Testing DocumentsApiFixed with Feign client";
|
||||
|
||||
byte[] fileData = Files.readAllBytes(testFile.toPath());
|
||||
FineractMultipartEncoder.MultipartData multipartData = new FineractMultipartEncoder.MultipartData()
|
||||
.addFile("file", testFile.getName(), fileData, "image/jpeg").addText("name", name).addText("description", description);
|
||||
|
||||
PostEntityTypeEntityIdDocumentsResponse response = ok(
|
||||
() -> fineractClient().documentsFixed().createDocument("clients", clientId, multipartData));
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getResourceId()).isNotNull();
|
||||
documentId = response.getResourceId();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
void testRetrieveAllDocuments() {
|
||||
var documents = ok(() -> fineractClient().documentsFixed().retrieveAllDocuments("clients", clientId));
|
||||
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
void testGetDocument() {
|
||||
DocumentData doc = ok(() -> fineractClient().documentsFixed().getDocument("clients", clientId, documentId));
|
||||
|
||||
assertThat(doc).isNotNull();
|
||||
assertThat(doc.getName()).isEqualTo("Feign Test Document");
|
||||
assertThat(doc.getFileName()).isEqualTo(testFile.getName());
|
||||
assertThat(doc.getDescription()).isEqualTo("Testing DocumentsApiFixed with Feign client");
|
||||
assertThat(doc.getId()).isEqualTo(documentId);
|
||||
assertThat(doc.getParentEntityType()).isEqualTo("clients");
|
||||
assertThat(doc.getParentEntityId()).isEqualTo(clientId);
|
||||
assertThat(doc.getType()).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
void testDownloadFile() throws IOException {
|
||||
Response response = fineractClient().documentsFixed().downloadFile("clients", clientId, documentId);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.status());
|
||||
|
||||
try (InputStream inputStream = response.body().asInputStream()) {
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
assertThat(bytes.length).isEqualTo((int) testFile.length());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
void testUpdateDocument() {
|
||||
String newName = "Updated Feign Test";
|
||||
String newDescription = "Updated via Feign client";
|
||||
|
||||
FineractMultipartEncoder.MultipartData multipartData = new FineractMultipartEncoder.MultipartData().addText("name", newName)
|
||||
.addText("description", newDescription);
|
||||
|
||||
var updateResponse = ok(() -> fineractClient().documentsFixed().updateDocument("clients", clientId, documentId, multipartData));
|
||||
|
||||
assertThat(updateResponse).isNotNull();
|
||||
|
||||
DocumentData doc = ok(() -> fineractClient().documentsFixed().getDocument("clients", clientId, documentId));
|
||||
assertThat(doc.getName()).isEqualTo(newName);
|
||||
assertThat(doc.getDescription()).isEqualTo(newDescription);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(99)
|
||||
void testDeleteDocument() {
|
||||
var deleteResponse = ok(() -> fineractClient().documentsFixed().deleteDocument("clients", clientId, documentId));
|
||||
assertThat(deleteResponse).isNotNull();
|
||||
|
||||
FeignException exception = assertThrows(FeignException.class,
|
||||
() -> fineractClient().documentsFixed().getDocument("clients", clientId, documentId));
|
||||
assertEquals(404, exception.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9999)
|
||||
void testCreateDocumentBadArgs() {
|
||||
FineractMultipartEncoder.MultipartData multipartData = new FineractMultipartEncoder.MultipartData().addText("name", "test.pdf");
|
||||
|
||||
FeignException exception = assertThrows(FeignException.class,
|
||||
() -> fineractClient().documentsFixed().createDocument("clients", clientId, multipartData));
|
||||
assertEquals(400, exception.status());
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.HashMap;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.models.CreateStaffResponse;
|
||||
import org.apache.fineract.client.models.StaffRequest;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class FeignImageTest extends FeignIntegrationTest {
|
||||
|
||||
final File testImage = new File(getClass().getResource("/michael.vorburger-crepes.jpg").getFile());
|
||||
|
||||
Long staffId;
|
||||
|
||||
@Override
|
||||
protected FineractFeignClient fineractClient() {
|
||||
return org.apache.fineract.integrationtests.common.FineractFeignClientHelper.createNewFineractFeignClient("mifos", "password",
|
||||
true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void setupStaff() {
|
||||
StaffRequest request = new StaffRequest();
|
||||
request.setOfficeId(1L);
|
||||
request.setFirstname("Feign");
|
||||
request.setLastname("ImageTest" + System.currentTimeMillis());
|
||||
request.setJoiningDate(LocalDate.now(ZoneId.of("UTC")).toString());
|
||||
request.setDateFormat("yyyy-MM-dd");
|
||||
request.setLocale("en_US");
|
||||
|
||||
CreateStaffResponse response = ok(() -> fineractClient().staff().create3(request));
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getResourceId()).isNotNull();
|
||||
staffId = response.getResourceId();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void testCreateStaffImage() throws Exception {
|
||||
String dataUrl = org.apache.fineract.client.feign.services.ImagesApi.prepareFileUpload(testImage);
|
||||
Response response = fineractClient().images().create("staff", staffId, dataUrl);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
void testRetrieveStaffImage() throws IOException {
|
||||
Response response = fineractClient().images().get("staff", staffId, new HashMap<>());
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.status());
|
||||
|
||||
try (InputStream inputStream = response.body().asInputStream()) {
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
assertThat(bytes.length).isGreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
void testUpdateStaffImage() {
|
||||
Response response = fineractClient().images().update("staff", staffId,
|
||||
org.apache.fineract.client.feign.services.ImagesApi.prepareFileUpload(testImage));
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(99)
|
||||
void testDeleteStaffImage() {
|
||||
Response response = fineractClient().images().delete("staff", staffId);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.status());
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.feign.util.FeignCalls;
|
||||
import org.apache.fineract.integrationtests.common.FineractFeignClientHelper;
|
||||
import org.assertj.core.api.AbstractBigDecimalAssert;
|
||||
import org.assertj.core.api.AbstractBooleanAssert;
|
||||
import org.assertj.core.api.AbstractDoubleAssert;
|
||||
import org.assertj.core.api.AbstractFloatAssert;
|
||||
import org.assertj.core.api.AbstractIntegerAssert;
|
||||
import org.assertj.core.api.AbstractLongAssert;
|
||||
import org.assertj.core.api.AbstractStringAssert;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.api.IterableAssert;
|
||||
import org.assertj.core.api.ObjectAssert;
|
||||
import org.assertj.core.api.OptionalAssert;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
* Base Integration Test class for Feign-based client
|
||||
*
|
||||
* @author Apache Fineract
|
||||
*/
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public abstract class FeignIntegrationTest {
|
||||
|
||||
protected FineractFeignClient fineractClient() {
|
||||
return FineractFeignClientHelper.getFineractFeignClient();
|
||||
}
|
||||
|
||||
protected <T> T ok(Supplier<T> call) {
|
||||
return FeignCalls.ok(call);
|
||||
}
|
||||
|
||||
protected void executeVoid(Runnable call) {
|
||||
FeignCalls.executeVoid(call);
|
||||
}
|
||||
|
||||
public static IterableAssert<?> assertThat(Iterable<?> actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static AbstractBigDecimalAssert<?> assertThat(BigDecimal actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static <T> ObjectAssert<T> assertThat(T actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static AbstractLongAssert<?> assertThat(Long actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static AbstractDoubleAssert<?> assertThat(Double actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static AbstractFloatAssert<?> assertThat(Float actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static AbstractIntegerAssert<?> assertThat(Integer actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static AbstractBooleanAssert<?> assertThat(Boolean actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static AbstractStringAssert<?> assertThat(String actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
|
||||
public static <T> OptionalAssert<T> assertThat(Optional<T> actual) {
|
||||
return Assertions.assertThat(actual);
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.function.Function;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdStatus;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||
import org.apache.fineract.client.models.PostLoansRequest;
|
||||
import org.apache.fineract.integrationtests.client.FeignIntegrationTest;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignAccountHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignBusinessDateHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignJournalEntryHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignTransactionHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanProductTemplates;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanRequestBuilders;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestAccounts;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestValidators;
|
||||
import org.apache.fineract.integrationtests.common.FineractFeignClientHelper;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
|
||||
public abstract class FeignLoanTestBase extends FeignIntegrationTest implements LoanProductTemplates {
|
||||
|
||||
protected static FeignAccountHelper accountHelper;
|
||||
protected static FeignLoanHelper loanHelper;
|
||||
protected static FeignTransactionHelper transactionHelper;
|
||||
protected static FeignJournalEntryHelper journalHelper;
|
||||
protected static FeignBusinessDateHelper businessDateHelper;
|
||||
protected static FeignClientHelper clientHelper;
|
||||
protected static LoanTestAccounts accounts;
|
||||
|
||||
@BeforeAll
|
||||
public static void setupHelpers() {
|
||||
FineractFeignClient client = FineractFeignClientHelper.getFineractFeignClient();
|
||||
accountHelper = new FeignAccountHelper(client);
|
||||
loanHelper = new FeignLoanHelper(client);
|
||||
transactionHelper = new FeignTransactionHelper(client);
|
||||
journalHelper = new FeignJournalEntryHelper(client);
|
||||
businessDateHelper = new FeignBusinessDateHelper(client);
|
||||
clientHelper = new FeignClientHelper(client);
|
||||
}
|
||||
|
||||
protected LoanTestAccounts getAccounts() {
|
||||
if (accounts == null) {
|
||||
accounts = new LoanTestAccounts(accountHelper);
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getAssetAccountId(String accountName) {
|
||||
return getAccounts().getAssetAccountId(accountName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getLiabilityAccountId(String accountName) {
|
||||
return getAccounts().getLiabilityAccountId(accountName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getIncomeAccountId(String accountName) {
|
||||
return getAccounts().getIncomeAccountId(accountName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getExpenseAccountId(String accountName) {
|
||||
return getAccounts().getExpenseAccountId(accountName);
|
||||
}
|
||||
|
||||
protected Long createClient(String firstName, String lastName) {
|
||||
return clientHelper.createClient(firstName, lastName);
|
||||
}
|
||||
|
||||
protected Long createLoanProduct(PostLoanProductsRequest request) {
|
||||
return loanHelper.createLoanProduct(request);
|
||||
}
|
||||
|
||||
protected Long applyForLoan(PostLoansRequest request) {
|
||||
return loanHelper.applyForLoan(request);
|
||||
}
|
||||
|
||||
protected Long approveLoan(Long loanId, PostLoansLoanIdRequest request) {
|
||||
return loanHelper.approveLoan(loanId, request);
|
||||
}
|
||||
|
||||
protected Long disburseLoan(Long loanId, PostLoansLoanIdRequest request) {
|
||||
return loanHelper.disburseLoan(loanId, request);
|
||||
}
|
||||
|
||||
protected GetLoansLoanIdResponse getLoanDetails(Long loanId) {
|
||||
return loanHelper.getLoanDetails(loanId);
|
||||
}
|
||||
|
||||
protected void undoApproval(Long loanId) {
|
||||
loanHelper.undoApproval(loanId);
|
||||
}
|
||||
|
||||
protected void undoDisbursement(Long loanId) {
|
||||
loanHelper.undoDisbursement(loanId);
|
||||
}
|
||||
|
||||
protected Long addRepayment(Long loanId, PostLoansLoanIdTransactionsRequest request) {
|
||||
return transactionHelper.addRepayment(loanId, request);
|
||||
}
|
||||
|
||||
protected Long addInterestWaiver(Long loanId, PostLoansLoanIdTransactionsRequest request) {
|
||||
return transactionHelper.addInterestWaiver(loanId, request);
|
||||
}
|
||||
|
||||
protected Long chargeOff(Long loanId, PostLoansLoanIdTransactionsRequest request) {
|
||||
return transactionHelper.chargeOff(loanId, request);
|
||||
}
|
||||
|
||||
protected Long addChargeback(Long loanId, Long transactionId, PostLoansLoanIdTransactionsRequest request) {
|
||||
return transactionHelper.addChargeback(loanId, transactionId, request);
|
||||
}
|
||||
|
||||
protected void undoRepayment(Long loanId, Long transactionId, String transactionDate) {
|
||||
transactionHelper.undoRepayment(loanId, transactionId, transactionDate);
|
||||
}
|
||||
|
||||
protected void verifyJournalEntries(Long loanId, LoanTestData.Journal... expectedEntries) {
|
||||
journalHelper.verifyJournalEntries(loanId, expectedEntries);
|
||||
}
|
||||
|
||||
protected void verifyJournalEntriesSequentially(Long loanId, LoanTestData.Journal... expectedEntries) {
|
||||
journalHelper.verifyJournalEntriesSequentially(loanId, expectedEntries);
|
||||
}
|
||||
|
||||
protected void runAt(String date, Runnable action) {
|
||||
businessDateHelper.runAt(date, action);
|
||||
}
|
||||
|
||||
protected void updateBusinessDate(String type, String date) {
|
||||
businessDateHelper.updateBusinessDate(type, date);
|
||||
}
|
||||
|
||||
protected void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
|
||||
double principalPaid, double principalOutstanding) {
|
||||
LoanTestValidators.validateRepaymentPeriod(loanDetails, index, dueDate, principalDue, principalPaid, principalOutstanding, 0.0,
|
||||
0.0);
|
||||
}
|
||||
|
||||
protected void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
|
||||
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
|
||||
LoanTestValidators.validateRepaymentPeriod(loanDetails, index, dueDate, principalDue, principalPaid, principalOutstanding,
|
||||
paidInAdvance, paidLate);
|
||||
}
|
||||
|
||||
protected void verifyLoanStatus(GetLoansLoanIdResponse loanDetails, Function<GetLoansLoanIdStatus, Boolean> extractor) {
|
||||
LoanTestValidators.verifyLoanStatus(loanDetails, extractor);
|
||||
}
|
||||
|
||||
protected Long createApproveAndDisburseLoan(Long clientId, Long productId, String date, Double principal, Integer numberOfRepayments) {
|
||||
PostLoansRequest applyRequest = LoanRequestBuilders.applyLoan(clientId, productId, date, principal, numberOfRepayments);
|
||||
Long loanId = applyForLoan(applyRequest);
|
||||
|
||||
PostLoansLoanIdRequest approveRequest = LoanRequestBuilders.approveLoan(principal, date);
|
||||
approveLoan(loanId, approveRequest);
|
||||
|
||||
PostLoansLoanIdRequest disburseRequest = LoanRequestBuilders.disburseLoan(principal, date);
|
||||
disburseLoan(loanId, disburseRequest);
|
||||
|
||||
return loanId;
|
||||
}
|
||||
|
||||
protected Long createApprovedLoan(Long clientId, Long productId, String date, Double principal, Integer numberOfRepayments) {
|
||||
PostLoansRequest applyRequest = LoanRequestBuilders.applyLoan(clientId, productId, date, principal, numberOfRepayments);
|
||||
Long loanId = applyForLoan(applyRequest);
|
||||
|
||||
PostLoansLoanIdRequest approveRequest = LoanRequestBuilders.approveLoan(principal, date);
|
||||
approveLoan(loanId, approveRequest);
|
||||
|
||||
return loanId;
|
||||
}
|
||||
|
||||
protected LoanTestData.Journal debit(Long glAccountId, double amount) {
|
||||
return LoanTestData.Journal.debit(glAccountId, amount);
|
||||
}
|
||||
|
||||
protected LoanTestData.Journal credit(Long glAccountId, double amount) {
|
||||
return LoanTestData.Journal.credit(glAccountId, amount);
|
||||
}
|
||||
|
||||
protected PostLoansLoanIdTransactionsRequest repayment(double amount, String date) {
|
||||
return LoanRequestBuilders.repayLoan(amount, date);
|
||||
}
|
||||
|
||||
protected PostLoansLoanIdTransactionsRequest waiveInterest(double amount, String date) {
|
||||
return LoanRequestBuilders.waiveInterest(amount, date);
|
||||
}
|
||||
|
||||
protected PostLoansLoanIdTransactionsRequest chargeOff(String date) {
|
||||
return LoanRequestBuilders.chargeOff(date);
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.helpers;
|
||||
|
||||
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.models.GetGLAccountsResponse;
|
||||
import org.apache.fineract.client.models.PostGLAccountsRequest;
|
||||
import org.apache.fineract.client.models.PostGLAccountsResponse;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
|
||||
public class FeignAccountHelper {
|
||||
|
||||
private final FineractFeignClient fineractClient;
|
||||
|
||||
public FeignAccountHelper(FineractFeignClient fineractClient) {
|
||||
this.fineractClient = fineractClient;
|
||||
}
|
||||
|
||||
public Account createAssetAccount(String name) {
|
||||
return createAccount(name, "1", "ASSET");
|
||||
}
|
||||
|
||||
public Account createLiabilityAccount(String name) {
|
||||
return createAccount(name, "2", "LIABILITY");
|
||||
}
|
||||
|
||||
public Account createIncomeAccount(String name) {
|
||||
return createAccount(name, "4", "INCOME");
|
||||
}
|
||||
|
||||
public Account createExpenseAccount(String name) {
|
||||
return createAccount(name, "5", "EXPENSE");
|
||||
}
|
||||
|
||||
private Account createAccount(String name, String glCode, String type) {
|
||||
String uniqueName = Utils.uniqueRandomStringGenerator(name + "_", 4);
|
||||
String accountCode = Utils.uniqueRandomStringGenerator("GL_" + glCode, 6);
|
||||
|
||||
PostGLAccountsRequest request = new PostGLAccountsRequest()//
|
||||
.name(uniqueName)//
|
||||
.glCode(accountCode)//
|
||||
.manualEntriesAllowed(true)//
|
||||
.type(getAccountTypeId(type))//
|
||||
.usage(1);
|
||||
|
||||
PostGLAccountsResponse response = ok(() -> fineractClient.generalLedgerAccount().createGLAccount1(request));
|
||||
|
||||
GetGLAccountsResponse account = ok(
|
||||
() -> fineractClient.generalLedgerAccount().retreiveAccount(response.getResourceId(), Collections.emptyMap()));
|
||||
|
||||
return new Account(account.getId().intValue(), getAccountType(type));
|
||||
}
|
||||
|
||||
private Integer getAccountTypeId(String type) {
|
||||
return switch (type) {
|
||||
case "ASSET" -> 1;
|
||||
case "LIABILITY" -> 2;
|
||||
case "EQUITY" -> 3;
|
||||
case "INCOME" -> 4;
|
||||
case "EXPENSE" -> 5;
|
||||
default -> throw new IllegalArgumentException("Unknown account type: " + type);
|
||||
};
|
||||
}
|
||||
|
||||
private Account.AccountType getAccountType(String type) {
|
||||
return switch (type) {
|
||||
case "ASSET" -> Account.AccountType.ASSET;
|
||||
case "LIABILITY" -> Account.AccountType.LIABILITY;
|
||||
case "EQUITY" -> Account.AccountType.EQUITY;
|
||||
case "INCOME" -> Account.AccountType.INCOME;
|
||||
case "EXPENSE" -> Account.AccountType.EXPENSE;
|
||||
default -> throw new IllegalArgumentException("Unknown account type: " + type);
|
||||
};
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.helpers;
|
||||
|
||||
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.models.BusinessDateResponse;
|
||||
import org.apache.fineract.client.models.BusinessDateUpdateRequest;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
|
||||
|
||||
public class FeignBusinessDateHelper {
|
||||
|
||||
private final FineractFeignClient fineractClient;
|
||||
|
||||
public FeignBusinessDateHelper(FineractFeignClient fineractClient) {
|
||||
this.fineractClient = fineractClient;
|
||||
}
|
||||
|
||||
public BusinessDateResponse getBusinessDate(String type) {
|
||||
return ok(() -> fineractClient.businessDateManagement().getBusinessDate(type));
|
||||
}
|
||||
|
||||
public void updateBusinessDate(String type, String date) {
|
||||
BusinessDateUpdateRequest request = new BusinessDateUpdateRequest()//
|
||||
.type(BusinessDateUpdateRequest.TypeEnum.fromValue(type))//
|
||||
.date(date)//
|
||||
.dateFormat("yyyy-MM-dd")//
|
||||
.locale(LoanTestData.LOCALE);
|
||||
|
||||
ok(() -> fineractClient.businessDateManagement().updateBusinessDate(request, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
public void runAt(String date, Runnable action) {
|
||||
BusinessDateResponse originalDate = getBusinessDate("BUSINESS_DATE");
|
||||
try {
|
||||
updateBusinessDate("BUSINESS_DATE", date);
|
||||
action.run();
|
||||
} finally {
|
||||
if (originalDate != null && originalDate.getDate() != null) {
|
||||
updateBusinessDate("BUSINESS_DATE", originalDate.getDate().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.helpers;
|
||||
|
||||
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.models.GetClientsClientIdResponse;
|
||||
import org.apache.fineract.client.models.PostClientsRequest;
|
||||
import org.apache.fineract.client.models.PostClientsResponse;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
|
||||
public class FeignClientHelper {
|
||||
|
||||
private final FineractFeignClient fineractClient;
|
||||
|
||||
public FeignClientHelper(FineractFeignClient fineractClient) {
|
||||
this.fineractClient = fineractClient;
|
||||
}
|
||||
|
||||
public Long createClient(String firstName, String lastName) {
|
||||
String externalId = Utils.randomStringGenerator("EXT_", 7);
|
||||
String activationDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
|
||||
|
||||
PostClientsRequest request = new PostClientsRequest()//
|
||||
.officeId(1L)//
|
||||
.legalFormId(1L)//
|
||||
.firstname(firstName)//
|
||||
.lastname(lastName)//
|
||||
.externalId(externalId)//
|
||||
.active(true)//
|
||||
.activationDate(activationDate)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN)//
|
||||
.locale(LoanTestData.LOCALE);
|
||||
|
||||
return createClient(request);
|
||||
}
|
||||
|
||||
public Long createClient(PostClientsRequest request) {
|
||||
PostClientsResponse response = ok(() -> fineractClient.clients().create6(request));
|
||||
return response.getClientId();
|
||||
}
|
||||
|
||||
public GetClientsClientIdResponse getClient(Long clientId) {
|
||||
return ok(() -> fineractClient.clients().retrieveOne11(clientId, Collections.emptyMap()));
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.helpers;
|
||||
|
||||
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
||||
import org.apache.fineract.client.models.JournalEntryTransactionItem;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
|
||||
|
||||
public class FeignJournalEntryHelper {
|
||||
|
||||
private final FineractFeignClient fineractClient;
|
||||
|
||||
public FeignJournalEntryHelper(FineractFeignClient fineractClient) {
|
||||
this.fineractClient = fineractClient;
|
||||
}
|
||||
|
||||
public GetJournalEntriesTransactionIdResponse getJournalEntriesForLoan(Long loanId) {
|
||||
return ok(() -> fineractClient.journalEntries().retrieveAll1(Map.of("loanId", loanId)));
|
||||
}
|
||||
|
||||
public void verifyJournalEntries(Long loanId, LoanTestData.Journal... expectedEntries) {
|
||||
GetJournalEntriesTransactionIdResponse journalEntries = getJournalEntriesForLoan(loanId);
|
||||
assertNotNull(journalEntries);
|
||||
assertNotNull(journalEntries.getPageItems());
|
||||
|
||||
List<JournalEntryTransactionItem> actualEntries = journalEntries.getPageItems();
|
||||
assertEquals(expectedEntries.length, actualEntries.size(),
|
||||
"Expected " + expectedEntries.length + " journal entries but found " + actualEntries.size());
|
||||
|
||||
for (int i = 0; i < expectedEntries.length; i++) {
|
||||
LoanTestData.Journal expected = expectedEntries[i];
|
||||
JournalEntryTransactionItem actual = actualEntries.get(i);
|
||||
|
||||
Double expectedAmount = expected.amount;
|
||||
Double actualAmount = actual.getAmount();
|
||||
assertEquals(0, Double.compare(expectedAmount, actualAmount),
|
||||
"Journal entry " + i + " amount mismatch: expected " + expectedAmount + " but got " + actualAmount);
|
||||
assertEquals(expected.account.getAccountID().longValue(), actual.getGlAccountId(), "Journal entry " + i + " account mismatch");
|
||||
assertEquals(expected.type, actual.getEntryType().getValue(), "Journal entry " + i + " type mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyJournalEntriesSequentially(Long loanId, LoanTestData.Journal... expectedEntries) {
|
||||
verifyJournalEntries(loanId, expectedEntries);
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.helpers;
|
||||
|
||||
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostLoansRequest;
|
||||
import org.apache.fineract.client.models.PostLoansResponse;
|
||||
|
||||
public class FeignLoanHelper {
|
||||
|
||||
private final FineractFeignClient fineractClient;
|
||||
|
||||
public FeignLoanHelper(FineractFeignClient fineractClient) {
|
||||
this.fineractClient = fineractClient;
|
||||
}
|
||||
|
||||
public Long createLoanProduct(PostLoanProductsRequest request) {
|
||||
PostLoanProductsResponse response = ok(() -> fineractClient.loanProducts().createLoanProduct(request));
|
||||
return response.getResourceId();
|
||||
}
|
||||
|
||||
public Long applyForLoan(PostLoansRequest request) {
|
||||
PostLoansResponse response = ok(() -> fineractClient.loans().calculateLoanScheduleOrSubmitLoanApplication(request, null));
|
||||
return response.getLoanId();
|
||||
}
|
||||
|
||||
public Long approveLoan(Long loanId, PostLoansLoanIdRequest request) {
|
||||
PostLoansLoanIdResponse response = ok(() -> fineractClient.loans().stateTransitions(loanId, request, Map.of("command", "approve")));
|
||||
return response.getLoanId();
|
||||
}
|
||||
|
||||
public Long disburseLoan(Long loanId, PostLoansLoanIdRequest request) {
|
||||
PostLoansLoanIdResponse response = ok(
|
||||
() -> fineractClient.loans().stateTransitions(loanId, request, Map.of("command", "disburse")));
|
||||
return response.getLoanId();
|
||||
}
|
||||
|
||||
public GetLoansLoanIdResponse getLoanDetails(Long loanId) {
|
||||
return ok(() -> fineractClient.loans().retrieveLoan(loanId, Map.of("associations", "all", "exclude", "guarantors,futureSchedule")));
|
||||
}
|
||||
|
||||
public void undoApproval(Long loanId) {
|
||||
PostLoansLoanIdRequest request = new PostLoansLoanIdRequest();
|
||||
ok(() -> fineractClient.loans().stateTransitions(loanId, request, Map.of("command", "undoApproval")));
|
||||
}
|
||||
|
||||
public void undoDisbursement(Long loanId) {
|
||||
PostLoansLoanIdRequest request = new PostLoansLoanIdRequest();
|
||||
ok(() -> fineractClient.loans().stateTransitions(loanId, request, Map.of("command", "undoDisbursal")));
|
||||
}
|
||||
|
||||
public Long applyAndApproveLoan(Long clientId, Long productId, String submittedOnDate, Double principal, Integer numberOfRepayments) {
|
||||
PostLoansRequest applyRequest = new PostLoansRequest()//
|
||||
.clientId(clientId)//
|
||||
.productId(productId)//
|
||||
.loanType("individual")//
|
||||
.submittedOnDate(submittedOnDate)//
|
||||
.expectedDisbursementDate(submittedOnDate)//
|
||||
.principal(BigDecimal.valueOf(principal))//
|
||||
.loanTermFrequency(numberOfRepayments)//
|
||||
.loanTermFrequencyType(2)//
|
||||
.numberOfRepayments(numberOfRepayments)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2)//
|
||||
.interestRatePerPeriod(BigDecimal.ZERO)//
|
||||
.amortizationType(1)//
|
||||
.interestType(0)//
|
||||
.interestCalculationPeriodType(1)//
|
||||
.transactionProcessingStrategyCode("mifos-standard-strategy")//
|
||||
.locale("en")//
|
||||
.dateFormat("dd MMMM yyyy");
|
||||
|
||||
Long loanId = applyForLoan(applyRequest);
|
||||
|
||||
PostLoansLoanIdRequest approveRequest = new PostLoansLoanIdRequest()//
|
||||
.approvedLoanAmount(BigDecimal.valueOf(principal))//
|
||||
.approvedOnDate(submittedOnDate)//
|
||||
.locale("en")//
|
||||
.dateFormat("dd MMMM yyyy");
|
||||
|
||||
approveLoan(loanId, approveRequest);
|
||||
return loanId;
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.helpers;
|
||||
|
||||
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
|
||||
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
|
||||
|
||||
public class FeignTransactionHelper {
|
||||
|
||||
private final FineractFeignClient fineractClient;
|
||||
|
||||
public FeignTransactionHelper(FineractFeignClient fineractClient) {
|
||||
this.fineractClient = fineractClient;
|
||||
}
|
||||
|
||||
public Long addRepayment(Long loanId, PostLoansLoanIdTransactionsRequest request) {
|
||||
PostLoansLoanIdTransactionsResponse response = ok(
|
||||
() -> fineractClient.loanTransactions().executeLoanTransaction(loanId, request, Map.of("command", "repayment")));
|
||||
return response.getResourceId();
|
||||
}
|
||||
|
||||
public Long addInterestWaiver(Long loanId, PostLoansLoanIdTransactionsRequest request) {
|
||||
PostLoansLoanIdTransactionsResponse response = ok(
|
||||
() -> fineractClient.loanTransactions().executeLoanTransaction(loanId, request, Map.of("command", "waiveInterest")));
|
||||
return response.getResourceId();
|
||||
}
|
||||
|
||||
public Long chargeOff(Long loanId, PostLoansLoanIdTransactionsRequest request) {
|
||||
PostLoansLoanIdTransactionsResponse response = ok(
|
||||
() -> fineractClient.loanTransactions().executeLoanTransaction(loanId, request, Map.of("command", "chargeOff")));
|
||||
return response.getResourceId();
|
||||
}
|
||||
|
||||
public Long addChargeback(Long loanId, Long transactionId, PostLoansLoanIdTransactionsRequest request) {
|
||||
PostLoansLoanIdTransactionsTransactionIdRequest chargebackRequest = new PostLoansLoanIdTransactionsTransactionIdRequest()//
|
||||
.transactionDate(request.getTransactionDate())//
|
||||
.transactionAmount(request.getTransactionAmount())//
|
||||
.locale(request.getLocale())//
|
||||
.dateFormat(request.getDateFormat());
|
||||
|
||||
PostLoansLoanIdTransactionsResponse response = ok(() -> fineractClient.loanTransactions().adjustLoanTransaction(loanId,
|
||||
transactionId, chargebackRequest, Map.of("command", "chargeback")));
|
||||
return response.getResourceId();
|
||||
}
|
||||
|
||||
public void undoRepayment(Long loanId, Long transactionId, String transactionDate) {
|
||||
PostLoansLoanIdTransactionsTransactionIdRequest request = new PostLoansLoanIdTransactionsTransactionIdRequest();
|
||||
request.setTransactionDate(transactionDate);
|
||||
request.setTransactionAmount(0.0);
|
||||
request.setDateFormat("dd MMMM yyyy");
|
||||
request.setLocale("en");
|
||||
ok(() -> fineractClient.loanTransactions().adjustLoanTransaction(loanId, transactionId, request, Map.of("command", "undo")));
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.modules;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import org.apache.fineract.client.models.AllowAttributeOverrides;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.AmortizationType;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.DaysInMonthType;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.DaysInYearType;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.InterestCalculationPeriodType;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.InterestRateFrequencyType;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.InterestType;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.RepaymentFrequencyType;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.RescheduleStrategyMethod;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
|
||||
public interface LoanProductTemplates {
|
||||
|
||||
Long getAssetAccountId(String accountName);
|
||||
|
||||
Long getLiabilityAccountId(String accountName);
|
||||
|
||||
Long getIncomeAccountId(String accountName);
|
||||
|
||||
Long getExpenseAccountId(String accountName);
|
||||
|
||||
default PostLoanProductsRequest onePeriod30DaysNoInterest() {
|
||||
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
|
||||
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
|
||||
.description("Loan Product Description")//
|
||||
.includeInBorrowerCycle(false)//
|
||||
.currencyCode("USD")//
|
||||
.digitsAfterDecimal(2)//
|
||||
.inMultiplesOf(0)//
|
||||
.installmentAmountInMultiplesOf(1)//
|
||||
.useBorrowerCycle(false)//
|
||||
.minPrincipal(100.0)//
|
||||
.principal(1000.0)//
|
||||
.maxPrincipal(100000.0)//
|
||||
.minNumberOfRepayments(1)//
|
||||
.numberOfRepayments(1)//
|
||||
.maxNumberOfRepayments(30)//
|
||||
.isLinkedToFloatingInterestRates(false)//
|
||||
.minInterestRatePerPeriod(0.0)//
|
||||
.interestRatePerPeriod(0.0)//
|
||||
.maxInterestRatePerPeriod(100.0)//
|
||||
.interestRateFrequencyType(InterestRateFrequencyType.MONTHS)//
|
||||
.repaymentEvery(30)//
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.DAYS_L)//
|
||||
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS)//
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.isEqualAmortization(false)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)//
|
||||
.transactionProcessingStrategyCode(
|
||||
LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY)//
|
||||
.loanScheduleType(LoanScheduleType.CUMULATIVE.toString())//
|
||||
.daysInYearType(DaysInYearType.ACTUAL)//
|
||||
.daysInMonthType(DaysInMonthType.ACTUAL)//
|
||||
.canDefineInstallmentAmount(true)//
|
||||
.graceOnArrearsAgeing(3)//
|
||||
.overdueDaysForNPA(179)//
|
||||
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
|
||||
.principalThresholdForLastInstallment(50)//
|
||||
.allowVariableInstallments(false)//
|
||||
.canUseForTopup(false)//
|
||||
.isInterestRecalculationEnabled(false)//
|
||||
.holdGuaranteeFunds(false)//
|
||||
.multiDisburseLoan(true)//
|
||||
.allowAttributeOverrides(new AllowAttributeOverrides()//
|
||||
.amortizationType(true)//
|
||||
.interestType(true)//
|
||||
.transactionProcessingStrategyCode(true)//
|
||||
.interestCalculationPeriodType(true)//
|
||||
.inArrearsTolerance(true)//
|
||||
.repaymentEvery(true)//
|
||||
.graceOnPrincipalAndInterestPayment(true)//
|
||||
.graceOnArrearsAgeing(true))//
|
||||
.allowPartialPeriodInterestCalcualtion(true)//
|
||||
.maxTrancheCount(10)//
|
||||
.outstandingLoanBalance(10000.0)//
|
||||
.charges(Collections.emptyList())//
|
||||
.accountingRule(3)//
|
||||
.fundSourceAccountId(getLiabilityAccountId("fundSource"))//
|
||||
.loanPortfolioAccountId(getAssetAccountId("loansReceivable"))//
|
||||
.transfersInSuspenseAccountId(getAssetAccountId("suspense"))//
|
||||
.interestOnLoanAccountId(getIncomeAccountId("interestIncome"))//
|
||||
.incomeFromFeeAccountId(getIncomeAccountId("feeIncome"))//
|
||||
.incomeFromPenaltyAccountId(getIncomeAccountId("penaltyIncome"))//
|
||||
.incomeFromRecoveryAccountId(getIncomeAccountId("recoveries"))//
|
||||
.writeOffAccountId(getExpenseAccountId("writtenOff"))//
|
||||
.overpaymentLiabilityAccountId(getLiabilityAccountId("overpayment"))//
|
||||
.receivableInterestAccountId(getAssetAccountId("interestReceivable"))//
|
||||
.receivableFeeAccountId(getAssetAccountId("feeReceivable"))//
|
||||
.receivablePenaltyAccountId(getAssetAccountId("penaltyReceivable"))//
|
||||
.goodwillCreditAccountId(getExpenseAccountId("goodwillExpense"))//
|
||||
.incomeFromGoodwillCreditInterestAccountId(getIncomeAccountId("interestIncomeChargeOff"))//
|
||||
.incomeFromGoodwillCreditFeesAccountId(getIncomeAccountId("feeChargeOff"))//
|
||||
.incomeFromGoodwillCreditPenaltyAccountId(getIncomeAccountId("feeChargeOff"))//
|
||||
.incomeFromChargeOffInterestAccountId(getIncomeAccountId("interestIncomeChargeOff"))//
|
||||
.incomeFromChargeOffFeesAccountId(getIncomeAccountId("feeChargeOff"))//
|
||||
.incomeFromChargeOffPenaltyAccountId(getIncomeAccountId("penaltyChargeOff"))//
|
||||
.chargeOffExpenseAccountId(getExpenseAccountId("chargeOff"))//
|
||||
.chargeOffFraudExpenseAccountId(getExpenseAccountId("chargeOffFraud"))//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN)//
|
||||
.locale("en_GB")//
|
||||
.disallowExpectedDisbursements(true)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||
.overAppliedCalculationType("percentage")//
|
||||
.overAppliedNumber(50);
|
||||
}
|
||||
|
||||
default PostLoanProductsRequest fourInstallmentsCumulative() {
|
||||
return fourInstallmentsCumulativeTemplate().loanScheduleType(LoanScheduleType.CUMULATIVE.toString());
|
||||
}
|
||||
|
||||
default PostLoanProductsRequest fourInstallmentsProgressive() {
|
||||
return fourInstallmentsCumulativeTemplate().loanScheduleType("PROGRESSIVE").loanScheduleProcessingType("HORIZONTAL")
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD);
|
||||
}
|
||||
|
||||
default PostLoanProductsRequest fourInstallmentsProgressiveWithCapitalizedIncome() {
|
||||
return fourInstallmentsProgressive().enableIncomeCapitalization(true)//
|
||||
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)//
|
||||
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)//
|
||||
.deferredIncomeLiabilityAccountId(getLiabilityAccountId("deferredIncomeLiability"))//
|
||||
.incomeFromCapitalizationAccountId(getIncomeAccountId("feeIncome"))//
|
||||
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE);
|
||||
}
|
||||
|
||||
default PostLoanProductsRequest fourInstallmentsCumulativeTemplate() {
|
||||
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("4I_PRODUCT_", 6))//
|
||||
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
|
||||
.description("4 installment product")//
|
||||
.includeInBorrowerCycle(false)//
|
||||
.useBorrowerCycle(false)//
|
||||
.currencyCode("EUR")//
|
||||
.digitsAfterDecimal(2)//
|
||||
.principal(1000.0)//
|
||||
.minPrincipal(100.0)//
|
||||
.maxPrincipal(10000.0)//
|
||||
.numberOfRepayments(4)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L)//
|
||||
.interestRatePerPeriod(10.0)//
|
||||
.minInterestRatePerPeriod(0.0)//
|
||||
.maxInterestRatePerPeriod(120.0)//
|
||||
.interestRateFrequencyType(InterestRateFrequencyType.YEARS)//
|
||||
.isLinkedToFloatingInterestRates(false)//
|
||||
.allowVariableInstallments(false)//
|
||||
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS)//
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.creditAllocation(List.of())//
|
||||
.overdueDaysForNPA(179)//
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30)//
|
||||
.daysInYearType(DaysInYearType.DAYS_360)//
|
||||
.isInterestRecalculationEnabled(false)//
|
||||
.canDefineInstallmentAmount(true)//
|
||||
.repaymentStartDateType(1)//
|
||||
.charges(List.of())//
|
||||
.principalVariationsForBorrowerCycle(List.of())//
|
||||
.interestRateVariationsForBorrowerCycle(List.of())//
|
||||
.numberOfRepaymentVariationsForBorrowerCycle(List.of())//
|
||||
.accountingRule(3)//
|
||||
.canUseForTopup(false)//
|
||||
.fundSourceAccountId(getLiabilityAccountId("fundSource"))//
|
||||
.loanPortfolioAccountId(getAssetAccountId("loansReceivable"))//
|
||||
.transfersInSuspenseAccountId(getAssetAccountId("suspense"))//
|
||||
.interestOnLoanAccountId(getIncomeAccountId("interestIncome"))//
|
||||
.incomeFromFeeAccountId(getIncomeAccountId("feeIncome"))//
|
||||
.incomeFromPenaltyAccountId(getIncomeAccountId("penaltyIncome"))//
|
||||
.incomeFromRecoveryAccountId(getIncomeAccountId("recoveries"))//
|
||||
.writeOffAccountId(getExpenseAccountId("writtenOff"))//
|
||||
.overpaymentLiabilityAccountId(getLiabilityAccountId("overpayment"))//
|
||||
.receivableInterestAccountId(getAssetAccountId("interestReceivable"))//
|
||||
.receivableFeeAccountId(getAssetAccountId("feeReceivable"))//
|
||||
.receivablePenaltyAccountId(getAssetAccountId("penaltyReceivable"))//
|
||||
.goodwillCreditAccountId(getExpenseAccountId("goodwillExpense"))//
|
||||
.incomeFromGoodwillCreditInterestAccountId(getIncomeAccountId("interestIncomeChargeOff"))//
|
||||
.incomeFromGoodwillCreditFeesAccountId(getIncomeAccountId("feeChargeOff"))//
|
||||
.incomeFromGoodwillCreditPenaltyAccountId(getIncomeAccountId("feeChargeOff"))//
|
||||
.incomeFromChargeOffInterestAccountId(getIncomeAccountId("interestIncomeChargeOff"))//
|
||||
.incomeFromChargeOffFeesAccountId(getIncomeAccountId("feeChargeOff"))//
|
||||
.incomeFromChargeOffPenaltyAccountId(getIncomeAccountId("penaltyChargeOff"))//
|
||||
.chargeOffExpenseAccountId(getExpenseAccountId("chargeOff"))//
|
||||
.chargeOffFraudExpenseAccountId(getExpenseAccountId("chargeOffFraud"))//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN)//
|
||||
.locale("en")//
|
||||
.enableAccrualActivityPosting(false)//
|
||||
.multiDisburseLoan(false)//
|
||||
.disallowExpectedDisbursements(false)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||
.overAppliedCalculationType("percentage")//
|
||||
.overAppliedNumber(50)//
|
||||
.principalThresholdForLastInstallment(50)//
|
||||
.holdGuaranteeFunds(false)//
|
||||
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
|
||||
.allowAttributeOverrides(new AllowAttributeOverrides()//
|
||||
.amortizationType(true)//
|
||||
.interestType(true)//
|
||||
.transactionProcessingStrategyCode(true)//
|
||||
.interestCalculationPeriodType(true)//
|
||||
.inArrearsTolerance(true)//
|
||||
.repaymentEvery(true)//
|
||||
.graceOnPrincipalAndInterestPayment(true)//
|
||||
.graceOnArrearsAgeing(true))//
|
||||
.isEqualAmortization(false)//
|
||||
.enableDownPayment(false)//
|
||||
.enableInstallmentLevelDelinquency(false)//
|
||||
.transactionProcessingStrategyCode(
|
||||
LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY);
|
||||
}
|
||||
|
||||
default PostLoanProductsRequest customizeProduct(PostLoanProductsRequest template,
|
||||
Function<PostLoanProductsRequest, PostLoanProductsRequest> customizer) {
|
||||
return customizer.apply(template);
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.modules;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||
import org.apache.fineract.client.models.PostLoansRequest;
|
||||
|
||||
public final class LoanRequestBuilders {
|
||||
|
||||
private LoanRequestBuilders() {}
|
||||
|
||||
public static PostLoansRequest applyLoan(Long clientId, Long productId, String submittedOnDate, Double principal,
|
||||
Integer numberOfRepayments) {
|
||||
return new PostLoansRequest()//
|
||||
.clientId(clientId)//
|
||||
.productId(productId)//
|
||||
.loanType("individual")//
|
||||
.submittedOnDate(submittedOnDate)//
|
||||
.expectedDisbursementDate(submittedOnDate)//
|
||||
.principal(BigDecimal.valueOf(principal))//
|
||||
.loanTermFrequency(numberOfRepayments)//
|
||||
.loanTermFrequencyType(LoanTestData.RepaymentFrequencyType.MONTHS)//
|
||||
.numberOfRepayments(numberOfRepayments)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(LoanTestData.RepaymentFrequencyType.MONTHS)//
|
||||
.interestRatePerPeriod(BigDecimal.ZERO)//
|
||||
.amortizationType(LoanTestData.AmortizationType.EQUAL_INSTALLMENTS)//
|
||||
.interestType(LoanTestData.InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(LoanTestData.InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)//
|
||||
.transactionProcessingStrategyCode("mifos-standard-strategy")//
|
||||
.locale(LoanTestData.LOCALE)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
public static PostLoansRequest applyCumulativeLoan(Long clientId, Long productId, String submittedOnDate, Double principal,
|
||||
Integer numberOfRepayments, Double interestRate) {
|
||||
return new PostLoansRequest()//
|
||||
.clientId(clientId)//
|
||||
.productId(productId)//
|
||||
.submittedOnDate(submittedOnDate)//
|
||||
.expectedDisbursementDate(submittedOnDate)//
|
||||
.principal(BigDecimal.valueOf(principal))//
|
||||
.loanTermFrequency(numberOfRepayments)//
|
||||
.loanTermFrequencyType(LoanTestData.RepaymentFrequencyType.MONTHS)//
|
||||
.numberOfRepayments(numberOfRepayments)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(LoanTestData.RepaymentFrequencyType.MONTHS)//
|
||||
.interestRatePerPeriod(BigDecimal.valueOf(interestRate))//
|
||||
.amortizationType(LoanTestData.AmortizationType.EQUAL_INSTALLMENTS)//
|
||||
.interestType(LoanTestData.InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(LoanTestData.InterestCalculationPeriodType.DAILY)//
|
||||
.transactionProcessingStrategyCode("DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY")//
|
||||
.loanType("individual")//
|
||||
.locale(LoanTestData.LOCALE)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
public static PostLoansRequest applyProgressiveLoan(Long clientId, Long productId, String submittedOnDate, Double principal,
|
||||
Integer numberOfRepayments, Double interestRate) {
|
||||
return applyCumulativeLoan(clientId, productId, submittedOnDate, principal, numberOfRepayments, interestRate);
|
||||
}
|
||||
|
||||
public static PostLoansLoanIdRequest approveLoan(Double approvedAmount, String approvedOnDate) {
|
||||
return new PostLoansLoanIdRequest()//
|
||||
.approvedLoanAmount(BigDecimal.valueOf(approvedAmount))//
|
||||
.approvedOnDate(approvedOnDate)//
|
||||
.locale(LoanTestData.LOCALE)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
public static PostLoansLoanIdRequest disburseLoan(Double disbursedAmount, String disbursedOnDate) {
|
||||
return new PostLoansLoanIdRequest()//
|
||||
.actualDisbursementDate(disbursedOnDate)//
|
||||
.transactionAmount(BigDecimal.valueOf(disbursedAmount))//
|
||||
.locale(LoanTestData.LOCALE)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
public static PostLoansLoanIdTransactionsRequest repayLoan(Double amount, String transactionDate) {
|
||||
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
|
||||
request.setTransactionDate(transactionDate);
|
||||
request.setTransactionAmount(amount);
|
||||
request.setLocale(LoanTestData.LOCALE);
|
||||
request.setDateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
return request;
|
||||
}
|
||||
|
||||
public static PostLoansLoanIdTransactionsRequest makeWaiver(Double amount, String transactionDate) {
|
||||
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
|
||||
request.setTransactionDate(transactionDate);
|
||||
request.setTransactionAmount(amount);
|
||||
request.setLocale(LoanTestData.LOCALE);
|
||||
request.setDateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
return request;
|
||||
}
|
||||
|
||||
public static PostLoansLoanIdTransactionsRequest chargeOff(String transactionDate) {
|
||||
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
|
||||
request.setTransactionDate(transactionDate);
|
||||
request.setLocale(LoanTestData.LOCALE);
|
||||
request.setDateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
return request;
|
||||
}
|
||||
|
||||
public static PostLoansLoanIdTransactionsRequest addChargeback(Long transactionId, Double amount, String transactionDate) {
|
||||
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
|
||||
request.setTransactionDate(transactionDate);
|
||||
request.setTransactionAmount(amount);
|
||||
request.setLocale(LoanTestData.LOCALE);
|
||||
request.setDateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
return request;
|
||||
}
|
||||
|
||||
public static PostLoansLoanIdTransactionsRequest waiveInterest(Double amount, String transactionDate) {
|
||||
return makeWaiver(amount, transactionDate);
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.modules;
|
||||
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignAccountHelper;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
|
||||
public class LoanTestAccounts implements LoanProductTemplates {
|
||||
|
||||
private final Account loansReceivableAccount;
|
||||
private final Account interestReceivableAccount;
|
||||
private final Account feeReceivableAccount;
|
||||
private final Account penaltyReceivableAccount;
|
||||
private final Account suspenseAccount;
|
||||
private final Account fundSource;
|
||||
private final Account overpaymentAccount;
|
||||
private final Account interestIncomeAccount;
|
||||
private final Account feeIncomeAccount;
|
||||
private final Account penaltyIncomeAccount;
|
||||
private final Account feeChargeOffAccount;
|
||||
private final Account penaltyChargeOffAccount;
|
||||
private final Account recoveriesAccount;
|
||||
private final Account interestIncomeChargeOffAccount;
|
||||
private final Account chargeOffExpenseAccount;
|
||||
private final Account chargeOffFraudExpenseAccount;
|
||||
private final Account writtenOffAccount;
|
||||
private final Account goodwillExpenseAccount;
|
||||
private final Account goodwillIncomeAccount;
|
||||
private final Account deferredIncomeLiabilityAccount;
|
||||
private final Account buyDownExpenseAccount;
|
||||
|
||||
public LoanTestAccounts(FeignAccountHelper accountHelper) {
|
||||
this.loansReceivableAccount = accountHelper.createAssetAccount("loanPortfolio");
|
||||
this.interestReceivableAccount = accountHelper.createAssetAccount("interestReceivable");
|
||||
this.feeReceivableAccount = accountHelper.createAssetAccount("feeReceivable");
|
||||
this.penaltyReceivableAccount = accountHelper.createAssetAccount("penaltyReceivable");
|
||||
this.suspenseAccount = accountHelper.createAssetAccount("suspense");
|
||||
this.fundSource = accountHelper.createLiabilityAccount("fundSource");
|
||||
this.overpaymentAccount = accountHelper.createLiabilityAccount("overpayment");
|
||||
this.interestIncomeAccount = accountHelper.createIncomeAccount("interestIncome");
|
||||
this.feeIncomeAccount = accountHelper.createIncomeAccount("feeIncome");
|
||||
this.penaltyIncomeAccount = accountHelper.createIncomeAccount("penaltyIncome");
|
||||
this.feeChargeOffAccount = accountHelper.createIncomeAccount("feeChargeOff");
|
||||
this.penaltyChargeOffAccount = accountHelper.createIncomeAccount("penaltyChargeOff");
|
||||
this.recoveriesAccount = accountHelper.createIncomeAccount("recoveries");
|
||||
this.interestIncomeChargeOffAccount = accountHelper.createIncomeAccount("interestIncomeChargeOff");
|
||||
this.chargeOffExpenseAccount = accountHelper.createExpenseAccount("chargeOff");
|
||||
this.chargeOffFraudExpenseAccount = accountHelper.createExpenseAccount("chargeOffFraud");
|
||||
this.writtenOffAccount = accountHelper.createExpenseAccount("writtenOffAccount");
|
||||
this.goodwillExpenseAccount = accountHelper.createExpenseAccount("goodwillExpenseAccount");
|
||||
this.goodwillIncomeAccount = accountHelper.createIncomeAccount("goodwillIncomeAccount");
|
||||
this.deferredIncomeLiabilityAccount = accountHelper.createLiabilityAccount("deferredIncomeLiabilityAccount");
|
||||
this.buyDownExpenseAccount = accountHelper.createExpenseAccount("buyDownExpenseAccount");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getAssetAccountId(String accountName) {
|
||||
return switch (accountName) {
|
||||
case "loansReceivable" -> loansReceivableAccount.getAccountID().longValue();
|
||||
case "interestReceivable" -> interestReceivableAccount.getAccountID().longValue();
|
||||
case "feeReceivable" -> feeReceivableAccount.getAccountID().longValue();
|
||||
case "penaltyReceivable" -> penaltyReceivableAccount.getAccountID().longValue();
|
||||
case "suspense" -> suspenseAccount.getAccountID().longValue();
|
||||
default -> throw new IllegalArgumentException("Unknown asset account: " + accountName);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getLiabilityAccountId(String accountName) {
|
||||
return switch (accountName) {
|
||||
case "fundSource" -> fundSource.getAccountID().longValue();
|
||||
case "overpayment" -> overpaymentAccount.getAccountID().longValue();
|
||||
case "deferredIncomeLiability" -> deferredIncomeLiabilityAccount.getAccountID().longValue();
|
||||
default -> throw new IllegalArgumentException("Unknown liability account: " + accountName);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getIncomeAccountId(String accountName) {
|
||||
return switch (accountName) {
|
||||
case "interestIncome" -> interestIncomeAccount.getAccountID().longValue();
|
||||
case "feeIncome" -> feeIncomeAccount.getAccountID().longValue();
|
||||
case "penaltyIncome" -> penaltyIncomeAccount.getAccountID().longValue();
|
||||
case "feeChargeOff" -> feeChargeOffAccount.getAccountID().longValue();
|
||||
case "penaltyChargeOff" -> penaltyChargeOffAccount.getAccountID().longValue();
|
||||
case "recoveries" -> recoveriesAccount.getAccountID().longValue();
|
||||
case "interestIncomeChargeOff" -> interestIncomeChargeOffAccount.getAccountID().longValue();
|
||||
case "goodwillIncome" -> goodwillIncomeAccount.getAccountID().longValue();
|
||||
default -> throw new IllegalArgumentException("Unknown income account: " + accountName);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getExpenseAccountId(String accountName) {
|
||||
return switch (accountName) {
|
||||
case "chargeOff" -> chargeOffExpenseAccount.getAccountID().longValue();
|
||||
case "chargeOffFraud" -> chargeOffFraudExpenseAccount.getAccountID().longValue();
|
||||
case "writtenOff" -> writtenOffAccount.getAccountID().longValue();
|
||||
case "goodwillExpense" -> goodwillExpenseAccount.getAccountID().longValue();
|
||||
case "buyDownExpense" -> buyDownExpenseAccount.getAccountID().longValue();
|
||||
default -> throw new IllegalArgumentException("Unknown expense account: " + accountName);
|
||||
};
|
||||
}
|
||||
|
||||
public Account getLoansReceivableAccount() {
|
||||
return loansReceivableAccount;
|
||||
}
|
||||
|
||||
public Account getInterestReceivableAccount() {
|
||||
return interestReceivableAccount;
|
||||
}
|
||||
|
||||
public Account getFeeReceivableAccount() {
|
||||
return feeReceivableAccount;
|
||||
}
|
||||
|
||||
public Account getPenaltyReceivableAccount() {
|
||||
return penaltyReceivableAccount;
|
||||
}
|
||||
|
||||
public Account getSuspenseAccount() {
|
||||
return suspenseAccount;
|
||||
}
|
||||
|
||||
public Account getFundSource() {
|
||||
return fundSource;
|
||||
}
|
||||
|
||||
public Account getOverpaymentAccount() {
|
||||
return overpaymentAccount;
|
||||
}
|
||||
|
||||
public Account getInterestIncomeAccount() {
|
||||
return interestIncomeAccount;
|
||||
}
|
||||
|
||||
public Account getFeeIncomeAccount() {
|
||||
return feeIncomeAccount;
|
||||
}
|
||||
|
||||
public Account getPenaltyIncomeAccount() {
|
||||
return penaltyIncomeAccount;
|
||||
}
|
||||
|
||||
public Account getFeeChargeOffAccount() {
|
||||
return feeChargeOffAccount;
|
||||
}
|
||||
|
||||
public Account getPenaltyChargeOffAccount() {
|
||||
return penaltyChargeOffAccount;
|
||||
}
|
||||
|
||||
public Account getRecoveriesAccount() {
|
||||
return recoveriesAccount;
|
||||
}
|
||||
|
||||
public Account getInterestIncomeChargeOffAccount() {
|
||||
return interestIncomeChargeOffAccount;
|
||||
}
|
||||
|
||||
public Account getChargeOffExpenseAccount() {
|
||||
return chargeOffExpenseAccount;
|
||||
}
|
||||
|
||||
public Account getChargeOffFraudExpenseAccount() {
|
||||
return chargeOffFraudExpenseAccount;
|
||||
}
|
||||
|
||||
public Account getWrittenOffAccount() {
|
||||
return writtenOffAccount;
|
||||
}
|
||||
|
||||
public Account getGoodwillExpenseAccount() {
|
||||
return goodwillExpenseAccount;
|
||||
}
|
||||
|
||||
public Account getGoodwillIncomeAccount() {
|
||||
return goodwillIncomeAccount;
|
||||
}
|
||||
|
||||
public Account getDeferredIncomeLiabilityAccount() {
|
||||
return deferredIncomeLiabilityAccount;
|
||||
}
|
||||
|
||||
public Account getBuyDownExpenseAccount() {
|
||||
return buyDownExpenseAccount;
|
||||
}
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.modules;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
|
||||
public final class LoanTestData {
|
||||
|
||||
public static final String DATETIME_PATTERN = "dd MMMM yyyy";
|
||||
public static final String LOCALE = "en";
|
||||
|
||||
private LoanTestData() {}
|
||||
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
public static class Transaction {
|
||||
|
||||
public Double amount;
|
||||
public String type;
|
||||
public String date;
|
||||
public Boolean reversed;
|
||||
}
|
||||
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
public static class TransactionExt {
|
||||
|
||||
public Double amount;
|
||||
public String type;
|
||||
public String date;
|
||||
public Double outstandingPrincipal;
|
||||
public Double principalPortion;
|
||||
public Double interestPortion;
|
||||
public Double feePortion;
|
||||
public Double penaltyPortion;
|
||||
public Double unrecognizedPortion;
|
||||
public Double overpaymentPortion;
|
||||
public Boolean reversed;
|
||||
}
|
||||
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Journal {
|
||||
|
||||
public Double amount;
|
||||
public Account account;
|
||||
public String type;
|
||||
|
||||
public static Journal debit(Long glAccountId, double amount) {
|
||||
Journal journal = new Journal();
|
||||
journal.amount = amount;
|
||||
journal.account = new Account(glAccountId.intValue(), null);
|
||||
journal.type = "DEBIT";
|
||||
return journal;
|
||||
}
|
||||
|
||||
public static Journal credit(Long glAccountId, double amount) {
|
||||
Journal journal = new Journal();
|
||||
journal.amount = amount;
|
||||
journal.account = new Account(glAccountId.intValue(), null);
|
||||
journal.type = "CREDIT";
|
||||
return journal;
|
||||
}
|
||||
}
|
||||
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
public static class Installment {
|
||||
|
||||
public Double principalAmount;
|
||||
public Double interestAmount;
|
||||
public Double feeAmount;
|
||||
public Double penaltyAmount;
|
||||
public Double totalOutstandingAmount;
|
||||
public Boolean completed;
|
||||
public String dueDate;
|
||||
public OutstandingAmounts outstandingAmounts;
|
||||
public Double loanBalance;
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public static class OutstandingAmounts {
|
||||
|
||||
public Double principalOutstanding;
|
||||
public Double interestOutstanding;
|
||||
public Double feeOutstanding;
|
||||
public Double penaltyOutstanding;
|
||||
public Double totalOutstanding;
|
||||
}
|
||||
|
||||
public static final class AmortizationType {
|
||||
|
||||
public static final Integer EQUAL_INSTALLMENTS = 1;
|
||||
|
||||
private AmortizationType() {}
|
||||
}
|
||||
|
||||
public static final class InterestType {
|
||||
|
||||
public static final Integer DECLINING_BALANCE = 0;
|
||||
public static final Integer FLAT = 1;
|
||||
|
||||
private InterestType() {}
|
||||
}
|
||||
|
||||
public static final class InterestRecalculationCompoundingMethod {
|
||||
|
||||
public static final Integer NONE = 0;
|
||||
|
||||
private InterestRecalculationCompoundingMethod() {}
|
||||
}
|
||||
|
||||
public static final class RepaymentFrequencyType {
|
||||
|
||||
public static final Integer MONTHS = 2;
|
||||
public static final Long MONTHS_L = 2L;
|
||||
public static final String MONTHS_STRING = "MONTHS";
|
||||
public static final Integer DAYS = 0;
|
||||
public static final Long DAYS_L = 0L;
|
||||
public static final String DAYS_STRING = "DAYS";
|
||||
|
||||
private RepaymentFrequencyType() {}
|
||||
}
|
||||
|
||||
public static final class RecalculationRestFrequencyType {
|
||||
|
||||
public static final Integer SAME_AS_REPAYMENT_PERIOD = 1;
|
||||
public static final Integer DAILY = 2;
|
||||
|
||||
private RecalculationRestFrequencyType() {}
|
||||
}
|
||||
|
||||
public static final class InterestCalculationPeriodType {
|
||||
|
||||
public static final Integer DAILY = 0;
|
||||
public static final Integer SAME_AS_REPAYMENT_PERIOD = 1;
|
||||
|
||||
private InterestCalculationPeriodType() {}
|
||||
}
|
||||
|
||||
public static final class InterestRateFrequencyType {
|
||||
|
||||
public static final Integer MONTHS = 2;
|
||||
public static final Integer YEARS = 3;
|
||||
public static final Integer WHOLE_TERM = 4;
|
||||
|
||||
private InterestRateFrequencyType() {}
|
||||
}
|
||||
|
||||
public static final class TransactionProcessingStrategyCode {
|
||||
|
||||
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
|
||||
|
||||
private TransactionProcessingStrategyCode() {}
|
||||
}
|
||||
|
||||
public static final class RescheduleStrategyMethod {
|
||||
|
||||
public static final Integer RESCHEDULE_NEXT_REPAYMENTS = 1;
|
||||
public static final Integer REDUCE_EMI_AMOUNT = 3;
|
||||
public static final Integer ADJUST_LAST_UNPAID_PERIOD = 4;
|
||||
|
||||
private RescheduleStrategyMethod() {}
|
||||
}
|
||||
|
||||
public static final class DaysInYearType {
|
||||
|
||||
public static final Integer INVALID = 0;
|
||||
public static final Integer ACTUAL = 1;
|
||||
public static final Integer DAYS_360 = 360;
|
||||
public static final Integer DAYS_364 = 364;
|
||||
public static final Integer DAYS_365 = 365;
|
||||
|
||||
private DaysInYearType() {}
|
||||
}
|
||||
|
||||
public static final class DaysInMonthType {
|
||||
|
||||
public static final Integer INVALID = 0;
|
||||
public static final Integer ACTUAL = 1;
|
||||
public static final Integer DAYS_30 = 30;
|
||||
|
||||
private DaysInMonthType() {}
|
||||
}
|
||||
|
||||
public static final class FuturePaymentAllocationRule {
|
||||
|
||||
public static final String LAST_INSTALLMENT = "LAST_INSTALLMENT";
|
||||
public static final String NEXT_INSTALLMENT = "NEXT_INSTALLMENT";
|
||||
public static final String NEXT_LAST_INSTALLMENT = "NEXT_LAST_INSTALLMENT";
|
||||
|
||||
private FuturePaymentAllocationRule() {}
|
||||
}
|
||||
|
||||
public static final class SupportedInterestRefundTypesItem {
|
||||
|
||||
public static final String MERCHANT_ISSUED_REFUND = "MERCHANT_ISSUED_REFUND";
|
||||
public static final String PAYOUT_REFUND = "PAYOUT_REFUND";
|
||||
|
||||
private SupportedInterestRefundTypesItem() {}
|
||||
}
|
||||
|
||||
public static final class DaysInYearCustomStrategy {
|
||||
|
||||
public static final String FEB_29_PERIOD_ONLY = "FEB_29_PERIOD_ONLY";
|
||||
public static final String FULL_LEAP_YEAR = "FULL_LEAP_YEAR";
|
||||
|
||||
private DaysInYearCustomStrategy() {}
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.modules;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdStatus;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
|
||||
public final class LoanTestValidators {
|
||||
|
||||
private LoanTestValidators() {}
|
||||
|
||||
public static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
|
||||
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
|
||||
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
|
||||
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
|
||||
assertEquals(dueDate, period.getDueDate());
|
||||
assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
|
||||
assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
|
||||
assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
|
||||
assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
|
||||
assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
|
||||
}
|
||||
|
||||
public static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, double principalDue, double principalPaid,
|
||||
double principalOutstanding, double paidInAdvance, double paidLate) {
|
||||
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
|
||||
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
|
||||
assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
|
||||
assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
|
||||
assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
|
||||
assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
|
||||
assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
|
||||
}
|
||||
|
||||
public static void validateFullyUnpaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
|
||||
double principalDue, double feeDue, double penaltyDue, double interestDue) {
|
||||
validateRepaymentPeriod(loanDetails, index,
|
||||
LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(LoanTestData.DATETIME_PATTERN, Locale.ENGLISH)), principalDue, 0,
|
||||
principalDue, feeDue, 0, feeDue, penaltyDue, 0, penaltyDue, interestDue, 0, interestDue, 0, 0);
|
||||
}
|
||||
|
||||
public static void validateFullyPaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
|
||||
double principalDue, double feeDue, double penaltyDue, double interestDue) {
|
||||
validateRepaymentPeriod(loanDetails, index,
|
||||
LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(LoanTestData.DATETIME_PATTERN, Locale.ENGLISH)), principalDue,
|
||||
principalDue, 0, feeDue, feeDue, 0, penaltyDue, penaltyDue, 0, interestDue, interestDue, 0, 0, 0);
|
||||
}
|
||||
|
||||
public static void validateFullyPaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
|
||||
double principalDue, double feeDue, double penaltyDue, double interestDue, double paidLate) {
|
||||
validateRepaymentPeriod(loanDetails, index,
|
||||
LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(LoanTestData.DATETIME_PATTERN, Locale.ENGLISH)), principalDue,
|
||||
principalDue, 0, feeDue, feeDue, 0, penaltyDue, penaltyDue, 0, interestDue, interestDue, 0, 0, paidLate);
|
||||
}
|
||||
|
||||
public static void validateFullyPaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
|
||||
double principalDue, double feeDue, double penaltyDue, double interestDue, double paidLate, double paidInAdvance) {
|
||||
validateRepaymentPeriod(loanDetails, index,
|
||||
LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(LoanTestData.DATETIME_PATTERN, Locale.ENGLISH)), principalDue,
|
||||
principalDue, 0, feeDue, feeDue, 0, penaltyDue, penaltyDue, 0, interestDue, interestDue, 0, paidInAdvance, paidLate);
|
||||
}
|
||||
|
||||
public static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
|
||||
double feeDue, double penaltyDue, double interestDue) {
|
||||
validateRepaymentPeriod(loanDetails, index, dueDate, principalDue, 0, principalDue, feeDue, 0, feeDue, penaltyDue, 0, penaltyDue,
|
||||
interestDue, 0, interestDue, 0, 0);
|
||||
}
|
||||
|
||||
public static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
|
||||
double principalPaid, double principalOutstanding, double feeDue, double feePaid, double feeOutstanding, double penaltyDue,
|
||||
double penaltyPaid, double penaltyOutstanding, double interestDue, double interestPaid, double interestOutstanding,
|
||||
double paidInAdvance, double paidLate) {
|
||||
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
|
||||
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
|
||||
assertEquals(dueDate, period.getDueDate());
|
||||
assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
|
||||
assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
|
||||
assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
|
||||
assertEquals(feeDue, Utils.getDoubleValue(period.getFeeChargesDue()));
|
||||
assertEquals(feePaid, Utils.getDoubleValue(period.getFeeChargesPaid()));
|
||||
assertEquals(feeOutstanding, Utils.getDoubleValue(period.getFeeChargesOutstanding()));
|
||||
assertEquals(penaltyDue, Utils.getDoubleValue(period.getPenaltyChargesDue()));
|
||||
assertEquals(penaltyPaid, Utils.getDoubleValue(period.getPenaltyChargesPaid()));
|
||||
assertEquals(penaltyOutstanding, Utils.getDoubleValue(period.getPenaltyChargesOutstanding()));
|
||||
assertEquals(interestDue, Utils.getDoubleValue(period.getInterestDue()));
|
||||
assertEquals(interestPaid, Utils.getDoubleValue(period.getInterestPaid()));
|
||||
assertEquals(interestOutstanding, Utils.getDoubleValue(period.getInterestOutstanding()));
|
||||
assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
|
||||
assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
|
||||
}
|
||||
|
||||
public static void verifyLoanStatus(GetLoansLoanIdResponse loanDetails, Function<GetLoansLoanIdStatus, Boolean> extractor) {
|
||||
assertNotNull(loanDetails);
|
||||
assertNotNull(loanDetails.getStatus());
|
||||
Boolean actualValue = extractor.apply(loanDetails.getStatus());
|
||||
assertNotNull(actualValue);
|
||||
assertTrue(actualValue);
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.tests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.integrationtests.client.feign.FeignLoanTestBase;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class FeignLoanCreationTest extends FeignLoanTestBase {
|
||||
|
||||
@Test
|
||||
void testCreateAndDisburseLoan_OnePeriodNoInterest() {
|
||||
Long clientId = createClient("John", "Doe");
|
||||
assertNotNull(clientId);
|
||||
|
||||
PostLoanProductsRequest productRequest = onePeriod30DaysNoInterest();
|
||||
Long productId = createLoanProduct(productRequest);
|
||||
assertNotNull(productId);
|
||||
|
||||
String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
|
||||
Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
|
||||
assertNotNull(loanId);
|
||||
|
||||
GetLoansLoanIdResponse loan = getLoanDetails(loanId);
|
||||
assertNotNull(loan);
|
||||
verifyLoanStatus(loan, status -> status.getActive());
|
||||
|
||||
LocalDate expectedRepaymentDate = Utils.getLocalDateOfTenant().plusMonths(1);
|
||||
validateRepaymentPeriod(loan, 1, expectedRepaymentDate, 1000.0, 0.0, 1000.0);
|
||||
|
||||
verifyJournalEntries(loanId, debit(getAccounts().getLoansReceivableAccount().getAccountID().longValue(), 1000.0),
|
||||
credit(getAccounts().getFundSource().getAccountID().longValue(), 1000.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoanRepayment_FullRepayment() {
|
||||
Long clientId = createClient("Jane", "Smith");
|
||||
Long productId = createLoanProduct(onePeriod30DaysNoInterest());
|
||||
String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
|
||||
Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
|
||||
|
||||
Long repaymentId = addRepayment(loanId, repayment(1000.0, todayDate));
|
||||
assertNotNull(repaymentId);
|
||||
|
||||
GetLoansLoanIdResponse loan = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loan, status -> status.getClosedObligationsMet());
|
||||
|
||||
assertEquals(0.0, Utils.getDoubleValue(loan.getSummary().getTotalOutstanding()));
|
||||
assertEquals(1000.0, Utils.getDoubleValue(loan.getSummary().getTotalRepayment()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoanUndoApproval() {
|
||||
Long clientId = createClient("Bob", "Johnson");
|
||||
Long productId = createLoanProduct(onePeriod30DaysNoInterest());
|
||||
String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
|
||||
Long loanId = createApprovedLoan(clientId, productId, todayDate, 1000.0, 1);
|
||||
|
||||
GetLoansLoanIdResponse loanBeforeUndo = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loanBeforeUndo, status -> status.getWaitingForDisbursal());
|
||||
|
||||
undoApproval(loanId);
|
||||
|
||||
GetLoansLoanIdResponse loanAfterUndo = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loanAfterUndo, status -> status.getPendingApproval());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoanUndoDisbursement() {
|
||||
Long clientId = createClient("Alice", "Williams");
|
||||
Long productId = createLoanProduct(onePeriod30DaysNoInterest());
|
||||
String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
|
||||
Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
|
||||
|
||||
GetLoansLoanIdResponse loanBeforeUndo = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loanBeforeUndo, status -> status.getActive());
|
||||
|
||||
undoDisbursement(loanId);
|
||||
|
||||
GetLoansLoanIdResponse loanAfterUndo = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loanAfterUndo, status -> status.getWaitingForDisbursal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFourInstallmentLoan_CumulativeInterest() {
|
||||
Long clientId = createClient("Charlie", "Brown");
|
||||
Long productId = createLoanProduct(fourInstallmentsCumulative());
|
||||
String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
|
||||
Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 4);
|
||||
|
||||
GetLoansLoanIdResponse loan = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loan, status -> status.getActive());
|
||||
|
||||
assertNotNull(loan.getRepaymentSchedule());
|
||||
assertNotNull(loan.getRepaymentSchedule().getPeriods());
|
||||
assertEquals(5, loan.getRepaymentSchedule().getPeriods().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPartialRepayment() {
|
||||
Long clientId = createClient("David", "Miller");
|
||||
Long productId = createLoanProduct(onePeriod30DaysNoInterest());
|
||||
String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
|
||||
Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
|
||||
|
||||
Long repaymentId = addRepayment(loanId, repayment(500.0, todayDate));
|
||||
assertNotNull(repaymentId);
|
||||
|
||||
GetLoansLoanIdResponse loan = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loan, status -> status.getActive());
|
||||
|
||||
assertEquals(500.0, Utils.getDoubleValue(loan.getSummary().getTotalOutstanding()));
|
||||
assertEquals(500.0, Utils.getDoubleValue(loan.getSummary().getTotalRepayment()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUndoRepayment() {
|
||||
Long clientId = createClient("Eva", "Davis");
|
||||
Long productId = createLoanProduct(onePeriod30DaysNoInterest());
|
||||
String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
|
||||
Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
|
||||
|
||||
Long repaymentId = addRepayment(loanId, repayment(1000.0, todayDate));
|
||||
assertNotNull(repaymentId);
|
||||
|
||||
GetLoansLoanIdResponse loanAfterRepayment = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loanAfterRepayment, status -> status.getClosedObligationsMet());
|
||||
|
||||
undoRepayment(loanId, repaymentId, todayDate);
|
||||
|
||||
GetLoansLoanIdResponse loanAfterUndo = getLoanDetails(loanId);
|
||||
verifyLoanStatus(loanAfterUndo, status -> status.getActive());
|
||||
|
||||
LocalDate expectedRepaymentDate = Utils.getLocalDateOfTenant().plusMonths(1);
|
||||
validateRepaymentPeriod(loanAfterUndo, 1, expectedRepaymentDate, 1000.0, 0.0, 1000.0);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.client.feign.tests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import org.apache.fineract.integrationtests.client.feign.FeignLoanTestBase;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class FeignLoanTestBaseSmokeTest extends FeignLoanTestBase {
|
||||
|
||||
@Test
|
||||
void testInfrastructureInitialization() {
|
||||
assertNotNull(accountHelper);
|
||||
assertNotNull(loanHelper);
|
||||
assertNotNull(transactionHelper);
|
||||
assertNotNull(journalHelper);
|
||||
assertNotNull(businessDateHelper);
|
||||
assertNotNull(clientHelper);
|
||||
assertNotNull(getAccounts());
|
||||
assertNotNull(getAccounts().getLoansReceivableAccount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateClient() {
|
||||
Long clientId = createClient("Test", "User");
|
||||
assertNotNull(clientId);
|
||||
}
|
||||
}
|
||||
+81
-13
@@ -59,6 +59,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuil
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -106,6 +107,12 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
|
||||
setProperFinancialActivity(TRANSFER_ACCOUNT);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
// Restore business date configuration after each test
|
||||
cleanUpAndRestoreBusinessDate();
|
||||
}
|
||||
|
||||
private static void setProperFinancialActivity(Account transferAccount) {
|
||||
List<GetFinancialActivityAccountsResponse> financialMappings = FINANCIAL_ACTIVITY_ACCOUNT_HELPER.getAllFinancialActivityAccounts();
|
||||
financialMappings.forEach(mapping -> FINANCIAL_ACTIVITY_ACCOUNT_HELPER.deleteFinancialActivityAccount(mapping.getId()));
|
||||
@@ -144,13 +151,16 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
|
||||
|
||||
// Force close loans 3, 4, ... , N-3, N-2
|
||||
Collections.sort(loanIds);
|
||||
List<Integer> forelosedLoanIds = new ArrayList<>();
|
||||
final CountDownLatch closeLatch = new CountDownLatch(N - 5);
|
||||
// Warm up (EclipseLink sometimes fails if JPQL cache is not warm up but concurrent queries are executed)
|
||||
LOAN_TRANSACTION_HELPER.forecloseLoan("02 March 2020", loanIds.get(2));
|
||||
forelosedLoanIds.add(loanIds.get(2));
|
||||
for (int i = 3; i < N - 2; i++) {
|
||||
final int idx = i;
|
||||
futures.add(executorService.submit(() -> {
|
||||
LOAN_TRANSACTION_HELPER.forecloseLoan("02 March 2020", loanIds.get(idx));
|
||||
forelosedLoanIds.add(loanIds.get(idx));
|
||||
closeLatch.countDown();
|
||||
}));
|
||||
}
|
||||
@@ -163,24 +173,45 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
|
||||
});
|
||||
closeLatch.await();
|
||||
|
||||
// Create list of active (non-foreclosed) loan IDs
|
||||
List<Integer> activeLoanIds = new ArrayList<>(loanIds);
|
||||
activeLoanIds.removeAll(forelosedLoanIds);
|
||||
log.info("Active loan IDs after foreclosure: {}", activeLoanIds);
|
||||
|
||||
// Let's retrieve the partitions
|
||||
List<Map<String, Object>> cobPartitions = CobHelper.getCobPartitions(REQUEST_SPEC, RESPONSE_SPEC, 3, "");
|
||||
log.info("\nLoans created: {},\nRetrieved partitions: {}", loanIds, cobPartitions);
|
||||
Assertions.assertEquals(2, cobPartitions.size());
|
||||
List<Map<String, Object>> allPartitions = CobHelper.getCobPartitions(REQUEST_SPEC, RESPONSE_SPEC, 3, "");
|
||||
log.info("\nLoans created by this test: {}\nAll partitions retrieved: {}", loanIds, allPartitions);
|
||||
|
||||
Assertions.assertEquals(0, cobPartitions.get(0).get("pageNo"));
|
||||
Assertions.assertEquals(loanIds.get(0), cobPartitions.get(0).get("minId"));
|
||||
Assertions.assertEquals(loanIds.get(8), cobPartitions.get(0).get("maxId"));
|
||||
Assertions.assertEquals(3, cobPartitions.get(0).get("count"));
|
||||
// Filter partitions to only include ACTIVE loans created by THIS test
|
||||
// This ensures test isolation - we don't care about leftover loans from other tests
|
||||
List<Map<String, Object>> testLoanPartitions = filterPartitionsForTestLoans(allPartitions, activeLoanIds);
|
||||
log.info("Filtered partitions containing only our active test loans: {}", testLoanPartitions);
|
||||
|
||||
Assertions.assertEquals(1, cobPartitions.get(1).get("pageNo"));
|
||||
Assertions.assertEquals(loanIds.get(9), cobPartitions.get(1).get("minId"));
|
||||
Assertions.assertEquals(loanIds.get(9), cobPartitions.get(1).get("maxId"));
|
||||
Assertions.assertEquals(1, cobPartitions.get(1).get("count"));
|
||||
// Verify partitioning works correctly for our 4 active loans (10 created - 6 foreclosed)
|
||||
// Expected: 2 partitions with page size 3
|
||||
// Partition 0: loans 0, 1, 8 (3 loans)
|
||||
// Partition 1: loan 9 (1 loan)
|
||||
Assertions.assertEquals(2, testLoanPartitions.size(), "Expected 2 partitions for our 4 active test loans");
|
||||
|
||||
// Verify first partition
|
||||
Assertions.assertEquals(0, testLoanPartitions.get(0).get("pageNo"));
|
||||
Assertions.assertEquals(3, testLoanPartitions.get(0).get("count"));
|
||||
Assertions.assertTrue(isLoanIdInRange(loanIds.get(0), testLoanPartitions.get(0)),
|
||||
"First active loan should be in first partition");
|
||||
Assertions.assertTrue(isLoanIdInRange(loanIds.get(1), testLoanPartitions.get(0)),
|
||||
"Second active loan should be in first partition");
|
||||
Assertions.assertTrue(isLoanIdInRange(loanIds.get(8), testLoanPartitions.get(0)),
|
||||
"Ninth active loan should be in first partition");
|
||||
|
||||
// Verify second partition
|
||||
Assertions.assertEquals(1, testLoanPartitions.get(1).get("pageNo"));
|
||||
Assertions.assertEquals(1, testLoanPartitions.get(1).get("count"));
|
||||
Assertions.assertTrue(isLoanIdInRange(loanIds.get(9), testLoanPartitions.get(1)), "Tenth loan should be in second partition");
|
||||
|
||||
executorService.shutdown();
|
||||
} finally {
|
||||
cleanUpAndRestoreBusinessDate();
|
||||
} catch (Exception e) {
|
||||
log.error("Test failed with exception", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,4 +322,41 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
|
||||
return collateral;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters partitions to only include those containing loans from the test's loan IDs. This ensures test isolation
|
||||
* by ignoring any leftover loans from previous test runs.
|
||||
*/
|
||||
private List<Map<String, Object>> filterPartitionsForTestLoans(List<Map<String, Object>> allPartitions, List<Integer> testLoanIds) {
|
||||
List<Map<String, Object>> filteredPartitions = new ArrayList<>();
|
||||
|
||||
for (Map<String, Object> partition : allPartitions) {
|
||||
Integer minId = (Integer) partition.get("minId");
|
||||
Integer maxId = (Integer) partition.get("maxId");
|
||||
|
||||
// Check if this partition contains any of our test loan IDs
|
||||
boolean containsTestLoans = testLoanIds.stream().anyMatch(loanId -> loanId >= minId && loanId <= maxId);
|
||||
|
||||
if (containsTestLoans) {
|
||||
// Count how many of OUR loans are in this partition's range
|
||||
long testLoansInPartition = testLoanIds.stream().filter(loanId -> loanId >= minId && loanId <= maxId).count();
|
||||
|
||||
// Create a new partition entry with corrected count
|
||||
Map<String, Object> filteredPartition = new HashMap<>(partition);
|
||||
filteredPartition.put("count", (int) testLoansInPartition);
|
||||
filteredPartitions.add(filteredPartition);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPartitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a loan ID falls within the min/max range of a partition.
|
||||
*/
|
||||
private boolean isLoanIdInRange(Integer loanId, Map<String, Object> partition) {
|
||||
Integer minId = (Integer) partition.get("minId");
|
||||
Integer maxId = (Integer) partition.get("maxId");
|
||||
return loanId >= minId && loanId <= maxId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.common;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.integrationtests.ConfigProperties;
|
||||
|
||||
public final class FineractFeignClientHelper {
|
||||
|
||||
private static final FineractFeignClient DEFAULT_FINERACT_FEIGN_CLIENT = createNewFineractFeignClient(ConfigProperties.Backend.USERNAME,
|
||||
ConfigProperties.Backend.PASSWORD);
|
||||
|
||||
private FineractFeignClientHelper() {}
|
||||
|
||||
public static FineractFeignClient getFineractFeignClient() {
|
||||
return DEFAULT_FINERACT_FEIGN_CLIENT;
|
||||
}
|
||||
|
||||
public static FineractFeignClient createNewFineractFeignClient(String username, String password) {
|
||||
return createNewFineractFeignClient(username, password, Function.identity()::apply);
|
||||
}
|
||||
|
||||
public static FineractFeignClient createNewFineractFeignClient(String username, String password, boolean debugEnabled) {
|
||||
return createNewFineractFeignClient(username, password, builder -> builder.debug(debugEnabled));
|
||||
}
|
||||
|
||||
public static FineractFeignClient createNewFineractFeignClient(String username, String password,
|
||||
Consumer<FineractFeignClient.Builder> customizer) {
|
||||
String url = System.getProperty("fineract.it.url", buildURI());
|
||||
FineractFeignClient.Builder builder = FineractFeignClient.builder().baseUrl(url).credentials(username, password)
|
||||
.disableSslVerification(true);
|
||||
customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static String buildURI() {
|
||||
return ConfigProperties.Backend.PROTOCOL + "://" + ConfigProperties.Backend.HOST + ":" + ConfigProperties.Backend.PORT
|
||||
+ "/fineract-provider/api";
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ include ':integration-tests'
|
||||
include ':twofactor-tests'
|
||||
include ':oauth2-tests'
|
||||
include ':fineract-client'
|
||||
include ':fineract-client-feign'
|
||||
include ':fineract-doc'
|
||||
include ':fineract-avro-schemas'
|
||||
include ':fineract-e2e-tests-core'
|
||||
|
||||
Reference in New Issue
Block a user