add IBAN+BIC validation
This commit is contained in:
parent
398f15d5de
commit
d98b8feaad
@ -60,6 +60,7 @@ dependencies {
|
|||||||
implementation 'com.vladmihalcea:hibernate-types-55:2.19.2'
|
implementation 'com.vladmihalcea:hibernate-types-55:2.19.2'
|
||||||
implementation 'org.openapitools:jackson-databind-nullable:0.2.3'
|
implementation 'org.openapitools:jackson-databind-nullable:0.2.3'
|
||||||
implementation 'org.modelmapper:modelmapper:3.1.0'
|
implementation 'org.modelmapper:modelmapper:3.1.0'
|
||||||
|
implementation 'org.iban4j:iban4j:3.2.3-RELEASE'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
testCompileOnly 'org.projectlombok:lombok'
|
testCompileOnly 'org.projectlombok:lombok'
|
||||||
|
@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.errors;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import org.iban4j.Iban4jException;
|
||||||
import org.springframework.core.NestedExceptionUtils;
|
import org.springframework.core.NestedExceptionUtils;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@ -54,6 +55,20 @@ public class RestResponseEntityExceptionHandler
|
|||||||
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
|
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Iban4jException.class)
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleIbanAndBicExceptions(
|
||||||
|
final Throwable exc, final WebRequest request) {
|
||||||
|
final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
|
||||||
|
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Throwable.class)
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleOtherExceptions(
|
||||||
|
final Throwable exc, final WebRequest request) {
|
||||||
|
final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
|
||||||
|
return errorResponse(request, httpStatus(message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
|
||||||
|
}
|
||||||
|
|
||||||
private String userReadableEntityClassName(final String exceptionMessage) {
|
private String userReadableEntityClassName(final String exceptionMessage) {
|
||||||
final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) ";
|
final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) ";
|
||||||
final var pattern = Pattern.compile(regex);
|
final var pattern = Pattern.compile(regex);
|
||||||
@ -80,13 +95,6 @@ public class RestResponseEntityExceptionHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Throwable.class)
|
|
||||||
protected ResponseEntity<CustomErrorResponse> handleOtherExceptions(
|
|
||||||
final Throwable exc, final WebRequest request) {
|
|
||||||
final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
|
|
||||||
return errorResponse(request, httpStatus(message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<HttpStatus> httpStatus(final String message) {
|
private Optional<HttpStatus> httpStatus(final String message) {
|
||||||
if (message.startsWith("ERROR: [")) {
|
if (message.startsWith("ERROR: [")) {
|
||||||
for (HttpStatus status : HttpStatus.values()) {
|
for (HttpStatus status : HttpStatus.values()) {
|
||||||
|
@ -5,6 +5,8 @@ import net.hostsharing.hsadminng.context.Context;
|
|||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
|
||||||
|
import org.iban4j.BicUtil;
|
||||||
|
import org.iban4j.IbanUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@ -49,6 +51,9 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
|||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
|
|
||||||
|
IbanUtil.validate(body.getIban());
|
||||||
|
BicUtil.validate(body.getBic());
|
||||||
|
|
||||||
final var entityToSave = map(body, HsOfficeBankAccountEntity.class);
|
final var entityToSave = map(body, HsOfficeBankAccountEntity.class);
|
||||||
entityToSave.setUuid(UUID.randomUUID());
|
entityToSave.setUuid(UUID.randomUUID());
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.errors;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
||||||
@ -9,7 +11,6 @@ import org.springframework.orm.jpa.JpaSystemException;
|
|||||||
import org.springframework.web.context.request.WebRequest;
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
|
||||||
import javax.persistence.EntityNotFoundException;
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@ -147,6 +148,25 @@ class RestResponseEntityExceptionHandlerUnitTest {
|
|||||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo("some error message");
|
assertThat(errorResponse.getBody().getMessage()).isEqualTo("some error message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(classes = {
|
||||||
|
org.iban4j.InvalidCheckDigitException.class,
|
||||||
|
org.iban4j.IbanFormatException.class,
|
||||||
|
org.iban4j.BicFormatException.class })
|
||||||
|
void handlesIbanAndBicExceptions(final Class<? extends RuntimeException> givenExceptionClass)
|
||||||
|
throws Exception {
|
||||||
|
// given
|
||||||
|
final var givenException = givenExceptionClass.getConstructor(String.class).newInstance("given error message");
|
||||||
|
final var givenWebRequest = mock(WebRequest.class);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var errorResponse = exceptionHandler.handleIbanAndBicExceptions(givenException, givenWebRequest);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(400);
|
||||||
|
assertThat(errorResponse.getBody().getMessage()).isEqualTo("given error message");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void handleOtherExceptionsWithoutErrorCode() {
|
void handleOtherExceptionsWithoutErrorCode() {
|
||||||
// given
|
// given
|
||||||
|
@ -115,7 +115,7 @@ class HsOfficeBankAccountControllerAcceptanceTest {
|
|||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@Accepts({ "bankaccount:C(Create)" })
|
@Accepts({ "bankaccount:C(Create)" })
|
||||||
class AddBankAccount {
|
class CreateBankAccount {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void globalAdmin_withoutAssumedRole_canAddBankAccount() {
|
void globalAdmin_withoutAssumedRole_canAddBankAccount() {
|
||||||
@ -195,8 +195,7 @@ class HsOfficeBankAccountControllerAcceptanceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Accepts({ "bankaccount:X(Access Control)" })
|
@Disabled("TODO: not implemented yet - also add Accepts annotation when done")
|
||||||
@Disabled("TODO: not implemented yet")
|
|
||||||
void bankaccountAdminUser_canGetRelatedBankAccount() {
|
void bankaccountAdminUser_canGetRelatedBankAccount() {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var givenBankAccountUuid = bankAccountRepo.findByOptionalHolderLike("first").get(0).getUuid();
|
final var givenBankAccountUuid = bankAccountRepo.findByOptionalHolderLike("first").get(0).getUuid();
|
||||||
@ -212,9 +211,9 @@ class HsOfficeBankAccountControllerAcceptanceTest {
|
|||||||
.contentType("application/json")
|
.contentType("application/json")
|
||||||
.body("", lenientlyEquals("""
|
.body("", lenientlyEquals("""
|
||||||
{
|
{
|
||||||
"label": "first bankaccount",
|
"holder": "...",
|
||||||
"emailAddresses": "bankaccount-admin@firstbankaccount.example.com",
|
"iban": "...",
|
||||||
"phoneNumbers": "+49 123 1234567"
|
"bic": "..."
|
||||||
}
|
}
|
||||||
""")); // @formatter:on
|
""")); // @formatter:on
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,125 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@WebMvcTest(HsOfficeBankAccountController.class)
|
||||||
|
class HsOfficeBankAccountControllerRestTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
Context contextMock;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
HsOfficeBankAccountRepository bankAccountRepo;
|
||||||
|
|
||||||
|
enum InvalidIbanTestCase {
|
||||||
|
TOO_SHORT("DE8810090000123456789", "[10090000123456789] length is 17, expected BBAN length is: 18"),
|
||||||
|
TOO_LONG("DE8810090000123456789123445", "[10090000123456789123445] length is 23, expected BBAN length is: 18"),
|
||||||
|
INVALID_CHARACTER("DE 8810090000123456789123445", "Iban's check digit should contain only digits."),
|
||||||
|
INVALID_CHECKSUM(
|
||||||
|
"DE88100900001234567893",
|
||||||
|
"[DE88100900001234567893] has invalid check digit: 88, expected check digit is: 61");
|
||||||
|
|
||||||
|
private final String givenIban;
|
||||||
|
private final String expectedIbanMessage;
|
||||||
|
|
||||||
|
InvalidIbanTestCase(final String givenIban, final String expectedErrorMessage) {
|
||||||
|
this.givenIban = givenIban;
|
||||||
|
this.expectedIbanMessage = expectedErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
String givenIban() {
|
||||||
|
return givenIban;
|
||||||
|
}
|
||||||
|
|
||||||
|
String expectedErrorMessage() {
|
||||||
|
return expectedIbanMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(InvalidIbanTestCase.class)
|
||||||
|
void invalidIbanBeRejected(final InvalidIbanTestCase testCase) throws Exception {
|
||||||
|
|
||||||
|
// when
|
||||||
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
|
.post("/api/hs/office/bankaccounts")
|
||||||
|
.header("current-user", "superuser-alex@hostsharing.net")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""
|
||||||
|
{
|
||||||
|
"holder": "new test holder",
|
||||||
|
"iban": "%s",
|
||||||
|
"bic": "BEVODEBB"
|
||||||
|
}
|
||||||
|
""".formatted(testCase.givenIban()))
|
||||||
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
|
||||||
|
// then
|
||||||
|
.andExpect(status().is4xxClientError())
|
||||||
|
.andExpect(jsonPath("status", is(400)))
|
||||||
|
.andExpect(jsonPath("error", is("Bad Request")))
|
||||||
|
.andExpect(jsonPath("message", is(testCase.expectedErrorMessage())));
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InvalidBicTestCase {
|
||||||
|
TOO_SHORT("BEVODEB", "Bic length must be 8 or 11"),
|
||||||
|
TOO_LONG("BEVODEBBX", "Bic length must be 8 or 11"),
|
||||||
|
INVALID_CHARACTER("BEV-ODEB", "Bank code must contain only letters.");
|
||||||
|
|
||||||
|
private final String givenBic;
|
||||||
|
private final String expectedErrorMessage;
|
||||||
|
|
||||||
|
InvalidBicTestCase(final String givenBic, final String expectedErrorMessage) {
|
||||||
|
this.givenBic = givenBic;
|
||||||
|
this.expectedErrorMessage = expectedErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
String givenIban() {
|
||||||
|
return givenBic;
|
||||||
|
}
|
||||||
|
|
||||||
|
String expectedErrorMessage() {
|
||||||
|
return expectedErrorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(InvalidBicTestCase.class)
|
||||||
|
void invalidBicBeRejected(final InvalidBicTestCase testCase) throws Exception {
|
||||||
|
|
||||||
|
// when
|
||||||
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
|
.post("/api/hs/office/bankaccounts")
|
||||||
|
.header("current-user", "superuser-alex@hostsharing.net")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""
|
||||||
|
{
|
||||||
|
"holder": "new test holder",
|
||||||
|
"iban": "DE88100900001234567892",
|
||||||
|
"bic": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(testCase.givenIban()))
|
||||||
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
|
||||||
|
// then
|
||||||
|
.andExpect(status().is4xxClientError())
|
||||||
|
.andExpect(jsonPath("status", is(400)))
|
||||||
|
.andExpect(jsonPath("error", is("Bad Request")))
|
||||||
|
.andExpect(jsonPath("message", is(testCase.expectedErrorMessage())));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user