From 00174e4c4ab32e92ef48f87eb8fa1ca0cea4fb0d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 21 Sep 2022 09:44:09 +0200 Subject: [PATCH] implements HsOfficePersonController --- .../person/HsOfficePersonController.java | 118 ++++++ .../person/HsOfficePersonEntityPatch.java | 26 ++ .../hs-office/api-mappings.yaml | 2 + .../hs-office/hs-office-person-schemas.yaml | 64 ++- .../hs-office-persons-with-uuid.yaml | 83 ++++ .../hs-office/hs-office-persons.yaml | 56 +++ .../api-definition/hs-office/hs-office.yaml | 9 + ...sOfficePersonControllerAcceptanceTest.java | 401 ++++++++++++++++++ .../HsOfficePersonEntityPatchUnitTest.java | 153 +++++++ 9 files changed, 895 insertions(+), 17 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityPatch.java create mode 100644 src/main/resources/api-definition/hs-office/hs-office-persons-with-uuid.yaml create mode 100644 src/main/resources/api-definition/hs-office/hs-office-persons.yaml create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityPatchUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java new file mode 100644 index 00000000..a869e73e --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java @@ -0,0 +1,118 @@ +package net.hostsharing.hsadminng.hs.office.person; + +import net.hostsharing.hsadminng.Mapper; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonPatchResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonResource; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntityPatch; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; + +import java.util.List; +import java.util.UUID; + +import static net.hostsharing.hsadminng.Mapper.map; + +@RestController + +public class HsOfficePersonController implements HsOfficePersonsApi { + + @Autowired + private Context context; + + @Autowired + private HsOfficePersonRepository personRepo; + + @Override + @Transactional(readOnly = true) + public ResponseEntity> listPersons( + final String currentUser, + final String assumedRoles, + final String label) { + context.define(currentUser, assumedRoles); + + final var entities = personRepo.findPersonByOptionalNameLike(label); + + final var resources = Mapper.mapList(entities, HsOfficePersonResource.class); + return ResponseEntity.ok(resources); + } + + @Override + @Transactional + public ResponseEntity addPerson( + final String currentUser, + final String assumedRoles, + final HsOfficePersonInsertResource body) { + + context.define(currentUser, assumedRoles); + + final var entityToSave = map(body, HsOfficePersonEntity.class); + entityToSave.setUuid(UUID.randomUUID()); + + final var saved = personRepo.save(entityToSave); + + final var uri = + MvcUriComponentsBuilder.fromController(getClass()) + .path("/api/hs/office/persons/{id}") + .buildAndExpand(entityToSave.getUuid()) + .toUri(); + final var mapped = map(saved, HsOfficePersonResource.class); + return ResponseEntity.created(uri).body(mapped); + } + + @Override + @Transactional(readOnly = true) + public ResponseEntity getPersonByUuid( + final String currentUser, + final String assumedRoles, + final UUID personUuid) { + + context.define(currentUser, assumedRoles); + + final var result = personRepo.findByUuid(personUuid); + if (result.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(map(result.get(), HsOfficePersonResource.class)); + } + + @Override + @Transactional + public ResponseEntity deletePersonByUuid( + final String currentUser, + final String assumedRoles, + final UUID personUuid) { + context.define(currentUser, assumedRoles); + + final var result = personRepo.deleteByUuid(personUuid); + if (result == 0) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.noContent().build(); + } + + @Override + @Transactional + public ResponseEntity patchPerson( + final String currentUser, + final String assumedRoles, + final UUID personUuid, + final HsOfficePersonPatchResource body) { + + context.define(currentUser, assumedRoles); + + final var current = personRepo.findByUuid(personUuid).orElseThrow(); + + new HsOfficePersonEntityPatch(current).apply(body); + + final var saved = personRepo.save(current); + final var mapped = map(saved, HsOfficePersonResource.class); + return ResponseEntity.ok(mapped); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityPatch.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityPatch.java new file mode 100644 index 00000000..e06e2a1d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityPatch.java @@ -0,0 +1,26 @@ +package net.hostsharing.hsadminng.hs.office.person; + +import net.hostsharing.hsadminng.OptionalFromJson; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonPatchResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonTypeResource; + +import java.util.Optional; + +class HsOfficePersonEntityPatch { + + private final HsOfficePersonEntity entity; + + HsOfficePersonEntityPatch(final HsOfficePersonEntity entity) { + this.entity = entity; + } + + void apply(final HsOfficePersonPatchResource resource) { + Optional.ofNullable(resource.getPersonType()) + .map(HsOfficePersonTypeResource::getValue) + .map(HsOfficePersonType::valueOf) + .ifPresent(entity::setPersonType); + OptionalFromJson.of(resource.getTradeName()).ifPresent(entity::setTradeName); + OptionalFromJson.of(resource.getFamilyName()).ifPresent(entity::setFamilyName); + OptionalFromJson.of(resource.getGivenName()).ifPresent(entity::setGivenName); + } +} diff --git a/src/main/resources/api-definition/hs-office/api-mappings.yaml b/src/main/resources/api-definition/hs-office/api-mappings.yaml index fb0b9424..d3ade71b 100644 --- a/src/main/resources/api-definition/hs-office/api-mappings.yaml +++ b/src/main/resources/api-definition/hs-office/api-mappings.yaml @@ -16,3 +16,5 @@ map: null: org.openapitools.jackson.nullable.JsonNullable /api/hs/office/contacts/{contactUUID}: null: org.openapitools.jackson.nullable.JsonNullable + /api/hs/office/persons/{personUUID}: + null: org.openapitools.jackson.nullable.JsonNullable diff --git a/src/main/resources/api-definition/hs-office/hs-office-person-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-person-schemas.yaml index a7df528a..5a3d4596 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-person-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-person-schemas.yaml @@ -3,16 +3,28 @@ components: schemas: - HsOfficePersonBase: + HsOfficePersonTypeValues: + - NATURAL # a human + - LEGAL # e.g. Corp., Inc., AG, GmbH, eG + - SOLE_REPRESENTATION # e.g. OHG, GbR + - JOINT_REPRESENTATION # e.g. community of heirs + + HsOfficePersonType: + type: string + enum: + - NATURAL # a human + - LEGAL # e.g. Corp., Inc., AG, GmbH, eG + - SOLE_REPRESENTATION # e.g. OHG, GbR + - JOINT_REPRESENTATION # e.g. community of heirs + + HsOfficePerson: type: object properties: - personType: + uuid: type: string - enum: - - NATURAL # a human - - LEGAL # e.g. Corp., Inc., AG, GmbH, eG - - SOLE_REPRESENTATION # e.g. OHG, GbR - - JOINT_REPRESENTATION # e.g. community of heirs + format: uuid + personType: + $ref: '#/components/schemas/HsOfficePersonType' tradeName: type: string givenName: @@ -20,14 +32,32 @@ components: familyName: type: string - HsOfficePerson: - allOf: - - type: object - properties: - uuid: - type: string - format: uuid - - $ref: '#/components/schemas/HsOfficePersonBase' + HsOfficePersonInsert: + type: object + properties: + personType: + $ref: '#/components/schemas/HsOfficePersonType' + tradeName: + type: string + givenName: + type: string + familyName: + type: string + required: + - personType - HsOfficePersonUpdate: - $ref: '#/components/schemas/HsOfficePersonBase' + HsOfficePersonPatch: + type: object + properties: + personType: + nullable: true + $ref: '#/components/schemas/HsOfficePersonType' + tradeName: + type: string + nullable: true + givenName: + type: string + nullable: true + familyName: + type: string + nullable: true diff --git a/src/main/resources/api-definition/hs-office/hs-office-persons-with-uuid.yaml b/src/main/resources/api-definition/hs-office/hs-office-persons-with-uuid.yaml new file mode 100644 index 00000000..4d550fc9 --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-persons-with-uuid.yaml @@ -0,0 +1,83 @@ +get: + tags: + - hs-office-persons + description: 'Fetch a single business person by its uuid, if visible for the current subject.' + operationId: getPersonByUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: personUUID + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the person to fetch. + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson' + + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +patch: + tags: + - hs-office-persons + description: 'Updates a single person by its uuid, if permitted for the current subject.' + operationId: patchPerson + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: personUUID + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + 'application/json': + schema: + $ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePersonPatch' + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +delete: + tags: + - hs-office-persons + description: 'Delete a single business person by its uuid, if permitted for the current subject.' + operationId: deletePersonByUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: personUUID + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the person to delete. + responses: + "204": + description: No Content + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + "404": + $ref: './error-responses.yaml#/components/responses/NotFound' diff --git a/src/main/resources/api-definition/hs-office/hs-office-persons.yaml b/src/main/resources/api-definition/hs-office/hs-office-persons.yaml new file mode 100644 index 00000000..3e6f0873 --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-persons.yaml @@ -0,0 +1,56 @@ +get: + summary: Returns a list of (optionally filtered) persons. + description: Returns the list of (optionally filtered) persons which are visible to the current user or any of it's assumed roles. + tags: + - hs-office-persons + operationId: listPersons + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: name + in: query + required: false + schema: + type: string + description: Prefix of label to filter the results. + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + $ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +post: + summary: Adds a new person. + tags: + - hs-office-persons + operationId: addPerson + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + requestBody: + content: + 'application/json': + schema: + $ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePersonInsert' + required: true + responses: + "201": + description: Created + content: + 'application/json': + schema: + $ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + "409": + $ref: './error-responses.yaml#/components/responses/Conflict' diff --git a/src/main/resources/api-definition/hs-office/hs-office.yaml b/src/main/resources/api-definition/hs-office/hs-office.yaml index 22da51a9..125ae698 100644 --- a/src/main/resources/api-definition/hs-office/hs-office.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office.yaml @@ -25,3 +25,12 @@ paths: /api/hs/office/contacts/{contactUUID}: $ref: "./hs-office-contacts-with-uuid.yaml" + + # Persons + + /api/hs/office/persons: + $ref: "./hs-office-persons.yaml" + + /api/hs/office/persons/{personUUID}: + $ref: "./hs-office-persons-with-uuid.yaml" + diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java new file mode 100644 index 00000000..ef9e00da --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java @@ -0,0 +1,401 @@ +package net.hostsharing.hsadminng.hs.office.person; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.Accepts; +import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.test.JpaAttempt; +import org.apache.commons.lang3.RandomStringUtils; +import org.json.JSONException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; +import static net.hostsharing.test.JsonMatcher.lenientlyEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { HsadminNgApplication.class, JpaAttempt.class } +) +@Transactional +class HsOfficePersonControllerAcceptanceTest { + + @LocalServerPort + private Integer port; + + @Autowired + Context context; + + @Autowired + Context contextMock; + + @Autowired + HsOfficePersonRepository personRepo; + + @Autowired + JpaAttempt jpaAttempt; + + Set tempPersonUuids = new HashSet<>(); + + @Nested + @Accepts({ "Person:F(Find)" }) + class ListPersons { + + @Test + void globalAdmin_withoutAssumedRoles_canViewAllPersons_ifNoCriteriaGiven() throws JSONException { + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/persons") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "personType": "JOINT_REPRESENTATION", + "tradeName": "Erben Bessler", + "givenName": "Bessler", + "familyName": "Mel" + }, + { + "personType": "LEGAL", + "tradeName": "First Impressions GmbH", + "givenName": null, + "familyName": null + }, + { + "personType": "SOLE_REPRESENTATION", + "tradeName": "Ostfriesische Kuhhandel OHG", + "givenName": null, + "familyName": null + }, + { + "personType": "NATURAL", + "tradeName": null, + "givenName": "Smith", + "familyName": "Peter" + }, + { + "personType": "LEGAL", + "tradeName": "Rockshop e.K.", + "givenName": "Miller", + "familyName": "Sandra" + } + ] + """ + )); + // @formatter:on + } + } + + @Nested + @Accepts({ "Person:C(Create)" }) + class AddPerson { + + @Test + void globalAdmin_withoutAssumedRole_canAddPerson() { + + context.define("superuser-alex@hostsharing.net"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "personType": "NATURAL", + "familyName": "Tester", + "givenName": "Testi" + } + """) + .port(port) + .when() + .post("http://localhost/api/hs/office/persons") + .then().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("uuid", isUuidValid()) + .body("personType", is("NATURAL")) + .body("familyName", is("Tester")) + .body("givenName", is("Testi")) + .header("Location", startsWith("http://localhost")) + .extract().header("Location"); // @formatter:on + + // finally, the new person can be accessed under the generated UUID + final var newUserUuid = toCleanup(UUID.fromString( + location.substring(location.lastIndexOf('/') + 1))); + assertThat(newUserUuid).isNotNull(); + } + } + + @Nested + @Accepts({ "Person:R(Read)" }) + class GetPerson { + + @Test + void globalAdmin_withoutAssumedRole_canGetArbitraryPerson() { + context.define("superuser-alex@hostsharing.net"); + final var givenPersonUuid = personRepo.findPersonByOptionalNameLike("Erben").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/persons/" + givenPersonUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "tradeName": "Erben Bessler" + } + """)); // @formatter:on + } + + @Test + @Accepts({ "Person:X(Access Control)" }) + void normalUser_canNotGetUnrelatedPerson() { + context.define("superuser-alex@hostsharing.net"); + final var givenPersonUuid = personRepo.findPersonByOptionalNameLike("Erben").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .get("http://localhost/api/hs/office/persons/" + givenPersonUuid) + .then().log().body().assertThat() + .statusCode(404); // @formatter:on + } + + @Test + @Accepts({ "Person:X(Access Control)" }) + void personOwnerUser_canGetRelatedPerson() { + context.define("superuser-alex@hostsharing.net"); + final var givenPersonUuid = personRepo.findPersonByOptionalNameLike("Erben").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "person-ErbenBesslerMelBessler@example.com") + .port(port) + .when() + .get("http://localhost/api/hs/office/persons/" + givenPersonUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "personType": "JOINT_REPRESENTATION", + "tradeName": "Erben Bessler", + "givenName": "Bessler", + "familyName": "Mel" + } + """)); // @formatter:on + } + } + + @Nested + @Accepts({ "Person:U(Update)" }) + class PatchPerson { + + @Test + void globalAdmin_withoutAssumedRole_canPatchAllPropertiesOfArbitraryPerson() { + + context.define("superuser-alex@hostsharing.net"); + final var givenPerson = givenSomeTemporaryPersonCreatedBy("selfregistered-test-user@hostsharing.org"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "personType": "JOINT_REPRESENTATION", + "tradeName": "Patched Trade Name", + "familyName": "Patched Family Name", + "givenName": "Patched Given Name" + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/office/persons/" + givenPerson.getUuid()) + .then().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("uuid", isUuidValid()) + .body("personType", is("JOINT_REPRESENTATION")) + .body("tradeName", is("Patched Trade Name")) + .body("familyName", is("Patched Family Name")) + .body("givenName", is("Patched Given Name")); + // @formatter:on + + // finally, the person is actually updated + context.define("superuser-alex@hostsharing.net"); + assertThat(personRepo.findByUuid(givenPerson.getUuid())).isPresent().get() + .matches(person -> { + assertThat(person.getPersonType()).isEqualTo(HsOfficePersonType.JOINT_REPRESENTATION); + assertThat(person.getTradeName()).isEqualTo("Patched Trade Name"); + assertThat(person.getFamilyName()).isEqualTo("Patched Family Name"); + assertThat(person.getGivenName()).isEqualTo("Patched Given Name"); + return true; + }); + } + + @Test + void globalAdmin_withoutAssumedRole_canPatchPartialPropertiesOfArbitraryPerson() { + + context.define("superuser-alex@hostsharing.net"); + final var givenPerson = givenSomeTemporaryPersonCreatedBy("selfregistered-test-user@hostsharing.org"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "familyName": "Patched Family Name", + "givenName": "Patched Given Name" + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/office/persons/" + givenPerson.getUuid()) + .then().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("uuid", isUuidValid()) + .body("personType", is(givenPerson.getPersonType().toString())) + .body("tradeName", is(givenPerson.getTradeName())) + .body("familyName", is("Patched Family Name")) + .body("givenName", is("Patched Given Name")); + // @formatter:on + + // finally, the person is actually updated + assertThat(personRepo.findByUuid(givenPerson.getUuid())).isPresent().get() + .matches(person -> { + assertThat(person.getPersonType()).isEqualTo(givenPerson.getPersonType()); + assertThat(person.getTradeName()).isEqualTo(givenPerson.getTradeName()); + assertThat(person.getFamilyName()).isEqualTo("Patched Family Name"); + assertThat(person.getGivenName()).isEqualTo("Patched Given Name"); + return true; + }); + } + } + + @Nested + @Accepts({ "Person:D(Delete)" }) + class DeletePerson { + + @Test + void globalAdmin_withoutAssumedRole_canDeleteArbitraryPerson() { + context.define("superuser-alex@hostsharing.net"); + final var givenPerson = givenSomeTemporaryPersonCreatedBy("selfregistered-test-user@hostsharing.org"); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .delete("http://localhost/api/hs/office/persons/" + givenPerson.getUuid()) + .then().log().body().assertThat() + .statusCode(204); // @formatter:on + + // then the given person is gone + assertThat(personRepo.findByUuid(givenPerson.getUuid())).isEmpty(); + } + + @Test + @Accepts({ "Person:X(Access Control)" }) + void personOwner_canDeleteRelatedPerson() { + final var givenPerson = givenSomeTemporaryPersonCreatedBy("selfregistered-test-user@hostsharing.org"); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-test-user@hostsharing.org") + .port(port) + .when() + .delete("http://localhost/api/hs/office/persons/" + givenPerson.getUuid()) + .then().log().body().assertThat() + .statusCode(204); // @formatter:on + + // then the given person is still there + assertThat(personRepo.findByUuid(givenPerson.getUuid())).isEmpty(); + } + + @Test + @Accepts({ "Person:X(Access Control)" }) + void normalUser_canNotDeleteUnrelatedPerson() { + context.define("superuser-alex@hostsharing.net"); + final var givenPerson = givenSomeTemporaryPersonCreatedBy("selfregistered-test-user@hostsharing.org"); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .delete("http://localhost/api/hs/office/persons/" + givenPerson.getUuid()) + .then().log().body().assertThat() + .statusCode(404); // unrelated user cannot even view the person + // @formatter:on + + // then the given person is still there + assertThat(personRepo.findByUuid(givenPerson.getUuid())).isNotEmpty(); + } + } + + private HsOfficePersonEntity givenSomeTemporaryPersonCreatedBy(final String creatingUser) { + return jpaAttempt.transacted(() -> { + context.define(creatingUser); + final var newPerson = HsOfficePersonEntity.builder() + .uuid(UUID.randomUUID()) + .personType(HsOfficePersonType.LEGAL) + .tradeName("Temp " + Context.getCallerMethodNameFromStackFrame(2)) + .familyName(RandomStringUtils.randomAlphabetic(10) + "@example.org") + .givenName("Given Name " + RandomStringUtils.randomAlphabetic(10)) + .build(); + + toCleanup(newPerson.getUuid()); + + return personRepo.save(newPerson); + }).assertSuccessful().returnedValue(); + } + + private UUID toCleanup(final UUID tempPersonUuid) { + tempPersonUuids.add(tempPersonUuid); + return tempPersonUuid; + } + + @BeforeEach + @AfterEach + void cleanup() { + tempPersonUuids.forEach(uuid -> { + jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net", null); + System.out.println("DELETING temporary person: " + uuid); + final var entity = personRepo.findByUuid(uuid); + final var count = personRepo.deleteByUuid(uuid); + System.out.println("DELETED temporary person: " + uuid + (count > 0 ? " successful" : " failed") + + (" (" + entity.map(HsOfficePersonEntity::getDisplayName).orElse("null") + ")")); + }).assertSuccessful(); + }); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityPatchUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityPatchUnitTest.java new file mode 100644 index 00000000..d0f93f50 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityPatchUnitTest.java @@ -0,0 +1,153 @@ +package net.hostsharing.hsadminng.hs.office.person; + +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonPatchResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonTypeResource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.openapitools.jackson.nullable.JsonNullable; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +// TODO: there must be an easier way to test such patch classes +class HsOfficePersonEntityPatchUnitTest { + + private static final UUID INITIAL_PERSON_UUID = UUID.randomUUID(); + final HsOfficePersonEntity givenPerson = new HsOfficePersonEntity(); + final HsOfficePersonPatchResource patchResource = new HsOfficePersonPatchResource(); + + private final HsOfficePersonEntityPatch hsOfficePersonEntityPatch = + new HsOfficePersonEntityPatch(givenPerson); + + { + givenPerson.setUuid(INITIAL_PERSON_UUID); + givenPerson.setPersonType(HsOfficePersonType.LEGAL); + givenPerson.setTradeName("initial@example.org"); + givenPerson.setFamilyName("initial postal address"); + givenPerson.setGivenName("+01 100 123456789"); + } + + @Test + void willPatchAllProperties() { + // given + patchResource.setPersonType(HsOfficePersonTypeResource.NATURAL); + patchResource.setTradeName(JsonNullable.of("patched@example.org")); + patchResource.setFamilyName(JsonNullable.of("patched postal address")); + patchResource.setGivenName(JsonNullable.of("+01 200 987654321")); + + // when + hsOfficePersonEntityPatch.apply(patchResource); + + // then + new HsOfficePersonEntityMatcher() + .withPatchedPersonType(HsOfficePersonType.NATURAL) + .withPatchedTradeName("patched@example.org") + .withPatchedFamilyName("patched postal address") + .withPatchedGivenName("+01 200 987654321") + .matches(givenPerson); + } + + @ParameterizedTest + @EnumSource(HsOfficePersonTypeResource.class) + void willPatchOnlyPersonTypeProperty(final HsOfficePersonTypeResource patchedValue) { + // given + patchResource.setPersonType(patchedValue); + + // when + hsOfficePersonEntityPatch.apply(patchResource); + + // then + new HsOfficePersonEntityMatcher() + .withPatchedPersonType(HsOfficePersonType.valueOf(patchedValue.getValue())) + .matches(givenPerson); + } + + @ParameterizedTest + @ValueSource(strings = { "patched@example.org" }) + @NullSource + void willPatchOnlyTradeNameProperty(final String patchedValue) { + // given + patchResource.setTradeName(JsonNullable.of(patchedValue)); + + // when + hsOfficePersonEntityPatch.apply(patchResource); + + // then + new HsOfficePersonEntityMatcher() + .withPatchedTradeName(patchedValue) + .matches(givenPerson); + } + + @ParameterizedTest + @ValueSource(strings = { "patched postal address" }) + @NullSource + void willPatchOnlyFamilyNameProperty(final String patchedValue) { + // given + patchResource.setFamilyName(JsonNullable.of(patchedValue)); + + // when + hsOfficePersonEntityPatch.apply(patchResource); + + // then + new HsOfficePersonEntityMatcher() + .withPatchedFamilyName(patchedValue) + .matches(givenPerson); + } + + @ParameterizedTest + @ValueSource(strings = { "+01 200 987654321" }) + @NullSource + void willPatchOnlyGivenNameProperty(final String patchedValue) { + // given + patchResource.setGivenName(JsonNullable.of(patchedValue)); + + // when + hsOfficePersonEntityPatch.apply(patchResource); + + // then + new HsOfficePersonEntityMatcher() + .withPatchedGivenName(patchedValue) + .matches(givenPerson); + } + + private static class HsOfficePersonEntityMatcher { + + private HsOfficePersonType expectedPersonType = HsOfficePersonType.LEGAL; + private String expectedTradeName = "initial@example.org"; + private String expectedFamilyName = "initial postal address"; + + private String expectedGivenName = "+01 100 123456789"; + + HsOfficePersonEntityMatcher withPatchedPersonType(final HsOfficePersonType patchedPersonType) { + expectedPersonType = patchedPersonType; + return this; + } + + HsOfficePersonEntityMatcher withPatchedTradeName(final String patchedTradeName) { + expectedTradeName = patchedTradeName; + return this; + } + + HsOfficePersonEntityMatcher withPatchedFamilyName(final String patchedFamilyName) { + expectedFamilyName = patchedFamilyName; + return this; + } + + HsOfficePersonEntityMatcher withPatchedGivenName(final String patchedGivenName) { + expectedGivenName = patchedGivenName; + return this; + } + + void matches(final HsOfficePersonEntity givenPerson) { + + assertThat(givenPerson.getPersonType()).isEqualTo(expectedPersonType); + assertThat(givenPerson.getTradeName()).isEqualTo(expectedTradeName); + assertThat(givenPerson.getFamilyName()).isEqualTo(expectedFamilyName); + assertThat(givenPerson.getGivenName()).isEqualTo(expectedGivenName); + } + } +}