diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java index 0a041763..bf8a9c09 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java @@ -13,15 +13,13 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.annotation.PostConstruct; import java.util.List; import java.util.UUID; -import java.util.function.BiConsumer; import static net.hostsharing.hsadminng.errors.Validate.validate; -import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; @RestController - public class HsOfficeContactController implements HsOfficeContactsApi { @Autowired @@ -30,9 +28,20 @@ public class HsOfficeContactController implements HsOfficeContactsApi { @Autowired private StrictMapper mapper; + @Autowired + private HsOfficeContactFromResourceConverter contactFromResourceConverter; + @Autowired private HsOfficeContactRbacRepository contactRepo; + @PostConstruct + public void init() { + // HOWTO: add a ModelMapper converter for a generic entity class to a ModelMapper to be used in a certain context + // This @PostConstruct could be implemented in the converter, but only without generics. + // But this converter is for HsOfficeContactRbacEntity and HsOfficeContactRealEntity. + mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRbacEntity.class); + } + @Override @Transactional(readOnly = true) @Timed("app.office.contacts.api.getListOfContacts") @@ -62,7 +71,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi { context.define(currentSubject, assumedRoles); - final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); + final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class); final var saved = contactRepo.save(entityToSave); @@ -128,11 +137,4 @@ public class HsOfficeContactController implements HsOfficeContactsApi { final var mapped = mapper.map(saved, HsOfficeContactResource.class); return ResponseEntity.ok(mapped); } - - @SuppressWarnings("unchecked") - final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { - entity.putPostalAddress(from(resource.getPostalAddress())); - entity.putEmailAddresses(from(resource.getEmailAddresses())); - entity.putPhoneNumbers(from(resource.getPhoneNumbers())); - }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java new file mode 100644 index 00000000..8ebcbc88 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java @@ -0,0 +1,27 @@ +package net.hostsharing.hsadminng.hs.office.contact; + +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; +import org.modelmapper.Converter; +import org.modelmapper.spi.MappingContext; +import org.springframework.stereotype.Component; + +import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; + +// HOWTO: implement a ModelMapper converter which converts from a (JSON) resource instance to a generic entity instance (RBAC vs. REAL) +@Component +public class HsOfficeContactFromResourceConverter + implements Converter { + + @Override + @SneakyThrows + public E convert(final MappingContext context) { + final var resource = context.getSource(); + final var entity = context.getDestinationType().getDeclaredConstructor().newInstance(); + entity.setCaption(resource.getCaption()); + entity.putPostalAddress(from(resource.getPostalAddress())); + entity.putEmailAddresses(from(resource.getEmailAddresses())); + entity.putPhoneNumbers(from(resource.getPhoneNumbers())); + return entity; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 2c519849..e73b108a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -3,8 +3,10 @@ package net.hostsharing.hsadminng.hs.office.partner; import io.micrometer.core.annotation.Timed; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.errors.ReferenceNotFoundException; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartnersApi; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource; @@ -22,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.util.List; @@ -41,6 +44,9 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { @Autowired private StrictMapper mapper; + @Autowired + private HsOfficeContactFromResourceConverter contactFromResourceConverter; + @Autowired private HsOfficePartnerRbacRepository rbacPartnerRepo; @@ -50,6 +56,11 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { @PersistenceContext private EntityManager em; + @PostConstruct + public void init() { + mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRealEntity.class); + } + @Override @Transactional(readOnly = true) @Timed("app.office.partners.api.getListOfPartners") @@ -185,7 +196,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) { final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid()); if (partnerPersonHasChanged) { - final var count = em.createNativeQuery(""" + em.createNativeQuery(""" UPDATE hs_office.relation SET holderUuid = :newPartnerPersonUuid WHERE type = 'DEBITOR' AND holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid @@ -193,7 +204,6 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) .executeUpdate(); - System.out.println(count); // FIXME: remove em.createNativeQuery(""" UPDATE hs_office.relation SET anchorUuid = :newPartnerPersonUuid diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java index 5d36b602..5c176f93 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.partner; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource; -import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntityPatcher; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationPatcher; import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.StrictMapper; @@ -26,7 +26,7 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher { +public class HsOfficeRelationPatcher implements EntityPatcher { private final StrictMapper mapper; private final EntityManager em; private final HsOfficeRelation entity; - public HsOfficeRelationEntityPatcher(final StrictMapper mapper, final EntityManager em, final HsOfficeRelation entity) { + public HsOfficeRelationPatcher(final StrictMapper mapper, final EntityManager em, final HsOfficeRelation entity) { this.mapper = mapper; this.em = em; this.entity = entity; diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 2bf87f09..664b803c 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -20,6 +20,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.repository.Repository; import org.springframework.web.bind.annotation.RestController; +import jakarta.annotation.PostConstruct; import jakarta.persistence.Table; import java.lang.annotation.Annotation; @@ -420,7 +421,7 @@ public class ArchitectureTest { if (isGeneratedSpringRepositoryMethod(item, method)) { continue; } - if (item.isAnnotatedWith(RestController.class) && !method.getModifiers().contains(PUBLIC)) { + if (!method.getModifiers().contains(PUBLIC) || method.isAnnotatedWith(PostConstruct.class)) { continue; } final var message = String.format( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java index 9a688367..074b3580 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.office.partner; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; @@ -38,7 +39,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficePartnerController.class) -@Import({StrictMapper.class, DisableSecurityConfig.class}) +@Import({ StrictMapper.class, HsOfficeContactFromResourceConverter.class, DisableSecurityConfig.class}) @ActiveProfiles("test") class HsOfficePartnerControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java deleted file mode 100644 index f4acb10b..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java +++ /dev/null @@ -1,227 +0,0 @@ -package net.hostsharing.hsadminng.hs.office.relation; - -import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonTypeResource; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource; -import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity; -import net.hostsharing.hsadminng.mapper.StrictMapper; -import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; -import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.openapitools.jackson.nullable.JsonNullable; - -import jakarta.validation.ValidationException; -import java.util.UUID; -import java.util.stream.Stream; - -import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON; -import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON; -import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.PARTNER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.assertj.core.api.Assumptions.assumeThat; -import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@TestInstance(PER_CLASS) -@ExtendWith(MockitoExtension.class) -class HsOfficeRelationEntityPatcherUnitTest extends PatchUnitTestBase< - HsOfficeRelationPatchResource, - HsOfficeRelation - > { - - private static final UUID INITIAL_RELATION_UUID = UUID.randomUUID(); - private static final UUID INITIAL_ANCHOR_UUID = UUID.randomUUID(); - private static final UUID INITIAL_HOLDER_UUID = UUID.randomUUID(); - private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID(); - - private static final UUID PATCHED_HOLDER_UUID = UUID.randomUUID(); - private static HsOfficePersonInsertResource HOLDER_PATCH_RESOURCE = new HsOfficePersonInsertResource() { - - { - setPersonType(HsOfficePersonTypeResource.NATURAL_PERSON); - setFamilyName("Patched-Holder-Family-Name"); - setGivenName("Patched-Holder-Given-Name"); - } - }; - private static HsOfficePersonRealEntity PATCHED_HOLDER = HsOfficePersonRealEntity.builder() - .uuid(PATCHED_HOLDER_UUID) - .personType(NATURAL_PERSON) - .familyName("Patched-Holder-Family-Name") - .givenName("Patched-Holder-Given-Name") - .build(); - - private static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); - private static HsOfficeContactInsertResource CONTACT_PATCH_RESOURCE = new HsOfficeContactInsertResource() { - - { - setCaption("Patched-Contact-Caption"); - } - }; - private static HsOfficeContactRealEntity PATCHED_CONTACT = HsOfficeContactRealEntity.builder() - .uuid(PATCHED_CONTACT_UUID) - .caption("Patched-Contact-Caption") - .build(); - - @Mock - private EntityManagerWrapper emw; - - private StrictMapper mapper = new StrictMapper(emw); - - @BeforeEach - void initMocks() { - lenient().when(emw.getReference(HsOfficePersonRealEntity.class, PATCHED_HOLDER_UUID)).thenAnswer( - p -> PATCHED_HOLDER); - lenient().when(emw.getReference(HsOfficeContactRealEntity.class, PATCHED_CONTACT_UUID)).thenAnswer( - p -> PATCHED_CONTACT); - } - - @Override - protected HsOfficeRelation newInitialEntity() { - final var entity = new HsOfficeRelationRealEntity(); - entity.setUuid(INITIAL_RELATION_UUID); - entity.setType(PARTNER); - entity.setAnchor(HsOfficePersonRealEntity.builder() - .uuid(INITIAL_ANCHOR_UUID) - .personType(LEGAL_PERSON) - .tradeName("Initial-Anchor-Tradename") - .build()); - entity.setHolder(HsOfficePersonRealEntity.builder() - .uuid(INITIAL_HOLDER_UUID) - .personType(NATURAL_PERSON) - .familyName("Initial-Holder-Family-Name") - .givenName("Initial-Holder-Given-Name") - .build()); - entity.setContact(HsOfficeContactRealEntity.builder() - .uuid(INITIAL_CONTACT_UUID) - .caption("Initial-Contact-Caption") - .build()); - return entity; - } - - @Override - protected HsOfficeRelationPatchResource newPatchResource() { - return new HsOfficeRelationPatchResource(); - } - - @Override - protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelation relation) { - return new HsOfficeRelationEntityPatcher(mapper, emw, relation); - } - - @Override - protected Stream propertyTestDescriptors() { - return Stream.of( - new JsonNullableProperty<>( - "holderUuid", - HsOfficeRelationPatchResource::setHolderUuid, - PATCHED_HOLDER_UUID, - HsOfficeRelation::setHolder, - PATCHED_HOLDER), - new SimpleProperty<>( - "holder", - HsOfficeRelationPatchResource::setHolder, - HOLDER_PATCH_RESOURCE, - HsOfficeRelation::setHolder, - withoutUuid(PATCHED_HOLDER)) - .notNullable(), - - new JsonNullableProperty<>( - "contactUuid", - HsOfficeRelationPatchResource::setContactUuid, - PATCHED_CONTACT_UUID, - HsOfficeRelation::setContact, - PATCHED_CONTACT), - new SimpleProperty<>( - "contact", - HsOfficeRelationPatchResource::setContact, - CONTACT_PATCH_RESOURCE, - HsOfficeRelation::setContact, - withoutUuid(PATCHED_CONTACT)) - .notNullable() - ); - } - - @Override - protected void willPatchAllProperties() { - // this generic test does not work because either holder or holder.uuid can be set - assumeThat(true).isFalse(); - } - - @Test - void willThrowExceptionIfHolderAndHolderUuidAreGiven() { - // given - final var givenEntity = newInitialEntity(); - final var patchResource = newPatchResource(); - patchResource.setHolderUuid(JsonNullable.of(PATCHED_HOLDER_UUID)); - patchResource.setHolder(HOLDER_PATCH_RESOURCE); - - // when - final var exception = catchThrowable(() -> createPatcher(givenEntity).apply(patchResource)); - - // then - assertThat(exception).isInstanceOf(ValidationException.class) - .hasMessage("either \"holder\" or \"holder.uuid\" can be given, not both"); - } - - @Test - void willThrowExceptionIfContactAndContactUuidAreGiven() { - // given - final var givenEntity = newInitialEntity(); - final var patchResource = newPatchResource(); - patchResource.setContactUuid(JsonNullable.of(PATCHED_CONTACT_UUID)); - patchResource.setContact(CONTACT_PATCH_RESOURCE); - - // when - final var exception = catchThrowable(() -> createPatcher(givenEntity).apply(patchResource)); - - // then - assertThat(exception).isInstanceOf(ValidationException.class) - .hasMessage("either \"contact\" or \"contact.uuid\" can be given, not both"); - } - - @Test - void willPersistNewHolder() { - // given - final var givenEntity = newInitialEntity(); - final var patchResource = newPatchResource(); - patchResource.setHolder(HOLDER_PATCH_RESOURCE); - - // when - createPatcher(givenEntity).apply(patchResource); - - // then - verify(emw, times(1)).persist(givenEntity.getHolder()); - } - - @Test - void willPersistNewContact() { - // given - final var givenEntity = newInitialEntity(); - final var patchResource = newPatchResource(); - patchResource.setContact(CONTACT_PATCH_RESOURCE); - - // when - createPatcher(givenEntity).apply(patchResource); - - // then - verify(emw, times(1)).persist(givenEntity.getContact()); - } - - private HsOfficePersonRealEntity withoutUuid(final HsOfficePersonRealEntity givenWithUuid) { - return givenWithUuid.toBuilder().uuid(null).build(); - } - - private HsOfficeContactRealEntity withoutUuid(final HsOfficeContactRealEntity givenWithUuid) { - return givenWithUuid.toBuilder().uuid(null).build(); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java index 72871086..8647fc19 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java @@ -1,24 +1,38 @@ package net.hostsharing.hsadminng.hs.office.relation; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonTypeResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity; import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.openapitools.jackson.nullable.JsonNullable; +import jakarta.validation.ValidationException; +import java.util.Map; import java.util.UUID; import java.util.stream.Stream; +import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON; +import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON; +import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.PARTNER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.assertj.core.api.Assumptions.assumeThat; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @TestInstance(PER_CLASS) @ExtendWith(MockitoExtension.class) @@ -27,38 +41,84 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase< HsOfficeRelation > { - static final UUID INITIAL_RELATION_UUID = UUID.randomUUID(); - static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); + private static final UUID INITIAL_RELATION_UUID = UUID.randomUUID(); + private static final UUID INITIAL_ANCHOR_UUID = UUID.randomUUID(); + private static final UUID INITIAL_HOLDER_UUID = UUID.randomUUID(); + private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID(); + + private static final UUID PATCHED_HOLDER_UUID = UUID.randomUUID(); + private static HsOfficePersonInsertResource HOLDER_PATCH_RESOURCE = new HsOfficePersonInsertResource(); + { + { + HOLDER_PATCH_RESOURCE.setPersonType(HsOfficePersonTypeResource.NATURAL_PERSON); + HOLDER_PATCH_RESOURCE.setFamilyName("Patched-Holder-Family-Name"); + HOLDER_PATCH_RESOURCE.setGivenName("Patched-Holder-Given-Name"); + } + }; + private static HsOfficePersonRealEntity PATCHED_HOLDER = HsOfficePersonRealEntity.builder() + .uuid(PATCHED_HOLDER_UUID) + .personType(NATURAL_PERSON) + .familyName("Patched-Holder-Family-Name") + .givenName("Patched-Holder-Given-Name") + .build(); + + private static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); + private static HsOfficeContactInsertResource CONTACT_PATCH_RESOURCE = new HsOfficeContactInsertResource(); + { + { + CONTACT_PATCH_RESOURCE.setCaption("Patched-Contact-Caption"); + CONTACT_PATCH_RESOURCE.setEmailAddresses(Map.ofEntries( + Map.entry("main", "patched@example.org") + )); + } + }; + private static HsOfficeContactRealEntity PATCHED_CONTACT = HsOfficeContactRealEntity.builder() + .uuid(PATCHED_CONTACT_UUID) + .caption("Patched-Contact-Caption") + .emailAddresses(Map.ofEntries( + Map.entry("main", "patched@example.org") + )) + .build(); @Mock - EntityManagerWrapper emw; + private EntityManagerWrapper emw; + + private StrictMapper mapper; - StrictMapper mapper = new StrictMapper(emw); @BeforeEach - void initMocks() { - lenient().when(emw.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation -> - HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build()); - } + void init() { + mapper = new StrictMapper(emw); // emw is injected after the constructor got called + mapper.addConverter( + new HsOfficeContactFromResourceConverter<>(), + HsOfficeContactInsertResource.class, HsOfficeContactRealEntity.class); - final HsOfficePersonRealEntity givenInitialAnchorPerson = HsOfficePersonRealEntity.builder() - .uuid(UUID.randomUUID()) - .build(); - final HsOfficePersonRealEntity givenInitialHolderPerson = HsOfficePersonRealEntity.builder() - .uuid(UUID.randomUUID()) - .build(); - final HsOfficeContactRealEntity givenInitialContact = HsOfficeContactRealEntity.builder() - .uuid(UUID.randomUUID()) - .build(); + lenient().when(emw.getReference(HsOfficePersonRealEntity.class, PATCHED_HOLDER_UUID)).thenAnswer( + p -> PATCHED_HOLDER); + lenient().when(emw.getReference(HsOfficeContactRealEntity.class, PATCHED_CONTACT_UUID)).thenAnswer( + p -> PATCHED_CONTACT); + } @Override protected HsOfficeRelation newInitialEntity() { - final var entity = new HsOfficeRelationRbacEntity(); + final var entity = new HsOfficeRelationRealEntity(); entity.setUuid(INITIAL_RELATION_UUID); - entity.setType(HsOfficeRelationType.REPRESENTATIVE); - entity.setAnchor(givenInitialAnchorPerson); - entity.setHolder(givenInitialHolderPerson); - entity.setContact(givenInitialContact); + entity.setType(PARTNER); + entity.setAnchor(HsOfficePersonRealEntity.builder() + .uuid(INITIAL_ANCHOR_UUID) + .personType(LEGAL_PERSON) + .tradeName("Initial-Anchor-Tradename") + .build()); + entity.setHolder(HsOfficePersonRealEntity.builder() + .uuid(INITIAL_HOLDER_UUID) + .personType(NATURAL_PERSON) + .familyName("Initial-Holder-Family-Name") + .givenName("Initial-Holder-Given-Name") + .build()); + entity.setContact(HsOfficeContactRealEntity.builder() + .uuid(INITIAL_CONTACT_UUID) + .caption("Initial-Contact-Caption") + .build()); return entity; } @@ -68,24 +128,114 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase< } @Override - protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelation relation) { - return new HsOfficeRelationEntityPatcher(mapper, emw, relation); + protected HsOfficeRelationPatcher createPatcher(final HsOfficeRelation relation) { + return new HsOfficeRelationPatcher(mapper, emw, relation); } @Override protected Stream propertyTestDescriptors() { return Stream.of( new JsonNullableProperty<>( - "contact", + "holderUuid", + HsOfficeRelationPatchResource::setHolderUuid, + PATCHED_HOLDER_UUID, + HsOfficeRelation::setHolder, + PATCHED_HOLDER), + new SimpleProperty<>( + "holder", + HsOfficeRelationPatchResource::setHolder, + HOLDER_PATCH_RESOURCE, + HsOfficeRelation::setHolder, + withoutUuid(PATCHED_HOLDER)) + .notNullable(), + + new JsonNullableProperty<>( + "contactUuid", HsOfficeRelationPatchResource::setContactUuid, PATCHED_CONTACT_UUID, HsOfficeRelation::setContact, - newContact(PATCHED_CONTACT_UUID)) + PATCHED_CONTACT), + new SimpleProperty<>( + "contact", + HsOfficeRelationPatchResource::setContact, + CONTACT_PATCH_RESOURCE, + HsOfficeRelation::setContact, + withoutUuid(PATCHED_CONTACT)) .notNullable() ); } - static HsOfficeContactRealEntity newContact(final UUID uuid) { - return HsOfficeContactRealEntity.builder().uuid(uuid).build(); + @Override + protected void willPatchAllProperties() { + // this generic test does not work because either holder or holder.uuid can be set + assumeThat(true).isFalse(); + } + + @Test + void willThrowExceptionIfHolderAndHolderUuidAreGiven() { + // given + final var givenEntity = newInitialEntity(); + final var patchResource = newPatchResource(); + patchResource.setHolderUuid(JsonNullable.of(PATCHED_HOLDER_UUID)); + patchResource.setHolder(HOLDER_PATCH_RESOURCE); + + // when + final var exception = catchThrowable(() -> createPatcher(givenEntity).apply(patchResource)); + + // then + assertThat(exception).isInstanceOf(ValidationException.class) + .hasMessage("either \"holder\" or \"holder.uuid\" can be given, not both"); + } + + @Test + void willThrowExceptionIfContactAndContactUuidAreGiven() { + // given + final var givenEntity = newInitialEntity(); + final var patchResource = newPatchResource(); + patchResource.setContactUuid(JsonNullable.of(PATCHED_CONTACT_UUID)); + patchResource.setContact(CONTACT_PATCH_RESOURCE); + + // when + final var exception = catchThrowable(() -> createPatcher(givenEntity).apply(patchResource)); + + // then + assertThat(exception).isInstanceOf(ValidationException.class) + .hasMessage("either \"contact\" or \"contact.uuid\" can be given, not both"); + } + + @Test + void willPersistNewHolder() { + // given + final var givenEntity = newInitialEntity(); + final var patchResource = newPatchResource(); + patchResource.setHolder(HOLDER_PATCH_RESOURCE); + + // when + createPatcher(givenEntity).apply(patchResource); + + // then + verify(emw, times(1)).persist(givenEntity.getHolder()); + } + + @Test + void willPersistNewContact() { + // given + final var givenEntity = newInitialEntity(); + final var patchResource = newPatchResource(); + patchResource.setContact(CONTACT_PATCH_RESOURCE); + + // when + createPatcher(givenEntity).apply(patchResource); + + // then + verify(emw, times(1)).persist(givenEntity.getContact()); + } + + private HsOfficePersonRealEntity withoutUuid(final HsOfficePersonRealEntity givenWithUuid) { + return givenWithUuid.toBuilder().uuid(null).build(); + } + + private HsOfficeContactRealEntity withoutUuid(final HsOfficeContactRealEntity givenWithUuid) { + return givenWithUuid.toBuilder().uuid(null).build(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java index c114809b..1db638df 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java @@ -101,22 +101,43 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase httpGet("/api/hs/office/partners/%{partnerNumber}") .expecting(OK).expecting(JSON).expectObject(), path("partnerRel.holder.tradeName").contains( - "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") + "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), + path("partnerRel.contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"), + path("partnerRel.contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"), + path("partnerRel.contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}") ); - // TODO.test: Verify the EX_PARTNER-Relation, once we fixed the anchor problem, see HsOfficePartnerController - // (net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerController.optionallyCreateExPartnerRelation) + verify( + "Verify the Ex-Partner-Relation", + () -> httpGet( + "/api/hs/office/relations?relationType=EX_PARTNER&personUuid=%{Person: %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}") + .expecting(OK).expecting(JSON).expectArrayElements(1), + path("[0].anchor.tradeName").contains( + "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") + ); verify( "Verify the Representative-Relation", () -> httpGet( - "/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: %{representativeGivenName} %{representativeFamilyName}}") + "/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}") .expecting(OK).expecting(JSON).expectArrayElements(1), path("[0].anchor.tradeName").contains( "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), - path("[0].holder.familyName").contains("%{representativeFamilyName}") + path("[0].holder.familyName").contains("%{representativeFamilyName}"), + path("[0].contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"), + path("[0].contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"), + path("[0].contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}") ); - // TODO.test: Verify Debitor, Membership, Coop-Shares and Coop-Assets once implemented + verify( + "Verify the Debitor-Relation", + () -> httpGet( + "/api/hs/office/debitors?partnerNumber=%{partnerNumber}") + .expecting(OK).expecting(JSON).expectArrayElements(1), + path("[0].debitorRel.anchor.tradeName").contains( + "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), + path("[0].debitorRel.holder.tradeName").contains( + "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java index 4a51932e..04ec0fe9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.scenarios.UseCase.HttpResponse; import java.util.function.Consumer; import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals; import static org.junit.jupiter.api.Assertions.fail; public class PathAssertion { @@ -27,6 +28,18 @@ public class PathAssertion { }; } + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Consumer lenientlyContainsJson(final String resolvableValue) { + return response -> { + try { + lenientlyEquals(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)).matches(response.getFromBody(path)) ; + } catch (final AssertionError e) { + // without this, the error message is often lacking important context + fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" ); + } + }; + } + public Consumer doesNotExist() { return response -> { try { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java index d28adb26..eb82d676 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java @@ -32,6 +32,12 @@ public class TemplateResolver { return jsonQuoted(value); } }, + JSON_OBJECT('§'){ + @Override + String convert(final Object value, final Resolver resolver) { + return jsonObject(value); + } + }, URI_ENCODED('&'){ @Override String convert(final Object value, final Resolver resolver) { @@ -213,4 +219,12 @@ public class TemplateResolver { default -> "\"" + value + "\""; }; } + + private static String jsonObject(final Object value) { + return switch (value) { + case null -> null; + case String string -> "{" + string.replace("\n", " ") + "}"; + default -> throw new IllegalArgumentException("can not format " + value.getClass() + " (" + value + ") as JSON object"); + }; + } }