fixes HTTP GET with multiple values in result array

This commit is contained in:
Michael Hoennig 2024-10-29 09:17:21 +01:00
parent 765b679042
commit 8fc81d8e2b
17 changed files with 161 additions and 73 deletions

View File

@ -84,6 +84,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
"ERROR: [400] debitorRel.mark must be null");
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
try {
if (body.getDebitorRel() != null) {
body.getDebitorRel().setType(DEBITOR.name());
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
@ -95,9 +96,28 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse(
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
() -> { throw new ValidationException("Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());});
() -> {
throw new ValidationException(
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
});
}
final var partnerRel = em.createNativeQuery("""
SELECT partnerRel.*
FROM hs_office.relation AS partnerRel
JOIN hs_office.relation AS debitorRel
ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid
WHERE partnerRel.type = 'PARTNER'
AND :NEW_DebitorRelUuid = debitorRel.uuid
""").setParameter("NEW_DebitorRelUuid", entityToSave.getDebitorRel().getUuid()).getResultList();
final var debitorRel = em.createNativeQuery("""
SELECT debitorRel.*
FROM hs_office.relation AS debitorRel
WHERE :NEW_DebitorRelUuid = debitorRel.uuid
""").setParameter("NEW_DebitorRelUuid", entityToSave.getDebitorRel().getUuid()).getResultList();
final var savedEntity = debitorRepo.save(entityToSave);
em.flush();
em.refresh(savedEntity);
@ -109,6 +129,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
.toUri();
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
return ResponseEntity.created(uri).body(mapped);
} catch (final RuntimeException exc) {
throw exc;
}
}
@Override

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.reflection;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import java.lang.annotation.Annotation;
@ -11,12 +12,12 @@ import static java.util.Optional.empty;
@UtilityClass
public class AnnotationFinder {
@SneakyThrows
public static <T extends Annotation> Optional<T> findCallerAnnotation(
final Class<T> annotationClassToFind,
final Class<? extends Annotation> annotationClassToStopLookup
) {
for (var element : Thread.currentThread().getStackTrace()) {
try {
final var clazz = Class.forName(element.getClassName());
final var method = getMethodFromStackElement(clazz, element);
@ -28,15 +29,11 @@ public class AnnotationFinder {
return empty();
}
}
} catch (final ClassNotFoundException | NoSuchMethodException e) {
throw new RuntimeException(e); // FIXME: when does this happen?
}
}
return empty();
}
private static Method getMethodFromStackElement(Class<?> clazz, StackTraceElement element)
throws NoSuchMethodException {
private static Method getMethodFromStackElement(Class<?> clazz, StackTraceElement element) {
for (var method : clazz.getDeclaredMethods()) {
if (method.getName().equals(element.getMethodName())) {
return method;

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.customer WHERE uuid = NEW.customerUuid INTO newCustomer;
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s of package', NEW.customerUuid);
perform rbac.defineRoleWithGrants(
@ -102,10 +102,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.customer WHERE uuid = OLD.customerUuid INTO oldCustomer;
assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s', OLD.customerUuid);
assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s of package', OLD.customerUuid);
SELECT * FROM rbactest.customer WHERE uuid = NEW.customerUuid INTO newCustomer;
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s of package', NEW.customerUuid);
if NEW.customerUuid <> OLD.customerUuid then

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.package WHERE uuid = NEW.packageUuid INTO newPackage;
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s of domain', NEW.packageUuid);
perform rbac.defineRoleWithGrants(
@ -98,10 +98,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.package WHERE uuid = OLD.packageUuid INTO oldPackage;
assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s', OLD.packageUuid);
assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s of domain', OLD.packageUuid);
SELECT * FROM rbactest.package WHERE uuid = NEW.packageUuid INTO newPackage;
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s of domain', NEW.packageUuid);
if NEW.packageUuid <> OLD.packageUuid then

View File

@ -38,13 +38,13 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.person WHERE uuid = NEW.holderUuid INTO newHolderPerson;
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid);
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s of relation', NEW.holderUuid);
SELECT * FROM hs_office.person WHERE uuid = NEW.anchorUuid INTO newAnchorPerson;
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid);
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s of relation', NEW.anchorUuid);
SELECT * FROM hs_office.contact WHERE uuid = NEW.contactUuid INTO newContact;
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid);
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s of relation', NEW.contactUuid);
perform rbac.defineRoleWithGrants(

View File

@ -37,10 +37,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s of partner', NEW.partnerRelUuid);
SELECT * FROM hs_office.partner_details WHERE uuid = NEW.detailsUuid INTO newPartnerDetails;
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s', NEW.detailsUuid);
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s of partner', NEW.detailsUuid);
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'DELETE'), hs_office.relation_OWNER(newPartnerRel));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.relation_TENANT(newPartnerRel));
@ -96,16 +96,16 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.relation WHERE uuid = OLD.partnerRelUuid INTO oldPartnerRel;
assert oldPartnerRel.uuid is not null, format('oldPartnerRel must not be null for OLD.partnerRelUuid = %s', OLD.partnerRelUuid);
assert oldPartnerRel.uuid is not null, format('oldPartnerRel must not be null for OLD.partnerRelUuid = %s of partner', OLD.partnerRelUuid);
SELECT * FROM hs_office.relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s of partner', NEW.partnerRelUuid);
SELECT * FROM hs_office.partner_details WHERE uuid = OLD.detailsUuid INTO oldPartnerDetails;
assert oldPartnerDetails.uuid is not null, format('oldPartnerDetails must not be null for OLD.detailsUuid = %s', OLD.detailsUuid);
assert oldPartnerDetails.uuid is not null, format('oldPartnerDetails must not be null for OLD.detailsUuid = %s of partner', OLD.detailsUuid);
SELECT * FROM hs_office.partner_details WHERE uuid = NEW.detailsUuid INTO newPartnerDetails;
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s', NEW.detailsUuid);
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s of partner', NEW.detailsUuid);
if NEW.partnerRelUuid <> OLD.partnerRelUuid then

View File

@ -44,10 +44,10 @@ begin
WHERE partnerRel.type = 'PARTNER'
AND NEW.debitorRelUuid = debitorRel.uuid
INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.debitorRelUuid = %s of debitor', NEW.debitorRelUuid);
SELECT * FROM hs_office.relation WHERE uuid = NEW.debitorRelUuid INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorRelUuid = %s of debitor', NEW.debitorRelUuid);
SELECT * FROM hs_office.bankaccount WHERE uuid = NEW.refundBankAccountUuid INTO newRefundBankAccount;

View File

@ -37,14 +37,14 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.bankaccount WHERE uuid = NEW.bankAccountUuid INTO newBankAccount;
assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s', NEW.bankAccountUuid);
assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s of sepamandate', NEW.bankAccountUuid);
SELECT debitorRel.*
FROM hs_office.relation debitorRel
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s of sepamandate', NEW.debitorUuid);
perform rbac.defineRoleWithGrants(

View File

@ -40,7 +40,7 @@ begin
JOIN hs_office.relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
WHERE partner.uuid = NEW.partnerUuid
INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerUuid = %s', NEW.partnerUuid);
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerUuid = %s of membership', NEW.partnerUuid);
perform rbac.defineRoleWithGrants(

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.membership WHERE uuid = NEW.membershipUuid INTO newMembership;
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s', NEW.membershipUuid);
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s of coopshares', NEW.membershipUuid);
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.membership_AGENT(newMembership));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership));

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.membership WHERE uuid = NEW.membershipUuid INTO newMembership;
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s', NEW.membershipUuid);
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s of coopasset', NEW.membershipUuid);
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.membership_AGENT(newMembership));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership));

View File

@ -37,14 +37,14 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.debitor WHERE uuid = NEW.debitorUuid INTO newDebitor;
assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s of project', NEW.debitorUuid);
SELECT debitorRel.*
FROM hs_office.relation debitorRel
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s or project', NEW.debitorUuid);
perform rbac.defineRoleWithGrants(

View File

@ -39,6 +39,8 @@ public abstract class ScenarioTest extends ContextBasedTest {
@Getter
private PrintWriter markdownFile;
private StringBuilder debugLog = new StringBuilder();
record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {}
private final static Map<String, Alias<?>> aliases = new HashMap<>();
@ -57,22 +59,37 @@ public abstract class ScenarioTest extends ContextBasedTest {
@BeforeEach
void init(final TestInfo testInfo) {
createHostsharingPerson();
try {
testInfo.getTestMethod().ifPresent(this::callRequiredProducers);
createTestLogMarkdownFile(testInfo);
} catch (Exception exc) {
throw exc;
}
}
@AfterEach
void cleanup() {
properties.clear();
if (markdownFile != null) {
markdownFile.close();
} else {
toString();
}
debugLog = new StringBuilder();
}
@SneakyThrows
void print(final String output) {
final var outputWithCommentsForUuids = UUIDAppender.appendUUIDKey(aliases, output.replace("+", "\\+"));
// for tests executed due to @Requires/@Produces there is no markdownFile yet
if (markdownFile != null) {
markdownFile.print(output);
markdownFile.print(outputWithCommentsForUuids);
}
// but the debugLog should contain all output
debugLog.append(outputWithCommentsForUuids);
}
void printLine(final String output) {

View File

@ -0,0 +1,34 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
import java.util.Map;
public class UUIDAppender {
public static String appendUUIDKey(Map<String, ScenarioTest.Alias<?>> uuidMap, String multilineText) {
// Split the multiline text into lines
String[] lines = multilineText.split("\\r?\\n");
// StringBuilder to build the resulting multiline text
StringBuilder result = new StringBuilder();
// Iterate over each line
for (String line : lines) {
// Iterate over the map to find if the line contains a UUID
for (Map.Entry<String, ScenarioTest.Alias<?>> entry : uuidMap.entrySet()) {
String uuidString = entry.getValue().uuid().toString();
// If the line contains the UUID, append the key at the end
if (line.contains(uuidString)) {
line = line + " // " + entry.getKey();
break; // Exit once we've matched one UUID
}
}
// Append the (possibly modified) line to the result
result.append(line).append("\n");
}
// Return the final modified text
return result.toString();
}
}

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.restassured.http.ContentType;
@ -17,10 +18,10 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
@ -67,7 +68,11 @@ public abstract class UseCase<T extends UseCase<?>> {
|------|-------|""");
givenProperties.forEach((key, value) -> printLine("| " + key + " | " + value.toString().replace("\n", "<br>") + " |"));
printLine("");
requirements.forEach((alias, factory) -> factory.apply(alias).run().keep());
requirements.forEach((alias, factory) -> {
if (!ScenarioTest.containsAlias(alias)) {
factory.apply(alias).run().keep();
}
});
return run();
}
protected abstract HttpResponse run();
@ -144,7 +149,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.timeout(Duration.ofSeconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.PATCH, uriPath, null, response);
return new HttpResponse(HttpMethod.DELETE, uriPath, null, response);
}
public final UUID uuid(final String alias) {
@ -240,8 +245,20 @@ public abstract class UseCase<T extends UseCase<?>> {
new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
}
@SneakyThrows
public HttpResponse expectArrayElements(final int expectedElementCount) {
final var rootNode = objectMapper.readTree(response.body());
assertThat(rootNode.isArray()).as("array expected, but got: " + response.body()).isTrue();
final var root = (List<?>) objectMapper.readValue(response.body(), new TypeReference<List<Object>>() {
});
assertThat(root.size()).as("unexpected number of array elements").isEqualTo(expectedElementCount);
return this;
}
@SneakyThrows
public String getFromBody(final String path) {
// FIXME: use JsonPath: https://www.baeldung.com/guide-to-jayway-jsonpath
final var rootNode = objectMapper.readTree(response.body());
return getPropertyFromJson(rootNode, path);
}

View File

@ -16,9 +16,9 @@ public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPar
@Override
protected HttpResponse run() {
keep("partnerPersonUuid", () ->
httpGet("/api/hs/office/relations?personData=" + uriEncoded("%{partnerPersonTradeName}"))
httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerPersonTradeName}"))
.expecting(OK).expecting(JSON),
response -> response.getFromBody("[0].holder.uuid")
response -> response.expectArrayElements(1).getFromBody("[0].holder.uuid")
);
printPara("From that output above, we're taking the UUID of the holder of the first result element.");
printPara("**HINT**: With production data, you might get multiple results and have to decide which is the right one.");
@ -49,7 +49,7 @@ public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPar
return httpPost("/api/hs/office/debitors", usingJsonBody("""
{
"debitorRel": {
"type": "DEBITOR", // FIXME: should be defaulted to DEBITOR
"type": "DEBITOR", // TODO.impl: should become defaulted to DEBITOR
"anchorUuid": ${partnerPersonUuid},
"holderUuid": ${partnerPersonUuid},
"contactUuid": ${Contact: Test AG - billing department}

View File

@ -10,7 +10,7 @@ public class DeleteDebitor extends UseCase<DeleteDebitor> {
super(testSuite);
requires("Debitor: Test AG - delete debitor", alias -> new CreateSelfDebitorForPartner(testSuite, alias)
.given("partnerPersonUuid", "%{Person: Test AG}")
.given("partnerPersonTradeName", "Test AG")
.given("billingContactCaption", "Test AG - billing department")
.given("billingContactEmailAddress", "billing@test-ag.example.org")
.given("debitorNumberSuffix", "%{debitorSuffix}")