From a3d2dd3db13771a4db9752b91066d1ffa465cc73 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 14 Sep 2022 09:24:19 +0200 Subject: [PATCH] HsOfficePartnerControllerAcceptanceTest against real repo+db --- .../partner/HsOfficePartnerController.java | 83 ++++------ .../changelog/203-hs-office-contact-rbac.sql | 6 +- ...OfficePartnerControllerAcceptanceTest.java | 144 +++++++++++++----- .../hostsharing/test/IsValidUuidMatcher.java | 23 ++- .../net/hostsharing/test/JsonMatcher.java | 64 ++++++++ 5 files changed, 223 insertions(+), 97 deletions(-) create mode 100644 src/test/java/net/hostsharing/test/JsonMatcher.java 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 7638f67a..aa053757 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -3,12 +3,14 @@ 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.person.HsOfficePersonRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -31,45 +33,21 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { @Autowired private HsOfficePartnerRepository partnerRepo; + @Autowired + private HsOfficePersonRepository personRepo; + + @Autowired + private HsOfficeContactRepository contactRepo; + @Override @Transactional(readOnly = true) public ResponseEntity> listPartners( final String currentUser, final String assumedRoles, final String name) { - // TODO.feat: context.define(currentUser, assumedRoles); + context.define(currentUser, assumedRoles); - // TODO.feat: final var entities = partnerRepo.findPartnerByOptionalNameLike(name); - - final var entities = List.of( - HsOfficePartnerEntity.builder() - .uuid(UUID.randomUUID()) - .person(HsOfficePersonEntity.builder() - .tradeName("Ixx AG") - .build()) - .contact(HsOfficeContactEntity.builder() - .label("Ixx AG") - .build()) - .build(), - HsOfficePartnerEntity.builder() - .uuid(UUID.randomUUID()) - .person(HsOfficePersonEntity.builder() - .tradeName("Ypsilon GmbH") - .build()) - .contact(HsOfficeContactEntity.builder() - .label("Ypsilon GmbH") - .build()) - .build(), - HsOfficePartnerEntity.builder() - .uuid(UUID.randomUUID()) - .person(HsOfficePersonEntity.builder() - .tradeName("Zett OHG") - .build()) - .contact(HsOfficeContactEntity.builder() - .label("Zett OHG") - .build()) - .build() - ); + final var entities = partnerRepo.findPartnerByOptionalNameLike(name); final var resources = Mapper.mapList(entities, HsOfficePartnerResource.class, PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER); @@ -83,14 +61,27 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { final String assumedRoles, final HsOfficePartnerResource body) { - // TODO.feat: context.define(currentUser, assumedRoles); + context.define(currentUser, assumedRoles); if (body.getUuid() == null) { body.setUuid(UUID.randomUUID()); } - // TODO.feat: final var saved = partnerRepo.save(map(body, HsOfficePartnerEntity.class)); - final var saved = map(body, HsOfficePartnerEntity.class, PARTNER_RESOURCE_TO_ENTITY_POSTMAPPER); + 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()) @@ -103,37 +94,29 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { } @Override + @Transactional(readOnly = true) public ResponseEntity getPartnerByUuid( final String currentUser, final String assumedRoles, final UUID partnerUuid) { - // TODO.feat: context.define(currentUser, assumedRoles); + context.define(currentUser, assumedRoles); - // TODO.feat: final var result = partnerRepo.findByUuid(partnerUuid); - final var result = - partnerUuid.equals(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")) ? null : - HsOfficePartnerEntity.builder() - .uuid(UUID.randomUUID()) - .person(HsOfficePersonEntity.builder() - .tradeName("Ixx AG") - .build()) - .contact(HsOfficeContactEntity.builder() - .label("Ixx AG") - .build()) - .build(); - if (result == null) { + final var result = partnerRepo.findByUuid(partnerUuid); + if (result.isEmpty()) { return ResponseEntity.notFound().build(); } - return ResponseEntity.ok(map(result, HsOfficePartnerResource.class, PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER)); + return ResponseEntity.ok(map(result.get(), HsOfficePartnerResource.class, PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER)); } @Override + @Transactional public ResponseEntity deletePartnerByUuid(final String currentUser, final String assumedRoles, final UUID userUuid) { return null; } @Override + @Transactional public ResponseEntity updatePartner( final String currentUser, final String assumedRoles, 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 5b5c1418..9819077a 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 @@ -45,7 +45,7 @@ end; $$; Creates the roles and their assignments for a new contact for the AFTER INSERT TRIGGER. */ -create or replace function createRbacRolesForhsOfficeContact() +create or replace function createRbacRolesForHsOfficeContact() returns trigger language plpgsql strict as $$ @@ -88,11 +88,11 @@ end; $$; An AFTER INSERT TRIGGER which creates the role structure for a new customer. */ -create trigger createRbacRolesForhsOfficeContact_Trigger +create trigger createRbacRolesForHsOfficeContact_Trigger after insert on hs_office_contact for each row -execute procedure createRbacRolesForhsOfficeContact(); +execute procedure createRbacRolesForHsOfficeContact(); --// 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 c1c42195..56425d7c 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,9 @@ 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.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; @@ -12,16 +15,20 @@ 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.JsonBuilder.jsonObject; +import static net.hostsharing.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = HsadminNgApplication.class + classes = { HsadminNgApplication.class, JpaAttempt.class } ) @Transactional class HsOfficePartnerControllerAcceptanceTest { @@ -34,31 +41,47 @@ class HsOfficePartnerControllerAcceptanceTest { @Autowired Context contextMock; + @Autowired - HsOfficePartnerRepository partnerRepository; + HsOfficePartnerRepository partnerRepo; + + @Autowired + JpaAttempt jpaAttempt; + + Set tempPartnerUuids = new HashSet<>(); @Nested @Accepts({ "Partner:F(Find)" }) class ListPartners { @Test - void testHostsharingAdmin_withoutAssumedRoles_canViewAllPartners_ifNoCriteriaGiven() { + void globalAdmin_withoutAssumedRoles_canViewAllPartners_ifNoCriteriaGiven() throws JSONException { + RestAssured // @formatter:off .given() - .header("current-user", "mike@hostsharing.net") + .header("current-user", "alex@hostsharing.net") .port(port) .when() .get("http://localhost/api/hs/office/partners") - .then().assertThat() + .then().log().all().assertThat() .statusCode(200) .contentType("application/json") - .body("[0].contact.label", is("Ixx AG")) - .body("[0].person.tradeName", is("Ixx AG")) - .body("[1].contact.label", is("Ypsilon GmbH")) - .body("[1].person.tradeName", is("Ypsilon GmbH")) - .body("[2].contact.label", is("Zett OHG")) - .body("[2].person.tradeName", is("Zett OHG")) - .body("size()", greaterThanOrEqualTo(3)); + .body("", lenientlyEquals(""" + [ + { + "person": { "tradeName": "First Impressions GmbH" }, + "contact": { "label": "first contact" } + }, + { + "person": { "tradeName": "Ostfriesische Kuhhandel OHG" }, + "contact": { "label": "third contact" } + }, + { + "person": { "tradeName": "Rockshop e.K." }, + "contact": { "label": "second contact" } + } + ] + """)); // @formatter:on } } @@ -91,13 +114,13 @@ class HsOfficePartnerControllerAcceptanceTest { """; @Test - void hostsharingAdmin_withoutAssumedRole_canAddPartner_withExplicitUuid() { + void globalAdmin_withoutAssumedRole_canAddPartner_withExplicitUuid() { - final var givenUUID = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"); + final var givenUUID = toCleanup(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")); final var location = RestAssured // @formatter:off .given() - .header("current-user", "mike@hostsharing.net") + .header("current-user", "alex@hostsharing.net") .contentType(ContentType.JSON) .body(jsonObject(NEW_PARTNER_JSON_WITHOUT_UUID) .with("uuid", givenUUID.toString()).toString()) @@ -110,24 +133,25 @@ class HsOfficePartnerControllerAcceptanceTest { .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 viewed by its own admin + // 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); - // TODO.feat: context.define("partner-admin@ttt.example.com"); - // assertThat(partnerRepository.findByUuid(newUserUuid)) - // .hasValueSatisfying(c -> assertThat(c.getPerson().getTradeName()).isEqualTo("Test Corp.")); + context.define("alex@hostsharing.net"); + assertThat(partnerRepo.findByUuid(newUserUuid)) + .hasValueSatisfying(c -> assertThat(c.getPerson().getTradeName()).isEqualTo("Test Corp.")); } @Test - void hostsharingAdmin_withoutAssumedRole_canAddPartner_withGeneratedUuid() { + void globalAdmin_withoutAssumedRole_canAddPartner_withGeneratedUuid() { final var location = RestAssured // @formatter:off .given() - .header("current-user", "mike@hostsharing.net") + .header("current-user", "alex@hostsharing.net") .contentType(ContentType.JSON) .body(NEW_PARTNER_JSON_WITHOUT_UUID) .port(port) @@ -142,13 +166,10 @@ class HsOfficePartnerControllerAcceptanceTest { .header("Location", startsWith("http://localhost")) .extract().header("Location"); // @formatter:on - // finally, the new partner can be viewed by its own admin - final var newUserUuid = UUID.fromString( - location.substring(location.lastIndexOf('/') + 1)); + // finally, the new partner can be accessed under the generated UUID + final var newUserUuid = toCleanup(UUID.fromString( + location.substring(location.lastIndexOf('/') + 1))); assertThat(newUserUuid).isNotNull(); - // TODO.feat: context.define("partner-admin@ttt.example.com"); - // assertThat(partnerRepository.findByUuid(newUserUuid)) - // .hasValueSatisfying(c -> assertThat(c.getPerson().getTradeName()).isEqualTo("Test Corp.")); } } @@ -157,39 +178,82 @@ class HsOfficePartnerControllerAcceptanceTest { class GetPartner { @Test - void hostsharingAdmin_withoutAssumedRole_canGetArbitraryPartner() { - // TODO.feat: final var givenPartnerUuid = partnerRepository.findPartnerByOptionalNameLike("Ixx").get(0).getUuid(); - final var givenPartnerUuid = UUID.randomUUID(); + void globalAdmin_withoutAssumedRole_canGetArbitraryPartner() { + context.define("alex@hostsharing.net"); + final var givenPartnerUuid = partnerRepo.findPartnerByOptionalNameLike("First").get(0).getUuid(); RestAssured // @formatter:off .given() - .header("current-user", "mike@hostsharing.net") + .header("current-user", "alex@hostsharing.net") .port(port) .when() .get("http://localhost/api/hs/office/partners/" + givenPartnerUuid) .then().log().body().assertThat() .statusCode(200) .contentType("application/json") - .body("person.tradeName", is("Ixx AG")) - .body("contact.label", is("Ixx AG")); - // @formatter:on + .body("", lenientlyEquals(""" + { + "person": { "tradeName": "First Impressions GmbH" }, + "contact": { "label": "first contact" } + } + """)); // @formatter:on } @Test @Accepts({ "Partner:X(Access Control)" }) void normalUser_canNotGetUnrelatedPartner() { - // TODO.feat: final var givenPartnerUuid = partnerRepository.findPartnerByOptionalNameLike("Ixx").get(0).getUuid(); - final UUID givenPartnerUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"); + context.define("alex@hostsharing.net"); + final var givenPartnerUuid = partnerRepo.findPartnerByOptionalNameLike("First").get(0).getUuid(); RestAssured // @formatter:off .given() - .header("current-user", "somebody@example.org") + .header("current-user", "drew@hostsharing.org") .port(port) .when() .get("http://localhost/api/hs/office/partners/" + givenPartnerUuid) .then().log().body().assertThat() - .statusCode(404); - // @formatter:on + .statusCode(404); // @formatter:on + } + + @Test + @Accepts({ "Partner:X(Access Control)" }) + void contactAdminUser_canGetRelatedPartner() { + context.define("alex@hostsharing.net"); + final var givenPartnerUuid = partnerRepo.findPartnerByOptionalNameLike("first contact").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "customer-admin@firstcontact.example.com") + .port(port) + .when() + .get("http://localhost/api/hs/office/partners/" + givenPartnerUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "person": { "tradeName": "First Impressions GmbH" }, + "contact": { "label": "first contact" } + } + """)); // @formatter:on } } + + private UUID toCleanup(final UUID tempPartnerUuid) { + tempPartnerUuids.add(tempPartnerUuid); + return tempPartnerUuid; + } + + @AfterEach + void cleanup() { + tempPartnerUuids.forEach(uuid -> { + jpaAttempt.transacted(() -> { + context.define("alex@hostsharing.net", null); + System.out.println("DELETING temporary partner: " + uuid); + final var count = partnerRepo.deleteByUuid(uuid); + assertThat(count).isGreaterThan(0); + }); + }); + } + } diff --git a/src/test/java/net/hostsharing/test/IsValidUuidMatcher.java b/src/test/java/net/hostsharing/test/IsValidUuidMatcher.java index a0de307f..37d523ce 100644 --- a/src/test/java/net/hostsharing/test/IsValidUuidMatcher.java +++ b/src/test/java/net/hostsharing/test/IsValidUuidMatcher.java @@ -9,10 +9,11 @@ import java.util.function.Predicate; public class IsValidUuidMatcher extends BaseMatcher { - public static Matcher isUuidValid() { - return new IsValidUuidMatcher(); - } - + /** + * Checks if the given String represents a valid UUID. + * @param actual the given String + * @return true if valid UUID, false otherwise + */ public static boolean isUuidValid(final String actual) { try { UUID.fromString(actual); @@ -22,6 +23,20 @@ public class IsValidUuidMatcher extends BaseMatcher { return true; } + /** + * Creates a matcher for RestAssured to validate if String is a valid UUID. + * + * @return the RestAssuredMatcher + */ + public static Matcher isUuidValid() { + return new IsValidUuidMatcher(); + } + + /** + * Creates matcher for AssertJ to validate if String is a valid UUID. + * + * @return the Predicate to be used as a matcher in AssertJ + */ public static Predicate isValidUuid() { return IsValidUuidMatcher::isUuidValid; } diff --git a/src/test/java/net/hostsharing/test/JsonMatcher.java b/src/test/java/net/hostsharing/test/JsonMatcher.java new file mode 100644 index 00000000..f29a0857 --- /dev/null +++ b/src/test/java/net/hostsharing/test/JsonMatcher.java @@ -0,0 +1,64 @@ +package net.hostsharing.test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.json.JSONException; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +public class JsonMatcher extends BaseMatcher { + + private final String expected; + private JSONCompareMode compareMode; + + public JsonMatcher(final String expected, final JSONCompareMode compareMode) { + this.expected = expected; + this.compareMode = compareMode; + } + + /** + * Creates a matcher for RestAssured to validate if the actual JSON matches the expected JSON leniently. + * + * @see package org.skyscreamer.jsonassert.JSONCompareMode.LENIENT + * + * @return the RestAssuredMatcher + */ + public static Matcher lenientlyEquals(final String expected) { + return new JsonMatcher(expected, JSONCompareMode.LENIENT); + } + + /** + * Creates a matcher for RestAssured to validate if the actual JSON matches the expected JSON strictly. + * + * @see package org.skyscreamer.jsonassert.JSONCompareMode.STRICT + * + * @return the RestAssuredMatcher + */ + public static Matcher strictlyEquals(final String expected) { + return new JsonMatcher(expected, JSONCompareMode.STRICT); + } + + @Override + public boolean matches(final Object actual) { + if (actual == null || actual.getClass().isAssignableFrom(CharSequence.class)) { + return false; + } + try { + final var actualJson = new ObjectMapper().writeValueAsString(actual); + compareMode = JSONCompareMode.LENIENT; + JSONAssert.assertEquals(expected, actualJson, compareMode); + return true; + } catch (final JSONException | JsonProcessingException e) { + throw new AssertionError(e); + } + } + + @Override + public void describeTo(final Description description) { + description.appendText("leniently matches JSON"); + } + +}