From 9cd4525e2b812d6631f6c994dcebc2c5867c0eed Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 14 Sep 2022 12:16:44 +0200 Subject: [PATCH] create partner now taking existing contact+person uuids instead of complete (new) objects --- .../RestResponseEntityExceptionHandler.java | 8 + .../partner/HsOfficePartnerController.java | 55 +++---- .../hs-office/hs-office-partner-schemas.yaml | 17 +- .../hs-office-partners-with-uuid.yaml | 2 +- .../hs-office/hs-office-partners.yaml | 2 +- .../changelog/203-hs-office-contact-rbac.sql | 12 +- ...OfficePartnerControllerAcceptanceTest.java | 148 +++++++++++------- 7 files changed, 143 insertions(+), 101 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java index d8602d1f..a0570138 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java @@ -13,6 +13,7 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import java.time.LocalDateTime; +import java.util.NoSuchElementException; import java.util.Optional; @ControllerAdvice @@ -34,6 +35,13 @@ public class RestResponseEntityExceptionHandler return errorResponse(request, httpStatus(message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message); } + @ExceptionHandler(NoSuchElementException.class) + protected ResponseEntity handleNoSuchElementException( + final RuntimeException exc, final WebRequest request) { + final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage()); + return errorResponse(request, HttpStatus.NOT_FOUND, message); + } + @ExceptionHandler(Throwable.class) protected ResponseEntity handleOtherExceptions( final Throwable exc, final WebRequest request) { 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 aa053757..95f1845c 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 @@ -2,14 +2,9 @@ package net.hostsharing.hsadminng.hs.office.partner; import net.hostsharing.hsadminng.Mapper; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartnersApi; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactResource; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerUpdateResource; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonResource; -import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -18,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import java.util.List; +import java.util.NoSuchElementException; import java.util.UUID; import java.util.function.BiConsumer; @@ -59,34 +55,25 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { public ResponseEntity addPartner( final String currentUser, final String assumedRoles, - final HsOfficePartnerResource body) { + final HsOfficePartnerInsertResource body) { context.define(currentUser, assumedRoles); - if (body.getUuid() == null) { - body.setUuid(UUID.randomUUID()); - } + final var entityToSave = mapToHsOfficePartnerEntity(body); + entityToSave.setUuid(UUID.randomUUID()); + entityToSave.setContact(contactRepo.findByUuid(body.getContactUuid()).orElseThrow( + () -> new NoSuchElementException("cannot find contact uuid " + body.getContactUuid()) + )); + entityToSave.setPerson(personRepo.findByUuid(body.getPersonUuid()).orElseThrow( + () -> new NoSuchElementException("cannot find person uuid " + body.getPersonUuid()) + )); - final var entityToSave = map(body, HsOfficePartnerEntity.class); - if (entityToSave.getContact().getUuid() != null) { - contactRepo.findByUuid(entityToSave.getContact().getUuid()).ifPresent(entityToSave::setContact); - } else { - entityToSave.getContact().setUuid(UUID.randomUUID()); - entityToSave.setContact(contactRepo.save(entityToSave.getContact())); - } - if (entityToSave.getPerson().getUuid() != null) { - personRepo.findByUuid(entityToSave.getPerson().getUuid()).ifPresent(entityToSave::setPerson); - } else { - entityToSave.getPerson().setUuid(UUID.randomUUID()); - entityToSave.setPerson(personRepo.save(entityToSave.getPerson())); - } - final var saved = partnerRepo.save(entityToSave); final var uri = MvcUriComponentsBuilder.fromController(getClass()) .path("/api/hs/office/partners/{id}") - .buildAndExpand(body.getUuid()) + .buildAndExpand(entityToSave.getUuid()) .toUri(); final var mapped = map(saved, HsOfficePartnerResource.class, PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER); @@ -121,18 +108,22 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { final String currentUser, final String assumedRoles, final UUID partnerUuid, - final HsOfficePartnerUpdateResource body) { + final HsOfficePartnerPatchResource body) { return null; } - private final BiConsumer PARTNER_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { - entity.setPerson(map(resource.getPerson(), HsOfficePersonEntity.class)); - entity.setContact(map(resource.getContact(), HsOfficeContactEntity.class)); - }; - - private final BiConsumer PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { + final BiConsumer PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setPerson(map(entity.getPerson(), HsOfficePersonResource.class)); resource.setContact(map(entity.getContact(), HsOfficeContactResource.class)); }; + private HsOfficePartnerEntity mapToHsOfficePartnerEntity(final HsOfficePartnerInsertResource resource) { + final var entity = new HsOfficePartnerEntity(); + entity.setBirthday(resource.getBirthday()); + entity.setBirthName(resource.getBirthName()); + entity.setDateOfDeath(resource.getDateOfDeath()); + entity.setRegistrationNumber(resource.getRegistrationNumber()); + entity.setRegistrationOffice(resource.getRegistrationOffice()); + return entity; + } } diff --git a/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml index 02188f03..bf567274 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml @@ -32,7 +32,7 @@ components: $ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact' - $ref: '#/components/schemas/HsOfficePartnerBase' - HsOfficePartnerUpdate: + HsOfficePartnerPatch: allOf: - type: object properties: @@ -43,3 +43,18 @@ components: type: string format: uuid - $ref: '#/components/schemas/HsOfficePartnerBase' + + HsOfficePartnerInsert: + allOf: + - type: object + properties: + personUuid: + type: string + format: uuid + contactUuid: + type: string + format: uuid + - required: + - personUuid + - contactUuid + - $ref: '#/components/schemas/HsOfficePartnerBase' diff --git a/src/main/resources/api-definition/hs-office/hs-office-partners-with-uuid.yaml b/src/main/resources/api-definition/hs-office/hs-office-partners-with-uuid.yaml index 98418bfa..757f97f7 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-partners-with-uuid.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-partners-with-uuid.yaml @@ -42,7 +42,7 @@ patch: content: 'application/json': schema: - $ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartnerUpdate' + $ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartnerPatch' responses: "200": description: OK diff --git a/src/main/resources/api-definition/hs-office/hs-office-partners.yaml b/src/main/resources/api-definition/hs-office/hs-office-partners.yaml index e6cb944d..b92bb897 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-partners.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-partners.yaml @@ -39,7 +39,7 @@ post: content: 'application/json': schema: - $ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner' + $ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartnerInsert' required: true responses: "201": diff --git a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql index 9819077a..8967daf4 100644 --- a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql +++ b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql @@ -156,7 +156,7 @@ grant all privileges on hs_office_contact_rv to restricted; /** Instead of insert trigger function for hs_office_contact_rv. */ -create or replace function inserthsOfficeContact() +create or replace function insertHsOfficeContact() returns trigger language plpgsql as $$ declare @@ -173,11 +173,11 @@ $$; /* Creates an instead of insert trigger for the hs_office_contact_rv view. */ -create trigger inserthsOfficeContact_Trigger +create trigger insertHsOfficeContact_Trigger instead of insert on hs_office_contact_rv for each row -execute function inserthsOfficeContact(); +execute function insertHsOfficeContact(); --// -- ============================================================================ @@ -189,7 +189,7 @@ execute function inserthsOfficeContact(); Checks if the current subject (user / assumed role) has the permission to delete the row. */ -create or replace function deletehsOfficeContact() +create or replace function deleteHsOfficeContact() returns trigger language plpgsql as $$ begin @@ -204,11 +204,11 @@ end; $$; /* Creates an instead of delete trigger for the hs_office_contact_rv view. */ -create trigger deletehsOfficeContact_Trigger +create trigger deleteHsOfficeContact_Trigger instead of delete on hs_office_contact_rv for each row -execute function deletehsOfficeContact(); +execute function deleteHsOfficeContact(); --/ -- ============================================================================ diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java index 73807d84..a3765394 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java @@ -5,6 +5,8 @@ import io.restassured.http.ContentType; import net.hostsharing.hsadminng.Accepts; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.test.JpaAttempt; import org.json.JSONException; import org.junit.jupiter.api.AfterEach; @@ -20,7 +22,6 @@ import java.util.Set; import java.util.UUID; import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; -import static net.hostsharing.test.JsonBuilder.jsonObject; import static net.hostsharing.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; @@ -45,6 +46,12 @@ class HsOfficePartnerControllerAcceptanceTest { @Autowired HsOfficePartnerRepository partnerRepo; + @Autowired + HsOfficePersonRepository personRepo; + + @Autowired + HsOfficeContactRepository contactRepo; + @Autowired JpaAttempt jpaAttempt; @@ -90,70 +97,28 @@ class HsOfficePartnerControllerAcceptanceTest { @Accepts({ "Partner:C(Create)" }) class AddPartner { - private final static String NEW_PARTNER_JSON_WITHOUT_UUID = - """ - { - "person": { - "personType": "LEGAL", - "tradeName": "Test Corp.", - "givenName": null, - "familyName": null - }, - "contact": { - "label": "Test Corp.", - "postalAddress": "Test Corp.\\nTestweg 50\\n20001 Hamburg", - "emailAddresses": "office@example.com", - "phoneNumbers": "040 12345" - }, + @Test + void globalAdmin_withoutAssumedRole_canAddPartner_withGeneratedUuid() { + + context.define("superuser-alex@hostsharing.net"); + final var givenPerson = personRepo.findPersonByOptionalNameLike("Ostfriesische").get(0); + final var givenContact = contactRepo.findContactByOptionalLabelLike("forth").get(0); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "contactUuid": "%s", + "personUuid": "%s", "registrationOffice": "Registergericht Hamburg", "registrationNumber": "123456", "birthName": null, "birthday": null, "dateOfDeath": null } - """; - - @Test - void globalAdmin_withoutAssumedRole_canAddPartner_withExplicitUuid() { - - final var givenUUID = toCleanup(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")); - - final var location = RestAssured // @formatter:off - .given() - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(ContentType.JSON) - .body(jsonObject(NEW_PARTNER_JSON_WITHOUT_UUID) - .with("uuid", givenUUID.toString()).toString()) - .port(port) - .when() - .post("http://localhost/api/hs/office/partners") - .then().assertThat() - .statusCode(201) - .contentType(ContentType.JSON) - .body("uuid", is("3fa85f64-5717-4562-b3fc-2c963f66afa6")) - .body("registrationNumber", is("123456")) - .body("person.tradeName", is("Test Corp.")) - .body("contact.label", is("Test Corp.")) - .header("Location", startsWith("http://localhost")) - .extract().header("Location"); // @formatter:on - - // finally, the new partner can be accessed under the given UUID - final var newUserUuid = UUID.fromString( - location.substring(location.lastIndexOf('/') + 1)); - assertThat(newUserUuid).isEqualTo(givenUUID); - context.define("superuser-alex@hostsharing.net"); - assertThat(partnerRepo.findByUuid(newUserUuid)) - .hasValueSatisfying(c -> assertThat(c.getPerson().getTradeName()).isEqualTo("Test Corp.")); - } - - @Test - void globalAdmin_withoutAssumedRole_canAddPartner_withGeneratedUuid() { - - final var location = RestAssured // @formatter:off - .given() - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(ContentType.JSON) - .body(NEW_PARTNER_JSON_WITHOUT_UUID) + """.formatted(givenContact.getUuid(), givenPerson.getUuid())) .port(port) .when() .post("http://localhost/api/hs/office/partners") @@ -162,7 +127,8 @@ class HsOfficePartnerControllerAcceptanceTest { .contentType(ContentType.JSON) .body("uuid", isUuidValid()) .body("registrationNumber", is("123456")) - .body("person.tradeName", is("Test Corp.")) + .body("contact.label", is(givenContact.getLabel())) + .body("person.tradeName", is(givenPerson.getTradeName())) .header("Location", startsWith("http://localhost")) .extract().header("Location"); // @formatter:on @@ -171,6 +137,68 @@ class HsOfficePartnerControllerAcceptanceTest { location.substring(location.lastIndexOf('/') + 1))); assertThat(newUserUuid).isNotNull(); } + + @Test + void globalAdmin_canNotAddPartner_ifContactDoesNotExist() { + + context.define("superuser-alex@hostsharing.net"); + final var givenPerson = personRepo.findPersonByOptionalNameLike("Ostfriesische").get(0); + final var givenContactUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "contactUuid": "%s", + "personUuid": "%s", + "registrationOffice": "Registergericht Hamburg", + "registrationNumber": "123456", + "birthName": null, + "birthday": null, + "dateOfDeath": null + } + """.formatted(givenContactUuid, givenPerson.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/office/partners") + .then().log().all().assertThat() + .statusCode(404) + .body("message", is("cannot find contact uuid 3fa85f64-5717-4562-b3fc-2c963f66afa6")); + // @formatter:on + } + + @Test + void globalAdmin_canNotAddPartner_ifPersonDoesNotExist() { + + context.define("superuser-alex@hostsharing.net"); + final var givenPersonUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"); + final var givenContact = contactRepo.findContactByOptionalLabelLike("forth").get(0); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "contactUuid": "%s", + "personUuid": "%s", + "registrationOffice": "Registergericht Hamburg", + "registrationNumber": "123456", + "birthName": null, + "birthday": null, + "dateOfDeath": null + } + """.formatted(givenContact.getUuid(), givenPersonUuid)) + .port(port) + .when() + .post("http://localhost/api/hs/office/partners") + .then().log().all().assertThat() + .statusCode(404) + .body("message", is("cannot find person uuid 3fa85f64-5717-4562-b3fc-2c963f66afa6")); + // @formatter:on + } } @Nested