HsOfficePartnerControllerAcceptanceTest against real repo+db

This commit is contained in:
Michael Hoennig 2022-09-14 09:24:19 +02:00
parent bc27e6dc89
commit a3d2dd3db1
5 changed files with 223 additions and 97 deletions

View File

@ -3,12 +3,14 @@ package net.hostsharing.hsadminng.hs.office.partner;
import net.hostsharing.hsadminng.Mapper; import net.hostsharing.hsadminng.Mapper;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; 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.api.HsOfficePartnersApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactResource; 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.HsOfficePartnerResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerUpdateResource; 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.generated.api.v1.model.HsOfficePersonResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; 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.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -31,45 +33,21 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Autowired @Autowired
private HsOfficePartnerRepository partnerRepo; private HsOfficePartnerRepository partnerRepo;
@Autowired
private HsOfficePersonRepository personRepo;
@Autowired
private HsOfficeContactRepository contactRepo;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficePartnerResource>> listPartners( public ResponseEntity<List<HsOfficePartnerResource>> listPartners(
final String currentUser, final String currentUser,
final String assumedRoles, final String assumedRoles,
final String name) { final String name) {
// TODO.feat: context.define(currentUser, assumedRoles); context.define(currentUser, assumedRoles);
// TODO.feat: final var entities = partnerRepo.findPartnerByOptionalNameLike(name); 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 resources = Mapper.mapList(entities, HsOfficePartnerResource.class, final var resources = Mapper.mapList(entities, HsOfficePartnerResource.class,
PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER); PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER);
@ -83,14 +61,27 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final String assumedRoles, final String assumedRoles,
final HsOfficePartnerResource body) { final HsOfficePartnerResource body) {
// TODO.feat: context.define(currentUser, assumedRoles); context.define(currentUser, assumedRoles);
if (body.getUuid() == null) { if (body.getUuid() == null) {
body.setUuid(UUID.randomUUID()); body.setUuid(UUID.randomUUID());
} }
// TODO.feat: final var saved = partnerRepo.save(map(body, HsOfficePartnerEntity.class)); final var entityToSave = map(body, HsOfficePartnerEntity.class);
final var saved = map(body, HsOfficePartnerEntity.class, PARTNER_RESOURCE_TO_ENTITY_POSTMAPPER); 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 = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
@ -103,37 +94,29 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
} }
@Override @Override
@Transactional(readOnly = true)
public ResponseEntity<HsOfficePartnerResource> getPartnerByUuid( public ResponseEntity<HsOfficePartnerResource> getPartnerByUuid(
final String currentUser, final String currentUser,
final String assumedRoles, final String assumedRoles,
final UUID partnerUuid) { final UUID partnerUuid) {
// TODO.feat: context.define(currentUser, assumedRoles); context.define(currentUser, assumedRoles);
// TODO.feat: final var result = partnerRepo.findByUuid(partnerUuid); final var result = partnerRepo.findByUuid(partnerUuid);
final var result = if (result.isEmpty()) {
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) {
return ResponseEntity.notFound().build(); 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 @Override
@Transactional
public ResponseEntity<Void> deletePartnerByUuid(final String currentUser, final String assumedRoles, final UUID userUuid) { public ResponseEntity<Void> deletePartnerByUuid(final String currentUser, final String assumedRoles, final UUID userUuid) {
return null; return null;
} }
@Override @Override
@Transactional
public ResponseEntity<HsOfficePartnerResource> updatePartner( public ResponseEntity<HsOfficePartnerResource> updatePartner(
final String currentUser, final String currentUser,
final String assumedRoles, final String assumedRoles,

View File

@ -45,7 +45,7 @@ end; $$;
Creates the roles and their assignments for a new contact for the AFTER INSERT TRIGGER. 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 returns trigger
language plpgsql language plpgsql
strict as $$ strict as $$
@ -88,11 +88,11 @@ end; $$;
An AFTER INSERT TRIGGER which creates the role structure for a new customer. An AFTER INSERT TRIGGER which creates the role structure for a new customer.
*/ */
create trigger createRbacRolesForhsOfficeContact_Trigger create trigger createRbacRolesForHsOfficeContact_Trigger
after insert after insert
on hs_office_contact on hs_office_contact
for each row for each row
execute procedure createRbacRolesForhsOfficeContact(); execute procedure createRbacRolesForHsOfficeContact();
--// --//

View File

@ -5,6 +5,9 @@ import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.Accepts; import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; 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.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.test.web.server.LocalServerPort;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid;
import static net.hostsharing.test.JsonBuilder.jsonObject; 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.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = HsadminNgApplication.class classes = { HsadminNgApplication.class, JpaAttempt.class }
) )
@Transactional @Transactional
class HsOfficePartnerControllerAcceptanceTest { class HsOfficePartnerControllerAcceptanceTest {
@ -34,31 +41,47 @@ class HsOfficePartnerControllerAcceptanceTest {
@Autowired @Autowired
Context contextMock; Context contextMock;
@Autowired @Autowired
HsOfficePartnerRepository partnerRepository; HsOfficePartnerRepository partnerRepo;
@Autowired
JpaAttempt jpaAttempt;
Set<UUID> tempPartnerUuids = new HashSet<>();
@Nested @Nested
@Accepts({ "Partner:F(Find)" }) @Accepts({ "Partner:F(Find)" })
class ListPartners { class ListPartners {
@Test @Test
void testHostsharingAdmin_withoutAssumedRoles_canViewAllPartners_ifNoCriteriaGiven() { void globalAdmin_withoutAssumedRoles_canViewAllPartners_ifNoCriteriaGiven() throws JSONException {
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
.header("current-user", "mike@hostsharing.net") .header("current-user", "alex@hostsharing.net")
.port(port) .port(port)
.when() .when()
.get("http://localhost/api/hs/office/partners") .get("http://localhost/api/hs/office/partners")
.then().assertThat() .then().log().all().assertThat()
.statusCode(200) .statusCode(200)
.contentType("application/json") .contentType("application/json")
.body("[0].contact.label", is("Ixx AG")) .body("", lenientlyEquals("""
.body("[0].person.tradeName", is("Ixx AG")) [
.body("[1].contact.label", is("Ypsilon GmbH")) {
.body("[1].person.tradeName", is("Ypsilon GmbH")) "person": { "tradeName": "First Impressions GmbH" },
.body("[2].contact.label", is("Zett OHG")) "contact": { "label": "first contact" }
.body("[2].person.tradeName", is("Zett OHG")) },
.body("size()", greaterThanOrEqualTo(3)); {
"person": { "tradeName": "Ostfriesische Kuhhandel OHG" },
"contact": { "label": "third contact" }
},
{
"person": { "tradeName": "Rockshop e.K." },
"contact": { "label": "second contact" }
}
]
"""));
// @formatter:on // @formatter:on
} }
} }
@ -91,13 +114,13 @@ class HsOfficePartnerControllerAcceptanceTest {
"""; """;
@Test @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 final var location = RestAssured // @formatter:off
.given() .given()
.header("current-user", "mike@hostsharing.net") .header("current-user", "alex@hostsharing.net")
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(jsonObject(NEW_PARTNER_JSON_WITHOUT_UUID) .body(jsonObject(NEW_PARTNER_JSON_WITHOUT_UUID)
.with("uuid", givenUUID.toString()).toString()) .with("uuid", givenUUID.toString()).toString())
@ -110,24 +133,25 @@ class HsOfficePartnerControllerAcceptanceTest {
.body("uuid", is("3fa85f64-5717-4562-b3fc-2c963f66afa6")) .body("uuid", is("3fa85f64-5717-4562-b3fc-2c963f66afa6"))
.body("registrationNumber", is("123456")) .body("registrationNumber", is("123456"))
.body("person.tradeName", is("Test Corp.")) .body("person.tradeName", is("Test Corp."))
.body("contact.label", is("Test Corp."))
.header("Location", startsWith("http://localhost")) .header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on .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( final var newUserUuid = UUID.fromString(
location.substring(location.lastIndexOf('/') + 1)); location.substring(location.lastIndexOf('/') + 1));
assertThat(newUserUuid).isEqualTo(givenUUID); assertThat(newUserUuid).isEqualTo(givenUUID);
// TODO.feat: context.define("partner-admin@ttt.example.com"); context.define("alex@hostsharing.net");
// assertThat(partnerRepository.findByUuid(newUserUuid)) assertThat(partnerRepo.findByUuid(newUserUuid))
// .hasValueSatisfying(c -> assertThat(c.getPerson().getTradeName()).isEqualTo("Test Corp.")); .hasValueSatisfying(c -> assertThat(c.getPerson().getTradeName()).isEqualTo("Test Corp."));
} }
@Test @Test
void hostsharingAdmin_withoutAssumedRole_canAddPartner_withGeneratedUuid() { void globalAdmin_withoutAssumedRole_canAddPartner_withGeneratedUuid() {
final var location = RestAssured // @formatter:off final var location = RestAssured // @formatter:off
.given() .given()
.header("current-user", "mike@hostsharing.net") .header("current-user", "alex@hostsharing.net")
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(NEW_PARTNER_JSON_WITHOUT_UUID) .body(NEW_PARTNER_JSON_WITHOUT_UUID)
.port(port) .port(port)
@ -142,13 +166,10 @@ class HsOfficePartnerControllerAcceptanceTest {
.header("Location", startsWith("http://localhost")) .header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on .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 generated UUID
final var newUserUuid = UUID.fromString( final var newUserUuid = toCleanup(UUID.fromString(
location.substring(location.lastIndexOf('/') + 1)); location.substring(location.lastIndexOf('/') + 1)));
assertThat(newUserUuid).isNotNull(); 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 { class GetPartner {
@Test @Test
void hostsharingAdmin_withoutAssumedRole_canGetArbitraryPartner() { void globalAdmin_withoutAssumedRole_canGetArbitraryPartner() {
// TODO.feat: final var givenPartnerUuid = partnerRepository.findPartnerByOptionalNameLike("Ixx").get(0).getUuid(); context.define("alex@hostsharing.net");
final var givenPartnerUuid = UUID.randomUUID(); final var givenPartnerUuid = partnerRepo.findPartnerByOptionalNameLike("First").get(0).getUuid();
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
.header("current-user", "mike@hostsharing.net") .header("current-user", "alex@hostsharing.net")
.port(port) .port(port)
.when() .when()
.get("http://localhost/api/hs/office/partners/" + givenPartnerUuid) .get("http://localhost/api/hs/office/partners/" + givenPartnerUuid)
.then().log().body().assertThat() .then().log().body().assertThat()
.statusCode(200) .statusCode(200)
.contentType("application/json") .contentType("application/json")
.body("person.tradeName", is("Ixx AG")) .body("", lenientlyEquals("""
.body("contact.label", is("Ixx AG")); {
// @formatter:on "person": { "tradeName": "First Impressions GmbH" },
"contact": { "label": "first contact" }
}
""")); // @formatter:on
} }
@Test @Test
@Accepts({ "Partner:X(Access Control)" }) @Accepts({ "Partner:X(Access Control)" })
void normalUser_canNotGetUnrelatedPartner() { void normalUser_canNotGetUnrelatedPartner() {
// TODO.feat: final var givenPartnerUuid = partnerRepository.findPartnerByOptionalNameLike("Ixx").get(0).getUuid(); context.define("alex@hostsharing.net");
final UUID givenPartnerUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"); final var givenPartnerUuid = partnerRepo.findPartnerByOptionalNameLike("First").get(0).getUuid();
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
.header("current-user", "somebody@example.org") .header("current-user", "drew@hostsharing.org")
.port(port) .port(port)
.when() .when()
.get("http://localhost/api/hs/office/partners/" + givenPartnerUuid) .get("http://localhost/api/hs/office/partners/" + givenPartnerUuid)
.then().log().body().assertThat() .then().log().body().assertThat()
.statusCode(404); .statusCode(404); // @formatter:on
// @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);
});
});
}
} }

View File

@ -9,10 +9,11 @@ import java.util.function.Predicate;
public class IsValidUuidMatcher extends BaseMatcher<CharSequence> { public class IsValidUuidMatcher extends BaseMatcher<CharSequence> {
public static Matcher<CharSequence> 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) { public static boolean isUuidValid(final String actual) {
try { try {
UUID.fromString(actual); UUID.fromString(actual);
@ -22,6 +23,20 @@ public class IsValidUuidMatcher extends BaseMatcher<CharSequence> {
return true; return true;
} }
/**
* Creates a matcher for RestAssured to validate if String is a valid UUID.
*
* @return the RestAssuredMatcher
*/
public static Matcher<CharSequence> 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<String> isValidUuid() { public static Predicate<String> isValidUuid() {
return IsValidUuidMatcher::isUuidValid; return IsValidUuidMatcher::isUuidValid;
} }

View File

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