From a1e04508ed480d3b5ea18b42857b53ab38333dfa Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 19 Oct 2024 14:06:01 +0200 Subject: [PATCH] all 4 tests green and printing test report --- .../office/usecases/HsOfficeUseCasesTest.java | 12 +- .../hs/office/usecases/TemplateResolver.java | 138 ++++++++++++++++++ .../hsadminng/hs/office/usecases/UseCase.java | 55 ++----- .../hs/office/usecases/UseCaseTest.java | 25 +++- .../debitor/CreateSelfDebitorForPartner.java | 40 ++--- .../usecases/membership/CreateMembership.java | 6 +- .../usecases/partner/CreatePartner.java | 14 +- 7 files changed, 208 insertions(+), 82 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/usecases/TemplateResolver.java diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/HsOfficeUseCasesTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/HsOfficeUseCasesTest.java index 04f32f35..3226900e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/HsOfficeUseCasesTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/HsOfficeUseCasesTest.java @@ -6,7 +6,6 @@ import net.hostsharing.hsadminng.hs.office.usecases.membership.CreateMembership; import net.hostsharing.hsadminng.hs.office.usecases.partner.CreatePartner; import net.hostsharing.hsadminng.hs.office.usecases.partner.DeletePartner; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; @@ -47,13 +46,22 @@ class HsOfficeUseCasesTest extends UseCaseTest { @Order(2000) void shouldCreateSelfDebitorForPartner() { new CreateSelfDebitorForPartner(this, "Debitor: Test AG - main debitor") + .given("partnerPersonUuid", "%{Person: 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 + .given("billable", true) + .given("vatId", "VAT123456") + .given("vatCountryCode", "DE") + .given("vatBusiness", true) + .given("vatReverseCharge", false) + .given("defaultPrefix", "tst") .doRun() .keep(); } @Test @Order(3000) - @Disabled void shouldCreateMembershipForPartner() { new CreateMembership(this).doRun(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/TemplateResolver.java b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/TemplateResolver.java new file mode 100644 index 00000000..b71a83ef --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/TemplateResolver.java @@ -0,0 +1,138 @@ +package net.hostsharing.hsadminng.hs.office.usecases; + +import java.util.Map; + +public class TemplateResolver { + + private final String template; + private final Map properties; + private final StringBuilder resolved = new StringBuilder(); + private int position = 0; + + public TemplateResolver(final String template, final Map properties) { + this.template = template; + this.properties = properties; + } + + String resolve() { + copy(); + return resolved.toString(); + } + + private void copy() { + while (hasMoreChars()) { + if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') { + startPlaceholder(currentChar()); + } else { + resolved.append(fetchChar()); + } + } + } + + private boolean hasMoreChars() { + return position < template.length(); + } + + private void startPlaceholder(final char intro) { + skipChars(intro + "{"); + int nested = 0; + final var placeholder = new StringBuilder(); + while (nested > 0 || currentChar() != '}') { + if (currentChar() == '}') { + --nested; + placeholder.append(fetchChar()); + } else if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') { + ++nested; + placeholder.append(fetchChar()); + } else { + placeholder.append(fetchChar()); + } + } + final var name = new TemplateResolver(placeholder.toString(), properties).resolve(); + final var value = propVal(name); + if ( intro == '%') { + resolved.append(value); + } else { + resolved.append(optionallyQuoted(value)); + } + skipChar('}'); + } + + private Object propVal(final String name) { + final var val = properties.get(name); + if (val == null) { + throw new IllegalStateException("Missing required property: " + name); + } + return val; + } + + private void skipChar(final char expectedChar) { + if (currentChar() != expectedChar) { + throw new IllegalStateException("expected '" + expectedChar + "' but got '" + currentChar() + "'"); + } + ++position; + } + + private void skipChars(final String expectedChars) { + final var nextChars = template.substring(position, position + expectedChars.length()); + if ( !nextChars.equals(expectedChars) ) { + throw new IllegalStateException("expected '" + expectedChars + "' but got '" + nextChars + "'"); + } + position += expectedChars.length(); + } + + private char fetchChar() { + if ((position+1) > template.length()) { + throw new IllegalStateException("no more characters. resolved so far: " + resolved); + } + final var currentChar = currentChar(); + //System.out.println("fetched #" + position + ": " + currentChar); + ++position; + return currentChar; + } + + private char currentChar() { + if (position >= template.length()) { + throw new IllegalStateException("no more characters. resolved so far: " + resolved); + } + return template.charAt(position); + } + + private char nextChar() { + if ((position+1) >= template.length()) { + throw new IllegalStateException("no more characters. resolved so far: " + resolved); + } + return template.charAt(position+1); + } + + private static String optionallyQuoted(final Object value) { + return switch (value) { + case Boolean bool -> bool.toString(); + case Number number -> number.toString(); + default -> "\"" + value + "\""; + }; + } + + public static void main(String[] args) { + System.out.println( + new TemplateResolver(""" + etwas davor, + + ${einfacher Platzhalter}, + ${verschachtelter %{Name}}, + + und nochmal ohne Quotes: + + %{einfacher Platzhalter}, + %{verschachtelter %{Name}}, + + etwas danach. + """, + Map.ofEntries( + Map.entry("Name", "placeholder"), + Map.entry("einfacher Platzhalter", "simple placeholder"), + Map.entry("verschachtelter placeholder", "nested placeholder") + )).resolve()); + + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/UseCase.java b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/UseCase.java index 06e34350..a7ea4b29 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/UseCase.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/UseCase.java @@ -8,16 +8,11 @@ import org.apache.commons.collections4.map.LinkedMap; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import java.util.ArrayList; import java.util.Map; import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; import static org.hamcrest.Matchers.startsWith; @@ -26,7 +21,7 @@ public abstract class UseCase> { private final UseCaseTest testSuite; private final Map>> requirements = new LinkedMap<>(); private final String resultAlias; - private String nextTitle; + private String nextTitle; // FIXME: ugly public UseCase(final UseCaseTest testSuite) { this(testSuite, null); @@ -42,7 +37,7 @@ public abstract class UseCase> { } public final void requires(final String alias, final Function> useCaseFactory) { - if ( !UseCaseTest.containsAlias(alias) ) { + if (!UseCaseTest.containsAlias(alias)) { requirements.put(alias, useCaseFactory); } } @@ -51,10 +46,17 @@ public abstract class UseCase> { assumeThat(UseCaseTest.containsAlias(alias)) .as("skipping because alias '" + alias + "' not found, maybe the other test failed?") .isTrue(); - log("depends on ["+alias+"]("+UseCaseTest.getAlias(alias).useCase().getSimpleName()+".md)"); + log("depends on [" + alias + "](" + UseCaseTest.getAlias(alias).useCase().getSimpleName() + ".md)"); } public final HttpResponse doRun() { + log("### Given Properties\n"); + log(""" + | name | value | + |------|-------| + """.trim()); + UseCaseTest.properties().forEach((key, value) -> log("| " + key + " | " + value + " |")); + log(""); requirements.forEach((alias, factory) -> factory.apply(alias).run().keep()); return run(); } @@ -71,7 +73,7 @@ public abstract class UseCase> { } public final void keep(final String alias, final Supplier http) { - this.nextTitle = alias; // FIXME: ugly + this.nextTitle = UseCaseTest.resolve(alias); http.get().keep(); this.nextTitle = null; } @@ -109,37 +111,7 @@ public abstract class UseCase> { } String resolvePlaceholders() { - var partiallyResolved = new AtomicReference<>(template); - UseCaseTest.knowVariables().forEach(entry -> partiallyResolved.set( - partiallyResolved.get().replace("${" + entry.getKey() + "}", optionallyQuoted(entry.getValue()))) - ); - verifyAllPlaceholdersResolved(partiallyResolved.get()); - return partiallyResolved.get(); - } - - private String optionallyQuoted(final Object value) { - return switch (value) { - case Boolean bool -> bool.toString(); - case Number number -> number.toString(); - default -> "\"" + value + "\""; - }; - } - - private void verifyAllPlaceholdersResolved(final String remainingTemplate) { - final var pattern = Pattern.compile("\\$\\{[^}]+\\}"); - final var matcher = pattern.matcher(remainingTemplate); - - final var unresolvedPlaceholders = new ArrayList<>(); - while (matcher.find()) { - unresolvedPlaceholders.add(matcher.group()); - } - - final var knownVariables = UseCaseTest.knowVariables() - .map(e -> '"' + e.getKey() + "\": \"" + e.getValue() + '"') - .collect(Collectors.joining("\n ")); - assertThat(unresolvedPlaceholders) - .as("known variables: [\n " + knownVariables + "\n]\nunresolved placeholders:") - .isEmpty(); + return UseCaseTest.resolve(template); } } @@ -173,6 +145,9 @@ public abstract class UseCase> { log(httpMethod.name() + " " + uri); log(requestBody + "=> status: " + status + " " + (locationUuid != null ? locationUuid : "")); + if (!status.is2xxSuccessful()) { + log(response.getBody().prettyPrint()); + } log("```"); log(""); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/UseCaseTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/UseCaseTest.java index db83585b..1570039b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/UseCaseTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/UseCaseTest.java @@ -1,6 +1,5 @@ package net.hostsharing.hsadminng.hs.office.usecases; -import com.tngtech.archunit.thirdparty.com.google.common.collect.Streams; import lombok.Getter; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; @@ -18,10 +17,10 @@ import java.io.FileWriter; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -101,13 +100,23 @@ public abstract class UseCaseTest extends ContextBasedTest { } static void putProperty(final String name, final Object value) { - properties.put(name, value); + properties.put(name, (value instanceof String string) ? resolve(string) : value); } - static Stream> knowVariables() { - return Streams.concat( - UseCaseTest.aliases.entrySet().stream().map(e -> Map.entry(e.getKey(), e.getValue().uuid())), - UseCaseTest.properties.entrySet().stream() - ); + static LinkedHashMap properties() { + return new LinkedHashMap<>(properties); } + + static Map knowVariables() { + final var map = new LinkedHashMap(); + UseCaseTest.aliases.forEach((key, value) -> map.put(key, value.uuid())); + map.putAll(UseCaseTest.properties); + return map; + } + + static String resolve(final String text) { + final var resolved = new TemplateResolver(text, UseCaseTest.knowVariables()).resolve(); + return resolved; + } + } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/debitor/CreateSelfDebitorForPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/debitor/CreateSelfDebitorForPartner.java index a616e2db..c210ace5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/debitor/CreateSelfDebitorForPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/debitor/CreateSelfDebitorForPartner.java @@ -30,9 +30,9 @@ public class CreateSelfDebitorForPartner extends UseCase httpPost("/api/hs/office/contacts", usingJsonBody(""" { - "caption": "Test AG - billing department", + "caption": ${billingContactCaption}, "emailAddresses": { - "main": "billing@test-ag.example.org" + "main": ${billingContactEmailAddress} } } """)) @@ -40,23 +40,23 @@ public class CreateSelfDebitorForPartner extends UseCase { public CreateMembership(final UseCaseTest testSuite) { super(testSuite); - requires("partner: Test AG.uuid"); + requires("Partner: Test AG"); } @Override protected HttpResponse run() { - keep("membership:Test AG 00.uuid", () -> + keep("Membership: Test AG 00", () -> httpPost("/api/hs/office/memberships", usingJsonBody(""" { - "partnerUuid": "${partner:Test AG.uuid}", + "partnerUuid": ${Partner: Test AG}, "memberNumberSuffix": "00", "validFrom": "2024-10-15", "membershipFeeBillable": "true" diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/partner/CreatePartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/partner/CreatePartner.java index 0c6d7298..ddd51efc 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/partner/CreatePartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/usecases/partner/CreatePartner.java @@ -7,10 +7,6 @@ import org.springframework.http.HttpStatus; public class CreatePartner extends UseCase { - public CreatePartner(final UseCaseTest testSuite) { - super(testSuite, null); - } - public CreatePartner(final UseCaseTest testSuite, final String resultAlias) { super(testSuite, resultAlias); } @@ -18,17 +14,17 @@ public class CreatePartner extends UseCase { @Override protected HttpResponse run() { - keep("Person: Test AG", () -> + keep("Person: %{tradeName}", () -> httpPost("/api/hs/office/persons", usingJsonBody(""" { "personType": ${personType}, "tradeName": ${tradeName} - }, + } """)) .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) ); - keep("Contact: Test AG - Bord of Directors", () -> + keep("Contact: %{tradeName} - Bord of Directors", () -> httpPost("/api/hs/office/contacts", usingJsonBody(""" { "caption": ${contactCaption}, @@ -45,8 +41,8 @@ public class CreatePartner extends UseCase { "partnerNumber": ${partnerNumber}, "partnerRel": { "anchorUuid": ${Person: Hostsharing eG}, - "holderUuid": ${Person: Test AG}, - "contactUuid": ${Contact: Test AG - Bord of Directors} + "holderUuid": ${Person: %{tradeName}}, + "contactUuid": ${Contact: %{tradeName} - Bord of Directors} }, "details": { "registrationOffice": "Registergericht Hamburg",