update dependend relations when updating partner person #162

Merged
hsh-michaelhoennig merged 13 commits from feature/update-relations-when-updating-partner-person into master 2025-03-10 12:05:01 +01:00
12 changed files with 299 additions and 283 deletions
Showing only changes of commit 01c86fa782 - Show all commits

View File

@ -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<HsOfficeContactRbacEntity> 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<HsOfficeContactInsertResource, HsOfficeContactRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
};
}

View File

@ -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<E extends HsOfficeContact>
implements Converter<HsOfficeContactInsertResource, E> {
@Override
@SneakyThrows
public E convert(final MappingContext<HsOfficeContactInsertResource, E> 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;
}
}

View File

@ -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<HsOfficeContactRealEntity> 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,15 +196,18 @@ 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("""
// self-debitors of the old partner-person become self-debitors of the new partner person
em.createNativeQuery("""
UPDATE hs_office.relation
SET holderUuid = :newPartnerPersonUuid
WHERE type = 'DEBITOR' AND holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid
WHERE type = 'DEBITOR' AND
holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid
""")
.setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid())
.setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid())
.executeUpdate();
System.out.println(count); // FIXME: remove
// re-anchor all relations from the old partner person to the new partner persion
em.createNativeQuery("""
UPDATE hs_office.relation
SET anchorUuid = :newPartnerPersonUuid

View File

@ -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<HsOfficePartnerPatch
public void apply(final HsOfficePartnerPatchResource resource) {
if (resource.getPartnerRel() != null) {
new HsOfficeRelationEntityPatcher(mapper, em, entity.getPartnerRel()).apply(resource.getPartnerRel());
new HsOfficeRelationPatcher(mapper, em, entity.getPartnerRel()).apply(resource.getPartnerRel());
}
if (resource.getDetails() != null) {

View File

@ -9,13 +9,13 @@ import net.hostsharing.hsadminng.mapper.StrictMapper;
import jakarta.persistence.EntityManager;
import jakarta.validation.ValidationException;
public class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
public class HsOfficeRelationPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
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;

View File

@ -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(

View File

@ -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 {

View File

@ -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<Property> 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();
}
}

View File

@ -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<Property> 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();
}
}

View File

@ -101,22 +101,43 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
() -> 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}")
);
}
}

View File

@ -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<UseCase.HttpResponse> 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<HttpResponse> doesNotExist() {
return response -> {
try {

View File

@ -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");
};
}
}