diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java index 7a512405..069a02b7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java @@ -112,7 +112,7 @@ class HsOfficeScenarioTests extends ScenarioTest { @Produces("Debitor: Test AG - main debitor") void shouldCreateSelfDebitorForPartner() { new CreateSelfDebitorForPartner(this, "Debitor: Test AG - main debitor") - .given("partnerPersonUuid", "%{Person: Test AG}") + .given("partnerPersonTradeName", "Test AG") .given("billingContactCaption", "Test AG - billing department") .given("billingContactEmailAddress", "billing@test-ag.example.org") .given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automatically, but is not yet diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java index 7b14e2a4..48c0b208 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java @@ -179,12 +179,12 @@ public abstract class ScenarioTest extends ContextBasedTest { return map; } - static String resolve(final String text) { + public static String resolve(final String text) { final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve(); return resolved; } - static Object resolveTyped(final String text) { + public static Object resolveTyped(final String text) { final var resolved = resolve(text); try { return UUID.fromString(resolved); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java index 9c93a111..c5b319da 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java @@ -1,6 +1,9 @@ package net.hostsharing.hsadminng.hs.office.scenarios; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.http.ContentType; +import lombok.Getter; import lombok.SneakyThrows; import net.hostsharing.hsadminng.reflection.AnnotationFinder; import org.apache.commons.collections4.map.LinkedMap; @@ -14,6 +17,7 @@ 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; @@ -22,6 +26,7 @@ import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; +import static java.net.URLEncoder.encode; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.platform.commons.util.StringUtils.isBlank; import static org.junit.platform.commons.util.StringUtils.isNotBlank; @@ -29,8 +34,9 @@ import static org.junit.platform.commons.util.StringUtils.isNotBlank; public abstract class UseCase> { private static final HttpClient client = HttpClient.newHttpClient(); + final ObjectMapper objectMapper = new ObjectMapper(); - private final ScenarioTest testSuite; + protected final ScenarioTest testSuite; private final Map>> requirements = new LinkedMap<>(); private final String resultAlias; private final Map givenProperties = new LinkedHashMap<>(); @@ -76,17 +82,35 @@ public abstract class UseCase> { return new JsonTemplate(jsonTemplate); } + public final void keep(final String alias, final Supplier http, final Function extractor) { + this.nextTitle = ScenarioTest.resolve(alias); + http.get().keep(extractor); + this.nextTitle = null; + } + public final void keep(final String alias, final Supplier http) { this.nextTitle = ScenarioTest.resolve(alias); http.get().keep(); this.nextTitle = null; } + @SneakyThrows + public final HttpResponse httpGet(final String uriPath) { + final var request = HttpRequest.newBuilder() + .GET() + .uri(new URI("http://localhost:" + testSuite.port + uriPath)) + .header("current-subject", ScenarioTest.RUN_AS_USER) + .timeout(Duration.ofSeconds(10)) + .build(); + final var response = client.send(request, BodyHandlers.ofString()); + return new HttpResponse(HttpMethod.POST, uriPath, null, response); + } + @SneakyThrows public final HttpResponse httpPost(final String uriPath, final JsonTemplate bodyJsonTemplate) { final var requestBody = bodyJsonTemplate.resolvePlaceholders(); final var request = HttpRequest.newBuilder() - .method(HttpMethod.POST.toString(), BodyPublishers.ofString(requestBody)) + .POST(BodyPublishers.ofString(requestBody)) .uri(new URI("http://localhost:" + testSuite.port + uriPath)) .header("Content-Type", "application/json") .header("current-subject", ScenarioTest.RUN_AS_USER) @@ -127,6 +151,10 @@ public abstract class UseCase> { return ScenarioTest.uuid(alias); } + public String uriEncoded(final String text) { + return encode(ScenarioTest.resolve(text)); + } + public static class JsonTemplate { private final String template; @@ -142,8 +170,12 @@ public abstract class UseCase> { public class HttpResponse { + @Getter private final java.net.http.HttpResponse response; + + @Getter private final HttpStatus status; + private UUID locationUuid; public HttpResponse( @@ -187,6 +219,16 @@ public abstract class UseCase> { return this; } + public void keep(final Function extractor) { + final var alias = nextTitle != null ? nextTitle : resultAlias; + assertThat(alias).as("cannot keep result, no alias found").isNotNull(); + + final var value = extractor.apply(this); + ScenarioTest.putAlias( + alias, + new ScenarioTest.Alias<>(UseCase.this.getClass(), UUID.fromString(value))); + } + public void keep() { final var alias = nextTitle != null ? nextTitle : resultAlias; assertThat(alias).as("cannot keep result, no alias found").isNotNull(); @@ -194,6 +236,12 @@ public abstract class UseCase> { alias, new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid)); } + + @SneakyThrows + public String getFromBody(final String path) { + final var rootNode = objectMapper.readTree(response.body()); + return getPropertyFromJson(rootNode, path); + } } public void print(final String output) { @@ -231,4 +279,55 @@ public abstract class UseCase> { private final String title(String resultAlias) { return getClass().getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1 $2") + " => " + resultAlias; } + + // FIXME: refactor to own class + /** + * Extracts a property from a JsonNode based on a dotted path. + * Supports array notation like "users[0].address.city" and root arrays like "[0].user.address.city". + * + * @param rootNode the root JsonNode + * @param propertyPath the property path in dot notation (e.g., "[0].user.address.city") + * @return the extracted property value as a String + */ + public static String getPropertyFromJson(final JsonNode rootNode, final String propertyPath) { + final var pathParts = propertyPath.split("\\."); + var currentNode = rootNode; + + // Traverse the JSON structure based on the path parts + for (final var part : pathParts) { + // Check if the part contains array notation like "[0]" + if (part.contains("[")) { + String arrayName; + final var arrayIndex = Integer.parseInt(part.substring(part.indexOf("[") + 1, part.indexOf("]"))); + + if (part.startsWith("[")) { + // This is a root-level array access (e.g., "[0]") + arrayName = null; + } else { + // This is a nested array access (e.g., "users[0]") + arrayName = part.substring(0, part.indexOf("[")); + } + + // If there's an array name, traverse to it + if (arrayName != null) { + currentNode = currentNode.path(arrayName); + } + + // Ensure the current node is an array, then access the element at the index + if (currentNode.isArray()) { + currentNode = currentNode.get(arrayIndex); + } + } else { + // Traverse as a normal field + currentNode = currentNode.path(part); + } + + // If at any point, the node is missing, return null + if (currentNode.isMissingNode()) { + return null; + } + } + + return currentNode.asText(); // Return the final value as a String + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java index f2e49088..ebfa6e4b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.OK; public class CreateSelfDebitorForPartner extends UseCase { @@ -14,6 +15,12 @@ public class CreateSelfDebitorForPartner extends UseCase + httpGet("/api/hs/office/relations?personData=" + uriEncoded("%{partnerPersonTradeName}")) + .expecting(OK).expecting(JSON), + response -> response.getFromBody("[0].holder.uuid") + ); + keep("BankAccount: Test AG - refund bank account", () -> httpPost("/api/hs/office/bankaccounts", usingJsonBody(""" { @@ -57,4 +64,5 @@ public class CreateSelfDebitorForPartner extends UseCase