produce client-error for unspecified-properties (#166)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/166 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@@ -27,7 +27,8 @@ public class JsonObjectMapperConfiguration {
|
|||||||
.modules(new JsonNullableModule(), new JavaTimeModule())
|
.modules(new JsonNullableModule(), new JavaTimeModule())
|
||||||
.featuresToEnable(
|
.featuresToEnable(
|
||||||
JsonParser.Feature.ALLOW_COMMENTS,
|
JsonParser.Feature.ALLOW_COMMENTS,
|
||||||
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
|
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS,
|
||||||
|
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
|
||||||
)
|
)
|
||||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,12 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatch
|
|||||||
@Override
|
@Override
|
||||||
public void apply(final HsOfficePartnerPatchResource resource) {
|
public void apply(final HsOfficePartnerPatchResource resource) {
|
||||||
|
|
||||||
|
// HOWTO: allow properties from the GET request to be passed to PATCH, but only if unchanged.
|
||||||
|
// These properties have to be specified in the OpenAPI PATCH resource specification,
|
||||||
|
// ídeally with a comment in the description field, that the value mist be unchanged, if given at all.
|
||||||
|
ignoreUnchangedPropertyValue("uuid", resource.getUuid(), entity.getUuid());
|
||||||
|
ignoreUnchangedPropertyValue("partnerNumber", resource.getPartnerNumber(), entity.getPartnerNumber().toString());
|
||||||
|
|
||||||
if (resource.getPartnerRel() != null) {
|
if (resource.getPartnerRel() != null) {
|
||||||
new HsOfficeRelationPatcher(mapper, em, entity.getPartnerRel()).apply(resource.getPartnerRel());
|
new HsOfficeRelationPatcher(mapper, em, entity.getPartnerRel()).apply(resource.getPartnerRel());
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,19 @@
|
|||||||
package net.hostsharing.hsadminng.mapper;
|
package net.hostsharing.hsadminng.mapper;
|
||||||
|
|
||||||
|
import org.openapitools.jackson.nullable.JsonNullable;
|
||||||
|
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface EntityPatcher<R> {
|
public interface EntityPatcher<R> {
|
||||||
|
|
||||||
void apply(R resource);
|
void apply(R resource);
|
||||||
|
|
||||||
|
default <T> void ignoreUnchangedPropertyValue(final String propertyName, final JsonNullable<T> resourcePropertyValue, final T entityPropertyValue) {
|
||||||
|
Optional.ofNullable(resourcePropertyValue).ifPresent(value -> {
|
||||||
|
if (!value.get().equals(entityPropertyValue) ) {
|
||||||
|
throw new ValidationException(propertyName + " cannot be changed, either leave empty or leave unchanged as " + entityPropertyValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -48,6 +48,7 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
resources:
|
resources:
|
||||||
$ref: '#/components/schemas/BookingResources'
|
$ref: '#/components/schemas/BookingResources'
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsBookingItemInsert:
|
HsBookingItemInsert:
|
||||||
type: object
|
type: object
|
||||||
|
@@ -21,6 +21,7 @@ components:
|
|||||||
caption:
|
caption:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsBookingProjectInsert:
|
HsBookingProjectInsert:
|
||||||
type: object
|
type: object
|
||||||
|
@@ -60,6 +60,7 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
config:
|
config:
|
||||||
$ref: '#/components/schemas/HsHostingAssetConfiguration'
|
$ref: '#/components/schemas/HsHostingAssetConfiguration'
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsHostingAssetInsert:
|
HsHostingAssetInsert:
|
||||||
type: object
|
type: object
|
||||||
|
@@ -29,3 +29,4 @@ components:
|
|||||||
- holder
|
- holder
|
||||||
- iban
|
- iban
|
||||||
- bic
|
- bic
|
||||||
|
additionalProperties: false
|
||||||
|
@@ -31,6 +31,7 @@ components:
|
|||||||
$ref: '#/components/schemas/HsOfficeContactPhoneNumbers'
|
$ref: '#/components/schemas/HsOfficeContactPhoneNumbers'
|
||||||
required:
|
required:
|
||||||
- caption
|
- caption
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsOfficeContactPatch:
|
HsOfficeContactPatch:
|
||||||
type: object
|
type: object
|
||||||
@@ -44,6 +45,7 @@ components:
|
|||||||
$ref: '#/components/schemas/HsOfficeContactEmailAddresses'
|
$ref: '#/components/schemas/HsOfficeContactEmailAddresses'
|
||||||
phoneNumbers:
|
phoneNumbers:
|
||||||
$ref: '#/components/schemas/HsOfficeContactPhoneNumbers'
|
$ref: '#/components/schemas/HsOfficeContactPhoneNumbers'
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsOfficeContactPostalAddress:
|
HsOfficeContactPostalAddress:
|
||||||
# forces generating a java.lang.Object containing a Map, instead of a class with fixed properties
|
# forces generating a java.lang.Object containing a Map, instead of a class with fixed properties
|
||||||
|
@@ -67,6 +67,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
pattern: '^[a-z0-9]{3}$'
|
pattern: '^[a-z0-9]{3}$'
|
||||||
nullable: true
|
nullable: true
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsOfficeDebitorInsert:
|
HsOfficeDebitorInsert:
|
||||||
type: object
|
type: object
|
||||||
@@ -101,3 +102,4 @@ components:
|
|||||||
- debitorNumberSuffix
|
- debitorNumberSuffix
|
||||||
- defaultPrefix
|
- defaultPrefix
|
||||||
- billable
|
- billable
|
||||||
|
additionalProperties: false
|
||||||
|
@@ -48,6 +48,16 @@ components:
|
|||||||
HsOfficePartnerPatch:
|
HsOfficePartnerPatch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
description: if given (e.g. taken from a GET request), it must be identical to the patched entities uuid
|
||||||
|
partnerNumber:
|
||||||
|
type: string
|
||||||
|
pattern: 'P-[0-9]{5}'
|
||||||
|
nullable: true
|
||||||
|
description: if given (e.g. taken from a GET request), it must be identical to the patched entities partnerNumber
|
||||||
partnerRel:
|
partnerRel:
|
||||||
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
|
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
|
||||||
details:
|
details:
|
||||||
@@ -78,6 +88,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date
|
format: date
|
||||||
nullable: true
|
nullable: true
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsOfficePartnerInsert:
|
HsOfficePartnerInsert:
|
||||||
type: object
|
type: object
|
||||||
@@ -93,6 +104,7 @@ components:
|
|||||||
- partnerNumber
|
- partnerNumber
|
||||||
- partnerRel
|
- partnerRel
|
||||||
- details
|
- details
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsOfficePartnerRelInsert:
|
HsOfficePartnerRelInsert:
|
||||||
type: object
|
type: object
|
||||||
@@ -111,6 +123,7 @@ components:
|
|||||||
- anchor.uuid
|
- anchor.uuid
|
||||||
- holder.uuid
|
- holder.uuid
|
||||||
- relContact.uuid
|
- relContact.uuid
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsOfficePartnerDetailsInsert:
|
HsOfficePartnerDetailsInsert:
|
||||||
type: object
|
type: object
|
||||||
@@ -136,3 +149,4 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date
|
format: date
|
||||||
nullable: true
|
nullable: true
|
||||||
|
additionalProperties: false
|
||||||
|
@@ -50,12 +50,12 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- personType
|
- personType
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsOfficePersonPatch:
|
HsOfficePersonPatch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
personType:
|
personType:
|
||||||
nullable: true
|
|
||||||
$ref: '#/components/schemas/HsOfficePersonType'
|
$ref: '#/components/schemas/HsOfficePersonType'
|
||||||
tradeName:
|
tradeName:
|
||||||
type: string
|
type: string
|
||||||
@@ -72,3 +72,4 @@ components:
|
|||||||
familyName:
|
familyName:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
|
additionalProperties: false
|
||||||
|
@@ -41,6 +41,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
HsOfficeRelationPatch:
|
HsOfficeRelationPatch:
|
||||||
type: object
|
type: object
|
||||||
@@ -95,6 +96,7 @@ components:
|
|||||||
description:
|
description:
|
||||||
Additionally to `type` and `anchor.uuid`, either `anchor.uuid` or `anchor`
|
Additionally to `type` and `anchor.uuid`, either `anchor.uuid` or `anchor`
|
||||||
and either `contact` or `contact.uuid` need to be given.
|
and either `contact` or `contact.uuid` need to be given.
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
# relation created as a sub-element with implicitly known type
|
# relation created as a sub-element with implicitly known type
|
||||||
HsOfficeRelationSubInsert:
|
HsOfficeRelationSubInsert:
|
||||||
@@ -116,3 +118,4 @@ components:
|
|||||||
- anchor.uuid
|
- anchor.uuid
|
||||||
- holder.uuid
|
- holder.uuid
|
||||||
- contact.uuid
|
- contact.uuid
|
||||||
|
additionalProperties: false
|
||||||
|
@@ -555,7 +555,6 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"validFrom": "2020-06-05",
|
|
||||||
"validTo": "2022-12-31",
|
"validTo": "2022-12-31",
|
||||||
"resources": {
|
"resources": {
|
||||||
"Traffic": 500,
|
"Traffic": 500,
|
||||||
|
@@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.booking.item;
|
|||||||
|
|
||||||
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository;
|
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
@@ -35,6 +36,7 @@ import static org.hamcrest.Matchers.matchesRegex;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
@@ -112,7 +114,6 @@ class HsBookingItemControllerRestTest {
|
|||||||
"type": "MANAGED_SERVER",
|
"type": "MANAGED_SERVER",
|
||||||
"caption": "some new booking",
|
"caption": "some new booking",
|
||||||
"validTo": "{validTo}",
|
"validTo": "{validTo}",
|
||||||
"garbage": "should not be accepted",
|
|
||||||
"resources": { "CPU": 12, "RAM": 4, "SSD": 100, "Traffic": 250 }
|
"resources": { "CPU": 12, "RAM": 4, "SSD": 100, "Traffic": 250 }
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -120,6 +121,7 @@ class HsBookingItemControllerRestTest {
|
|||||||
.replace("{validTo}", LocalDate.now().plusMonths(1).toString())
|
.replace("{validTo}", LocalDate.now().plusMonths(1).toString())
|
||||||
)
|
)
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(print())
|
||||||
|
|
||||||
// then
|
// then
|
||||||
.andExpect(status().isCreated())
|
.andExpect(status().isCreated())
|
||||||
@@ -161,7 +163,7 @@ class HsBookingItemControllerRestTest {
|
|||||||
"project.uuid": "{projectUuid}",
|
"project.uuid": "{projectUuid}",
|
||||||
"type": "MANAGED_SERVER",
|
"type": "MANAGED_SERVER",
|
||||||
"caption": "some new booking",
|
"caption": "some new booking",
|
||||||
"validFrom": "{validFrom}",
|
"validFrom": "{validFrom}", // not specified => not accepted
|
||||||
"resources": { "CPU": 12, "RAM": 4, "SSD": 100, "Traffic": 250 }
|
"resources": { "CPU": 12, "RAM": 4, "SSD": 100, "Traffic": 250 }
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -169,24 +171,15 @@ class HsBookingItemControllerRestTest {
|
|||||||
.replace("{validFrom}", LocalDate.now().plusMonths(1).toString())
|
.replace("{validFrom}", LocalDate.now().plusMonths(1).toString())
|
||||||
)
|
)
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(print())
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// TODO.test: MockMvc does not seem to validate additionalProperties=false
|
.andExpect(status().is4xxClientError())
|
||||||
// .andExpect(status().is4xxClientError())
|
|
||||||
.andExpect(status().isCreated())
|
|
||||||
.andExpect(jsonPath("$", lenientlyEquals("""
|
.andExpect(jsonPath("$", lenientlyEquals("""
|
||||||
{
|
{
|
||||||
"type": "MANAGED_SERVER",
|
"message": "ERROR: [400] JSON parse error: Unrecognized field \\"validFrom\\" (class ${resourceClass}), not marked as ignorable"
|
||||||
"caption": "some new booking",
|
}
|
||||||
"validFrom": "{today}",
|
""".replace("${resourceClass}", HsBookingItemInsertResource.class.getName()))));
|
||||||
"validTo": null,
|
|
||||||
"resources": { "CPU": 12, "SSD": 100, "Traffic": 250 }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
.replace("{today}", LocalDate.now().toString())
|
|
||||||
.replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString()))
|
|
||||||
))
|
|
||||||
.andExpect(header().string("Location", matchesRegex("http://localhost/api/hs/booking/items/[^/]*")));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -50,6 +50,7 @@ import static org.mockito.ArgumentMatchers.any;
|
|||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@@ -667,10 +668,8 @@ public class HsHostingAssetControllerRestTest {
|
|||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("""
|
.content("""
|
||||||
{
|
{
|
||||||
"type": "DOMAIN_HTTP_SETUP",
|
|
||||||
"identifier": "updated example.org|HTTP",
|
|
||||||
"caption": "some updated fake Domain-HTTP-Setup",
|
"caption": "some updated fake Domain-HTTP-Setup",
|
||||||
"alarmContact": null,
|
"alarmContact.uuid": null,
|
||||||
"config": {
|
"config": {
|
||||||
"autoconfig": true,
|
"autoconfig": true,
|
||||||
"multiviews": true,
|
"multiviews": true,
|
||||||
@@ -682,6 +681,7 @@ public class HsHostingAssetControllerRestTest {
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(print())
|
||||||
|
|
||||||
// then
|
// then
|
||||||
.andExpect(status().is2xxSuccessful())
|
.andExpect(status().is2xxSuccessful())
|
||||||
|
@@ -627,13 +627,12 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"contactUuid": "%s",
|
|
||||||
"vatId": "VAT222222",
|
"vatId": "VAT222222",
|
||||||
"vatCountryCode": "AA",
|
"vatCountryCode": "AA",
|
||||||
"vatBusiness": true,
|
"vatBusiness": true,
|
||||||
"defaultPrefix": "for"
|
"defaultPrefix": "for"
|
||||||
}
|
}
|
||||||
""".formatted(givenContact.getUuid()))
|
""")
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
.patch("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid())
|
.patch("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid())
|
||||||
|
@@ -157,7 +157,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
final var givenMandantPerson = personRealRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0);
|
final var givenMandantPerson = personRealRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0);
|
||||||
final var givenPerson = personRealRepo.findPersonByOptionalNameLike("Third").get(0);
|
final var givenPerson = personRealRepo.findPersonByOptionalNameLike("Third").get(0);
|
||||||
|
|
||||||
final var location = RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
@@ -169,8 +169,6 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
"holder.uuid": "%s",
|
"holder.uuid": "%s",
|
||||||
"contact.uuid": "%s"
|
"contact.uuid": "%s"
|
||||||
},
|
},
|
||||||
"person.uuid": "%s",
|
|
||||||
"contact.uuid": "%s",
|
|
||||||
"details": {}
|
"details": {}
|
||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(
|
||||||
@@ -207,8 +205,6 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
"holder.uuid": "%s",
|
"holder.uuid": "%s",
|
||||||
"contact.uuid": "%s"
|
"contact.uuid": "%s"
|
||||||
},
|
},
|
||||||
"person.uuid": "%s",
|
|
||||||
"contact.uuid": "%s",
|
|
||||||
"details": {}
|
"details": {}
|
||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(
|
||||||
@@ -324,7 +320,6 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"partnerNumber": "P-20011",
|
|
||||||
"partnerRel": {
|
"partnerRel": {
|
||||||
"holder.uuid": "%s"
|
"holder.uuid": "%s"
|
||||||
},
|
},
|
||||||
|
@@ -10,28 +10,28 @@ import net.hostsharing.hsadminng.mapper.StrictMapper;
|
|||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.TestInstance;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.openapitools.jackson.nullable.JsonNullable;
|
import org.openapitools.jackson.nullable.JsonNullable;
|
||||||
|
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
|
|
||||||
@TestInstance(PER_CLASS)
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
// This test class does not subclass PatchUnitTestBase because it has no directly patchable properties.
|
// This test class does not subclass PatchUnitTestBase because it has no directly patchable properties.
|
||||||
// But the factory-structure is kept, so PatchUnitTestBase could easily be plugged back in if needed.
|
// But the factory-structure is kept, so PatchUnitTestBase could easily be plugged back in if needed.
|
||||||
class HsOfficePartnerEntityPatcherUnitTest {
|
class HsOfficePartnerEntityPatcherUnitTest {
|
||||||
|
|
||||||
private static final UUID INITIAL_PARTNER_UUID = UUID.randomUUID();
|
private static final UUID INITIAL_PARTNER_UUID = UUID.randomUUID();
|
||||||
|
private static final int INITIAL_PARTNER_NUMBER = 12345;
|
||||||
private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID();
|
private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID();
|
||||||
private static final UUID INITIAL_PARTNER_PERSON_UUID = UUID.randomUUID();
|
private static final UUID INITIAL_PARTNER_PERSON_UUID = UUID.randomUUID();
|
||||||
private static final UUID INITIAL_DETAILS_UUID = UUID.randomUUID();
|
private static final UUID INITIAL_DETAILS_UUID = UUID.randomUUID();
|
||||||
@@ -50,7 +50,9 @@ class HsOfficePartnerEntityPatcherUnitTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private EntityManagerWrapper emw;
|
private EntityManagerWrapper emw;
|
||||||
|
|
||||||
private StrictMapper mapper = new StrictMapper(emw);
|
private final StrictMapper mapper = new StrictMapper(emw);
|
||||||
|
private final HsOfficePartnerPatchResource patchResource = newPatchResource();
|
||||||
|
private final HsOfficePartnerRbacEntity entity = newInitialEntity();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void initMocks() {
|
void initMocks() {
|
||||||
@@ -60,14 +62,88 @@ class HsOfficePartnerEntityPatcherUnitTest {
|
|||||||
HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build());
|
HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignorePartnerUuidIfNotGiven() {
|
||||||
|
// given
|
||||||
|
patchResource.setUuid(null);
|
||||||
|
|
||||||
|
// when
|
||||||
|
createPatcher(entity).apply(patchResource);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(entity.getUuid()).isEqualTo(INITIAL_PARTNER_UUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignoreUnchangedPartnerUuid() {
|
||||||
|
// given
|
||||||
|
patchResource.setUuid(JsonNullable.of(INITIAL_PARTNER_UUID));
|
||||||
|
|
||||||
|
// when
|
||||||
|
createPatcher(entity).apply(patchResource);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(entity.getUuid()).isEqualTo(INITIAL_PARTNER_UUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectChangingThePartnerUuid() {
|
||||||
|
// given
|
||||||
|
patchResource.setUuid(JsonNullable.of(UUID.randomUUID()));
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var exception = catchThrowable(() -> createPatcher(entity).apply(patchResource));
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(exception).isInstanceOf(ValidationException.class).hasMessageContaining(
|
||||||
|
"uuid cannot be changed, either leave empty or leave unchanged as " + INITIAL_PARTNER_UUID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignorePartnerNumberIfNotGiven() {
|
||||||
|
// given
|
||||||
|
patchResource.setPartnerNumber(null);
|
||||||
|
|
||||||
|
// when
|
||||||
|
createPatcher(entity).apply(patchResource);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(entity.getPartnerNumber()).isEqualTo(INITIAL_PARTNER_NUMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignoreUnchangedPartnerNumber() {
|
||||||
|
// given
|
||||||
|
patchResource.setPartnerNumber(JsonNullable.of(String.valueOf(INITIAL_PARTNER_NUMBER)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
createPatcher(entity).apply(patchResource);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(entity.getPartnerNumber()).isEqualTo(INITIAL_PARTNER_NUMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectChangingThePartnerNumber() {
|
||||||
|
// given
|
||||||
|
patchResource.setPartnerNumber(JsonNullable.of("99999"));
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var exception = catchThrowable(() -> createPatcher(entity).apply(patchResource));
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(exception).isInstanceOf(ValidationException.class).hasMessageContaining(
|
||||||
|
"partnerNumber cannot be changed, either leave empty or leave unchanged as " + INITIAL_PARTNER_NUMBER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void patchPartnerPerson() {
|
void patchPartnerPerson() {
|
||||||
// given
|
// given
|
||||||
final var patchResource = newPatchResource();
|
|
||||||
final var newHolderUuid = UUID.randomUUID();
|
final var newHolderUuid = UUID.randomUUID();
|
||||||
patchResource.setPartnerRel(new HsOfficeRelationPatchResource());
|
patchResource.setPartnerRel(new HsOfficeRelationPatchResource());
|
||||||
patchResource.getPartnerRel().setHolderUuid(JsonNullable.of(newHolderUuid));
|
patchResource.getPartnerRel().setHolderUuid(JsonNullable.of(newHolderUuid));
|
||||||
final var entity = newInitialEntity();
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
createPatcher(entity).apply(patchResource);
|
createPatcher(entity).apply(patchResource);
|
||||||
@@ -79,11 +155,9 @@ class HsOfficePartnerEntityPatcherUnitTest {
|
|||||||
@Test
|
@Test
|
||||||
void patchPartnerContact() {
|
void patchPartnerContact() {
|
||||||
// given
|
// given
|
||||||
final var patchResource = newPatchResource();
|
|
||||||
final var newContactUuid = UUID.randomUUID();
|
final var newContactUuid = UUID.randomUUID();
|
||||||
patchResource.setPartnerRel(new HsOfficeRelationPatchResource());
|
patchResource.setPartnerRel(new HsOfficeRelationPatchResource());
|
||||||
patchResource.getPartnerRel().setContactUuid(JsonNullable.of(newContactUuid));
|
patchResource.getPartnerRel().setContactUuid(JsonNullable.of(newContactUuid));
|
||||||
final var entity = newInitialEntity();
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
createPatcher(entity).apply(patchResource);
|
createPatcher(entity).apply(patchResource);
|
||||||
@@ -95,11 +169,9 @@ class HsOfficePartnerEntityPatcherUnitTest {
|
|||||||
@Test
|
@Test
|
||||||
void patchPartnerDetails() {
|
void patchPartnerDetails() {
|
||||||
// given
|
// given
|
||||||
final var patchResource = newPatchResource();
|
|
||||||
final var newDateOfBirth = LocalDate.now();
|
final var newDateOfBirth = LocalDate.now();
|
||||||
patchResource.setDetails(new HsOfficePartnerDetailsPatchResource());
|
patchResource.setDetails(new HsOfficePartnerDetailsPatchResource());
|
||||||
patchResource.getDetails().setDateOfDeath(JsonNullable.of(newDateOfBirth));
|
patchResource.getDetails().setDateOfDeath(JsonNullable.of(newDateOfBirth));
|
||||||
final var entity = newInitialEntity();
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
createPatcher(entity).apply(patchResource);
|
createPatcher(entity).apply(patchResource);
|
||||||
@@ -111,7 +183,7 @@ class HsOfficePartnerEntityPatcherUnitTest {
|
|||||||
protected HsOfficePartnerRbacEntity newInitialEntity() {
|
protected HsOfficePartnerRbacEntity newInitialEntity() {
|
||||||
final var entity = HsOfficePartnerRbacEntity.builder()
|
final var entity = HsOfficePartnerRbacEntity.builder()
|
||||||
.uuid(INITIAL_PARTNER_UUID)
|
.uuid(INITIAL_PARTNER_UUID)
|
||||||
.partnerNumber(12345)
|
.partnerNumber(INITIAL_PARTNER_NUMBER)
|
||||||
.partnerRel(HsOfficeRelationRealEntity.builder()
|
.partnerRel(HsOfficeRelationRealEntity.builder()
|
||||||
.holder(givenInitialPartnerPerson)
|
.holder(givenInitialPartnerPerson)
|
||||||
.contact(givenInitialContact)
|
.contact(givenInitialContact)
|
||||||
|
@@ -398,7 +398,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
void shouldInvalidateSepaMandateForDebitor() {
|
void shouldInvalidateSepaMandateForDebitor() {
|
||||||
new InvalidateSepaMandateForDebitor(scenarioTest)
|
new InvalidateSepaMandateForDebitor(scenarioTest)
|
||||||
.given("bankAccountIBAN", "DE02701500000000594937")
|
.given("bankAccountIBAN", "DE02701500000000594937")
|
||||||
.given("mandateValidUntil", "2025-09-30")
|
.given("mandateValidTo", "2025-09-30")
|
||||||
.doRun();
|
.doRun();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,7 +25,7 @@ public class InvalidateSepaMandateForDebitor extends UseCase<InvalidateSepaManda
|
|||||||
return withTitle("Patch the End of the Mandate into the SEPA-Mandate", () ->
|
return withTitle("Patch the End of the Mandate into the SEPA-Mandate", () ->
|
||||||
httpPatch("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}", usingJsonBody("""
|
httpPatch("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}", usingJsonBody("""
|
||||||
{
|
{
|
||||||
"validUntil": ${mandateValidUntil}
|
"validTo": ${mandateValidTo}
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
.expecting(OK).expecting(JSON)
|
.expecting(OK).expecting(JSON)
|
||||||
|
@@ -34,18 +34,15 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
|
|||||||
() -> httpPatch("/api/hs/office/partners/%{Partner: %{partnerNumber}}",
|
() -> httpPatch("/api/hs/office/partners/%{Partner: %{partnerNumber}}",
|
||||||
usingJsonBody("""
|
usingJsonBody("""
|
||||||
{
|
{
|
||||||
"wrong1": false,
|
|
||||||
"partnerRel": {
|
"partnerRel": {
|
||||||
"wrong2": false,
|
|
||||||
"holder": {
|
"holder": {
|
||||||
"personType": "UNINCORPORATED_FIRM",
|
"personType": "UNINCORPORATED_FIRM",
|
||||||
"tradeName": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
"tradeName": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"wrong3": false,
|
|
||||||
"caption": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
"caption": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
||||||
"postalAddress": {
|
"postalAddress": {
|
||||||
"wrong4": false,
|
"whatever": "any key is allowed here",
|
||||||
"name": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
"name": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
||||||
"co": "%{representativeGivenName} %{representativeFamilyName}",
|
"co": "%{representativeGivenName} %{representativeFamilyName}",
|
||||||
%{communityOfHeirsPostalAddress}
|
%{communityOfHeirsPostalAddress}
|
||||||
|
Reference in New Issue
Block a user