From 863f0e281167902f86dd24b613195a986bfc9167 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 20 Sep 2022 14:17:12 +0200 Subject: [PATCH] implements HsOfficeContactController --- .../hsadminng/context/Context.java | 6 +- .../contact/HsOfficeContactController.java | 117 ++++++ .../contact/HsOfficeContactEntityPatch.java | 20 + .../hs-office/api-mappings.yaml | 2 + .../hs-office/hs-office-contact-schemas.yaml | 45 ++- .../hs-office-contacts-with-uuid.yaml | 83 ++++ .../hs-office/hs-office-contacts.yaml | 56 +++ .../hs-office/hs-office-partners.yaml | 2 +- .../api-definition/hs-office/hs-office.yaml | 11 + ...OfficeContactControllerAcceptanceTest.java | 377 ++++++++++++++++++ ...fficeContactRepositoryIntegrationTest.java | 2 + 11 files changed, 705 insertions(+), 16 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatch.java create mode 100644 src/main/resources/api-definition/hs-office/hs-office-contacts-with-uuid.yaml create mode 100644 src/main/resources/api-definition/hs-office/hs-office-contacts.yaml create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/context/Context.java b/src/main/java/net/hostsharing/hsadminng/context/Context.java index 79f1f566..b0c63fb4 100644 --- a/src/main/java/net/hostsharing/hsadminng/context/Context.java +++ b/src/main/java/net/hostsharing/hsadminng/context/Context.java @@ -95,11 +95,11 @@ public class Context { .getSingleResult(); } - private static String getCallerMethodNameFromStack() { + public static String getCallerMethodNameFromStackFrame(final int skipFrames) { final Optional caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) .walk(frames -> frames - .skip(2) + .skip(skipFrames) .filter(c -> c.getDeclaringClass() != Context.class) .filter(c -> c.getDeclaringClass() .getPackageName() @@ -115,7 +115,7 @@ public class Context { if (isRequestScopeAvailable()) { return request.getMethod() + " " + request.getRequestURI(); } else { - return getCallerMethodNameFromStack(); + return getCallerMethodNameFromStackFrame(2); } } 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 new file mode 100644 index 00000000..6a99098e --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java @@ -0,0 +1,117 @@ +package net.hostsharing.hsadminng.hs.office.contact; + +import net.hostsharing.hsadminng.Mapper; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactResource; +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 HsOfficeContactController implements HsOfficeContactsApi { + + @Autowired + private Context context; + + @Autowired + private HsOfficeContactRepository contactRepo; + + @Override + @Transactional(readOnly = true) + public ResponseEntity> listContacts( + final String currentUser, + final String assumedRoles, + final String label) { + context.define(currentUser, assumedRoles); + + final var entities = contactRepo.findContactByOptionalLabelLike(label); + + final var resources = Mapper.mapList(entities, HsOfficeContactResource.class); + return ResponseEntity.ok(resources); + } + + @Override + @Transactional + public ResponseEntity addContact( + final String currentUser, + final String assumedRoles, + final HsOfficeContactInsertResource body) { + + context.define(currentUser, assumedRoles); + + final var entityToSave = map(body, HsOfficeContactEntity.class); + entityToSave.setUuid(UUID.randomUUID()); + + final var saved = contactRepo.save(entityToSave); + + final var uri = + MvcUriComponentsBuilder.fromController(getClass()) + .path("/api/hs/office/contacts/{id}") + .buildAndExpand(entityToSave.getUuid()) + .toUri(); + final var mapped = map(saved, HsOfficeContactResource.class); + return ResponseEntity.created(uri).body(mapped); + } + + @Override + @Transactional(readOnly = true) + public ResponseEntity getContactByUuid( + final String currentUser, + final String assumedRoles, + final UUID contactUuid) { + + context.define(currentUser, assumedRoles); + + final var result = contactRepo.findByUuid(contactUuid); + if (result.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(map(result.get(), HsOfficeContactResource.class)); + } + + @Override + @Transactional + public ResponseEntity deleteContactByUuid( + final String currentUser, + final String assumedRoles, + final UUID contactUuid) { + context.define(currentUser, assumedRoles); + + final var result = contactRepo.deleteByUuid(contactUuid); + if (result == 0) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.noContent().build(); + } + + @Override + @Transactional + public ResponseEntity patchContact( + final String currentUser, + final String assumedRoles, + final UUID contactUuid, + final HsOfficeContactPatchResource body) { + + context.define(currentUser, assumedRoles); + + final var current = contactRepo.findByUuid(contactUuid).orElseThrow(); + + new HsOfficeContactEntityPatch(current).apply(body); + + final var saved = contactRepo.save(current); + final var mapped = map(saved, HsOfficeContactResource.class); + return ResponseEntity.ok(mapped); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatch.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatch.java new file mode 100644 index 00000000..d79a6345 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatch.java @@ -0,0 +1,20 @@ +package net.hostsharing.hsadminng.hs.office.contact; + +import net.hostsharing.hsadminng.OptionalFromJson; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource; + +class HsOfficeContactEntityPatch { + + private final HsOfficeContactEntity entity; + + HsOfficeContactEntityPatch(final HsOfficeContactEntity entity) { + this.entity = entity; + } + + void apply(final HsOfficeContactPatchResource resource) { + OptionalFromJson.of(resource.getLabel()).ifPresent(entity::setLabel); + OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress); + OptionalFromJson.of(resource.getEmailAddresses()).ifPresent(entity::setEmailAddresses); + OptionalFromJson.of(resource.getPhoneNumbers()).ifPresent(entity::setPhoneNumbers); + } +} 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 14309def..fb0b9424 100644 --- a/src/main/resources/api-definition/hs-office/api-mappings.yaml +++ b/src/main/resources/api-definition/hs-office/api-mappings.yaml @@ -14,3 +14,5 @@ map: paths: /api/hs/office/partners/{partnerUUID}: null: org.openapitools.jackson.nullable.JsonNullable + /api/hs/office/contacts/{contactUUID}: + null: org.openapitools.jackson.nullable.JsonNullable diff --git a/src/main/resources/api-definition/hs-office/hs-office-contact-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-contact-schemas.yaml index eaa77d23..9d8dc76a 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-contact-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-contact-schemas.yaml @@ -3,7 +3,22 @@ components: schemas: - HsOfficeContactBase: + HsOfficeContact: + type: object + properties: + uuid: + type: string + format: uuid + label: + type: string + postalAddress: + type: string + emailAddresses: + type: string + phoneNumbers: + type: string + + HsOfficeContactInsert: type: object properties: label: @@ -14,15 +29,21 @@ components: type: string phoneNumbers: type: string + required: + - label - HsOfficeContact: - allOf: - - type: object - properties: - uuid: - type: string - format: uuid - - $ref: '#/components/schemas/HsOfficeContactBase' - - HsOfficeContactUpdate: - $ref: '#/components/schemas/HsOfficeContactBase' + HsOfficeContactPatch: + type: object + properties: + label: + type: string + nullable: true + postalAddress: + type: string + nullable: true + emailAddresses: + type: string + nullable: true + phoneNumbers: + type: string + nullable: true diff --git a/src/main/resources/api-definition/hs-office/hs-office-contacts-with-uuid.yaml b/src/main/resources/api-definition/hs-office/hs-office-contacts-with-uuid.yaml new file mode 100644 index 00000000..b61848f3 --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-contacts-with-uuid.yaml @@ -0,0 +1,83 @@ +get: + tags: + - hs-office-contacts + description: 'Fetch a single business contact by its uuid, if visible for the current subject.' + operationId: getContactByUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: contactUUID + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the contact to fetch. + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact' + + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +patch: + tags: + - hs-office-contacts + description: 'Updates a single business contact by its uuid, if permitted for the current subject.' + operationId: patchContact + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: contactUUID + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + 'application/json': + schema: + $ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContactPatch' + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +delete: + tags: + - hs-office-contacts + description: 'Delete a single business contact by its uuid, if permitted for the current subject.' + operationId: deleteContactByUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: contactUUID + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the contact 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-contacts.yaml b/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml new file mode 100644 index 00000000..89bf366a --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml @@ -0,0 +1,56 @@ +get: + summary: Returns a list of (optionally filtered) contacts. + description: Returns the list of (optionally filtered) contacts which are visible to the current user or any of it's assumed roles. + tags: + - hs-office-contacts + operationId: listContacts + 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-contact-schemas.yaml#/components/schemas/HsOfficeContact' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +post: + summary: Adds a new contact. + tags: + - hs-office-contacts + operationId: addContact + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + requestBody: + content: + 'application/json': + schema: + $ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContactInsert' + required: true + responses: + "201": + description: Created + content: + 'application/json': + schema: + $ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact' + "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-partners.yaml b/src/main/resources/api-definition/hs-office/hs-office-partners.yaml index b92bb897..80f356ab 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 @@ -12,7 +12,7 @@ get: required: false schema: type: string - description: Customer-prefix to filter the results. TODO + description: Prefix of name properties from person or contact to filter the results. responses: "200": description: OK 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 2d1cc4ed..22da51a9 100644 --- a/src/main/resources/api-definition/hs-office/hs-office.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office.yaml @@ -8,9 +8,20 @@ servers: paths: + # Partners + /api/hs/office/partners: $ref: "./hs-office-partners.yaml" /api/hs/office/partners/{partnerUUID}: $ref: "./hs-office-partners-with-uuid.yaml" + + # Contacts + + /api/hs/office/contacts: + $ref: "./hs-office-contacts.yaml" + + /api/hs/office/contacts/{contactUUID}: + $ref: "./hs-office-contacts-with-uuid.yaml" + diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java new file mode 100644 index 00000000..8bf0c71b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java @@ -0,0 +1,377 @@ +package net.hostsharing.hsadminng.hs.office.contact; + +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.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 HsOfficeContactControllerAcceptanceTest { + + @LocalServerPort + private Integer port; + + @Autowired + Context context; + + @Autowired + Context contextMock; + + @Autowired + HsOfficeContactRepository contactRepo; + + @Autowired + JpaAttempt jpaAttempt; + + Set tempContactUuids = new HashSet<>(); + + @Nested + @Accepts({ "Contact:F(Find)" }) + class ListContacts { + + @Test + void globalAdmin_withoutAssumedRoles_canViewAllContacts_ifNoCriteriaGiven() throws JSONException { + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/contacts") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { "label": "first contact" }, + { "label": "second contact" }, + { "label": "third contact" }, + { "label": "forth contact" }, + { "label": "fifth contact" }, + { "label": "sixth contact" }, + { "label": "eighth contact" }, + { "label": "ninth contact" }, + { "label": "tenth contact" }, + { "label": "eleventh contact" }, + { "label": "twelfth contact" } + ] + """ + )); + // @formatter:on + } + } + + @Nested + @Accepts({ "Contact:C(Create)" }) + class AddContact { + + @Test + void globalAdmin_withoutAssumedRole_canAddContact() { + + context.define("superuser-alex@hostsharing.net"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "label": "Test Contact", + "emailAddresses": "test@example.org" + } + """) + .port(port) + .when() + .post("http://localhost/api/hs/office/contacts") + .then().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("uuid", isUuidValid()) + .body("label", is("Test Contact")) + .body("emailAddresses", is("test@example.org")) + .header("Location", startsWith("http://localhost")) + .extract().header("Location"); // @formatter:on + + // finally, the new contact can be accessed under the generated UUID + final var newUserUuid = toCleanup(UUID.fromString( + location.substring(location.lastIndexOf('/') + 1))); + assertThat(newUserUuid).isNotNull(); + } + } + + @Nested + @Accepts({ "Contact:R(Read)" }) + class GetContact { + + @Test + void globalAdmin_withoutAssumedRole_canGetArbitraryContact() { + context.define("superuser-alex@hostsharing.net"); + final var givenContactUuid = contactRepo.findContactByOptionalLabelLike("first").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/contacts/" + givenContactUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "label": "first contact" + } + """)); // @formatter:on + } + + @Test + @Accepts({ "Contact:X(Access Control)" }) + void normalUser_canNotGetUnrelatedContact() { + context.define("superuser-alex@hostsharing.net"); + final var givenContactUuid = contactRepo.findContactByOptionalLabelLike("first").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .get("http://localhost/api/hs/office/contacts/" + givenContactUuid) + .then().log().body().assertThat() + .statusCode(404); // @formatter:on + } + + @Test + @Accepts({ "Contact:X(Access Control)" }) + void contactAdminUser_canGetRelatedContact() { + context.define("superuser-alex@hostsharing.net"); + final var givenContactUuid = contactRepo.findContactByOptionalLabelLike("first").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "customer-admin@firstcontact.example.com") + .port(port) + .when() + .get("http://localhost/api/hs/office/contacts/" + givenContactUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "label": "first contact", + "emailAddresses": "customer-admin@firstcontact.example.com", + "phoneNumbers": "+49 123 1234567" + } + """)); // @formatter:on + } + } + + @Nested + @Accepts({ "Contact:U(Update)" }) + class PatchContact { + + @Test + void globalAdmin_withoutAssumedRole_canPatchAllPropertiesOfArbitraryContact() { + + context.define("superuser-alex@hostsharing.net"); + final var givenContact = givenSomeTemporaryContactCreatedBy("selfregistered-test-user@hostsharing.org"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "label": "patched contact", + "emailAddresses": "patched@example.org", + "postalAddress": "Patched Address", + "phoneNumbers": "+01 100 123456" + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/office/contacts/" + givenContact.getUuid()) + .then().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("uuid", isUuidValid()) + .body("label", is("patched contact")) + .body("emailAddresses", is("patched@example.org")) + .body("postalAddress", is("Patched Address")) + .body("phoneNumbers", is("+01 100 123456")); + // @formatter:on + + // finally, the contact is actually updated + context.define("superuser-alex@hostsharing.net"); + assertThat(contactRepo.findByUuid(givenContact.getUuid())).isPresent().get() + .matches(person -> { + assertThat(person.getLabel()).isEqualTo("patched contact"); + assertThat(person.getEmailAddresses()).isEqualTo("patched@example.org"); + assertThat(person.getPostalAddress()).isEqualTo("Patched Address"); + assertThat(person.getPhoneNumbers()).isEqualTo("+01 100 123456"); + return true; + }); + } + + @Test + void globalAdmin_withoutAssumedRole_canPatchPartialPropertiesOfArbitraryContact() { + + context.define("superuser-alex@hostsharing.net"); + final var givenContact = givenSomeTemporaryContactCreatedBy("selfregistered-test-user@hostsharing.org"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "emailAddresses": "patched@example.org", + "phoneNumbers": "+01 100 123456" + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/office/contacts/" + givenContact.getUuid()) + .then().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("uuid", isUuidValid()) + .body("label", is(givenContact.getLabel())) + .body("emailAddresses", is("patched@example.org")) + .body("postalAddress", is(givenContact.getPostalAddress())) + .body("phoneNumbers", is("+01 100 123456")); + // @formatter:on + + // finally, the contact is actually updated + assertThat(contactRepo.findByUuid(givenContact.getUuid())).isPresent().get() + .matches(person -> { + assertThat(person.getLabel()).isEqualTo(givenContact.getLabel()); + assertThat(person.getEmailAddresses()).isEqualTo("patched@example.org"); + assertThat(person.getPostalAddress()).isEqualTo(givenContact.getPostalAddress()); + assertThat(person.getPhoneNumbers()).isEqualTo("+01 100 123456"); + return true; + }); + } + + } + + @Nested + @Accepts({ "Contact:D(Delete)" }) + class DeleteContact { + + @Test + void globalAdmin_withoutAssumedRole_canDeleteArbitraryContact() { + context.define("superuser-alex@hostsharing.net"); + final var givenContact = givenSomeTemporaryContactCreatedBy("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/contacts/" + givenContact.getUuid()) + .then().log().body().assertThat() + .statusCode(204); // @formatter:on + + // then the given contact is gone + assertThat(contactRepo.findByUuid(givenContact.getUuid())).isEmpty(); + } + + @Test + @Accepts({ "Contact:X(Access Control)" }) + void contactOwner_canDeleteRelatedContact() { + final var givenContact = givenSomeTemporaryContactCreatedBy("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/contacts/" + givenContact.getUuid()) + .then().log().body().assertThat() + .statusCode(204); // @formatter:on + + // then the given contact is still there + assertThat(contactRepo.findByUuid(givenContact.getUuid())).isEmpty(); + } + + @Test + @Accepts({ "Contact:X(Access Control)" }) + void normalUser_canNotDeleteUnrelatedContact() { + context.define("superuser-alex@hostsharing.net"); + final var givenContact = givenSomeTemporaryContactCreatedBy("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/contacts/" + givenContact.getUuid()) + .then().log().body().assertThat() + .statusCode(404); // unrelated user cannot even view the contact + // @formatter:on + + // then the given contact is still there + assertThat(contactRepo.findByUuid(givenContact.getUuid())).isNotEmpty(); + } + } + + private HsOfficeContactEntity givenSomeTemporaryContactCreatedBy(final String creatingUser) { + return jpaAttempt.transacted(() -> { + context.define(creatingUser); + final var newContact = HsOfficeContactEntity.builder() + .uuid(UUID.randomUUID()) + .label("Temp from " + Context.getCallerMethodNameFromStackFrame(1) ) + .emailAddresses(RandomStringUtils.randomAlphabetic(10) + "@example.org") + .postalAddress("Postal Address " + RandomStringUtils.randomAlphabetic(10)) + .phoneNumbers("+01 200 " + RandomStringUtils.randomNumeric(8)) + .build(); + + toCleanup(newContact.getUuid()); + + return contactRepo.save(newContact); + }).assertSuccessful().returnedValue(); + } + + private UUID toCleanup(final UUID tempContactUuid) { + tempContactUuids.add(tempContactUuid); + return tempContactUuid; + } + + @AfterEach + void cleanup() { + tempContactUuids.forEach(uuid -> { + jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net", null); + System.out.println("DELETING temporary contact: " + uuid); + final var count = contactRepo.deleteByUuid(uuid); + System.out.println("DELETED temporary contact: " + uuid + (count > 0 ? " successful" : " failed")); + }).assertSuccessful(); + }); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java index a981a714..b991fd4a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; import net.hostsharing.test.Array; import net.hostsharing.test.JpaAttempt; 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.modelmapper.internal.bytebuddy.utility.RandomString; @@ -270,6 +271,7 @@ class HsOfficeContactRepositoryIntegrationTest extends ContextBasedTest { }).assumeSuccessful().returnedValue(); } + @BeforeEach @AfterEach void cleanup() { context("superuser-alex@hostsharing.net", null);