From 63af33d00392750832c5645275277b925e47b0af Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 5 Nov 2024 13:58:31 +0100 Subject: [PATCH] feature/use-case-acceptance-tests-2 (#117) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/117 Reviewed-by: Marc Sandlus --- Jenkinsfile | 36 ++++- build.gradle | 55 ++++++- doc/scenarios/template.html | 124 ++++++++++++++++ etc/jenkinsAgent.Dockerfile | 6 +- .../hs-office/hs-office-sepamandates.yaml | 2 +- .../scenarios/HsOfficeScenarioTests.java | 112 ++++++++++++-- .../hs/office/scenarios/PathAssertion.java | 23 +++ .../hsadminng/hs/office/scenarios/README.md | 32 +++- .../hs/office/scenarios/ScenarioTest.java | 4 +- .../hs/office/scenarios/TemplateResolver.java | 140 +++++++++++++----- .../scenarios/TemplateResolverUnitTest.java | 73 +++++++++ .../hs/office/scenarios/TestReport.java | 4 +- .../hs/office/scenarios/UseCase.java | 78 ++++++++-- .../scenarios/UseCaseNotImplementedYet.java | 16 ++ .../contact/AddPhoneNumberToContactData.java | 51 +++++++ .../scenarios/contact/AmendContactData.java | 44 ++++++ .../RemovePhoneNumberFromContactData.java | 50 +++++++ .../scenarios/contact/ReplaceContactData.java | 64 ++++++++ .../CreateExternalDebitorForPartner.java | 2 +- .../debitor/CreateSelfDebitorForPartner.java | 3 +- .../debitor/CreateSepaMandateForDebitor.java | 20 ++- .../scenarios/debitor/DeleteDebitor.java | 6 +- .../debitor/DeleteSepaMandateForDebitor.java | 20 --- .../debitor/DontDeleteDefaultDebitor.java | 2 +- .../FinallyDeleteSepaMandateForDebitor.java | 31 ++++ .../InvalidateSepaMandateForDebitor.java | 15 +- .../AddOperationsContactToPartner.java | 13 +- .../partner/AddRepresentativeToPartner.java | 12 +- .../scenarios/partner/CreatePartner.java | 29 +++- .../scenarios/partner/DeletePartner.java | 6 +- .../office/scenarios/person/CreatePerson.java | 6 +- .../RemoveOperationsContactFromPartner.java | 26 +++- .../subscription/SubscribeToMailinglist.java | 2 +- .../UnsubscribeFromMailinglist.java | 8 +- ...ceSepaMandateControllerAcceptanceTest.java | 33 ++++- .../reflection/AnnotationFinder.java | 0 36 files changed, 1008 insertions(+), 140 deletions(-) create mode 100644 doc/scenarios/template.html create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteSepaMandateForDebitor.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java rename src/{main => test}/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java (100%) diff --git a/Jenkinsfile b/Jenkinsfile index 5c1722d6..dc466d28 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -26,9 +26,35 @@ pipeline { } } - stage ('Compile & Test') { + stage ('Compile') { steps { - sh './gradlew clean check --no-daemon -x pitest -x dependencyCheckAnalyze' + sh './gradlew clean processSpring compileJava compileTestJava --no-daemon' + } + } + + stage ('Tests') { + parallel { + stage('Unit-/Integration/Acceptance-Tests') { + steps { + sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets' + } + } + stage('Import-Tests') { + steps { + sh './gradlew importOfficeData importHostingAssets --no-daemon' + } + } + stage ('Scenario-Tests') { + steps { + sh './gradlew scenarioTests --no-daemon' + } + } + } + } + + stage ('Check') { + steps { + sh './gradlew check -x pitest -x dependencyCheckAnalyze --no-daemon' } } } @@ -45,6 +71,12 @@ pipeline { sourcePattern: 'src/main/java' ) + // archive scenario-test reports in HTML format + sh ''' + ./gradlew convertMarkdownToHtml + ''' + archiveArtifacts artifacts: 'doc/scenarios/*.html', allowEmptyArchive: true + // cleanup workspace cleanWs() } diff --git a/build.gradle b/build.gradle index 96b16673..ed7a290d 100644 --- a/build.gradle +++ b/build.gradle @@ -255,7 +255,7 @@ test { 'net.hostsharing.hsadminng.**.generated.**', ] useJUnitPlatform { - excludeTags 'import' + excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest' } } jacocoTestReport { @@ -344,6 +344,17 @@ tasks.register('importHostingAssets', Test) { mustRunAfter spotlessJava } +tasks.register('scenarioTests', Test) { + useJUnitPlatform { + includeTags 'scenarioTest' + } + + group 'verification' + description 'run the import jobs as tests' + + mustRunAfter spotlessJava +} + // pitest mutation testing pitest { targetClasses = ['net.hostsharing.hsadminng.**'] @@ -391,3 +402,45 @@ tasks.named("dependencyUpdates").configure { isNonStable(it.candidate.version) } } + + +// Generate HTML from Markdown scenario-test-reports using Pandoc: +tasks.register('convertMarkdownToHtml') { + description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.' + group = 'Conversion' + + // Define the template file and input directory + def templateFile = file('doc/scenarios/template.html') + + // Task configuration and execution + doFirst { + // Check if pandoc is installed + try { + exec { + commandLine 'pandoc', '--version' + } + } catch (Exception) { + throw new GradleException("Pandoc is not installed or not found in the system path.") + } + + // Check if the template file exists + if (!templateFile.exists()) { + throw new GradleException("Template file 'doc/scenarios/template.html' not found.") + } + } + + doLast { + // Gather all Markdown files in the current directory + fileTree(dir: '.', include: 'doc/scenarios/*.md').each { file -> + // Corrected way to create the output file path + def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html')) + + // Execute pandoc for each markdown file + exec { + commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath + } + + println "Converted ${file.name} to ${outputFile.name}" + } + } +} diff --git a/doc/scenarios/template.html b/doc/scenarios/template.html new file mode 100644 index 00000000..1bb16e52 --- /dev/null +++ b/doc/scenarios/template.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +$for(author-meta)$ + +$endfor$ +$if(date-meta)$ + +$endif$ + $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ + +$if(quotes)$ + +$endif$ +$if(highlighting-css)$ + +$endif$ +$for(css)$ + +$endfor$ +$if(math)$ + $math$ +$endif$ +$for(header-includes)$ + $header-includes$ +$endfor$ + + + + + $if(title)$ + + $endif$ +
+
+ $if(toc)$ +
+
+ + $toc$ + +
+
+ $endif$ +
+ + $if(abstract)$ +

$abstract-title$

+ $abstract$ + $endif$ + + $for(include-before)$ + $include-before$ + $endfor$ +$body$ + $for(include-after)$ + $include-after$ + $endfor$ +
+
+
+ + + + diff --git a/etc/jenkinsAgent.Dockerfile b/etc/jenkinsAgent.Dockerfile index 648e2f8e..f06e9e4f 100644 --- a/etc/jenkinsAgent.Dockerfile +++ b/etc/jenkinsAgent.Dockerfile @@ -1,10 +1,6 @@ FROM eclipse-temurin:21-jdk RUN apt-get update && \ - apt-get install -y bind9-utils && \ + apt-get install -y bind9-utils pandoc && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* - -# RUN mkdir /opt/app -# COPY japp.jar /opt -# CMD ["java", "-jar", "/opt/app/japp.jar"] diff --git a/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml b/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml index 3050ab79..724d8ece 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml @@ -7,7 +7,7 @@ get: parameters: - $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/assumedRoles' - - name: name + - name: iban in: query required: false schema: 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 85e86127..07723a1e 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 @@ -1,10 +1,14 @@ package net.hostsharing.hsadminng.hs.office.scenarios; import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.hs.office.scenarios.contact.AddPhoneNumberToContactData; +import net.hostsharing.hsadminng.hs.office.scenarios.contact.AmendContactData; +import net.hostsharing.hsadminng.hs.office.scenarios.contact.RemovePhoneNumberFromContactData; +import net.hostsharing.hsadminng.hs.office.scenarios.contact.ReplaceContactData; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateExternalDebitorForPartner; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSelfDebitorForPartner; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSepaMandateForDebitor; -import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteSepaMandateForDebitor; +import net.hostsharing.hsadminng.hs.office.scenarios.debitor.FinallyDeleteSepaMandateForDebitor; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDebitor; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor; import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership; @@ -43,14 +47,39 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(1010) - @Produces(explicitly = "Partner: Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Board of Directors"}) - void shouldCreatePartner() { + @Produces(explicitly = "Partner: Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Hamburg"}) + void shouldCreateLegalPersonAsPartner() { new CreatePartner(this) .given("partnerNumber", 31010) .given("personType", "LEGAL_PERSON") .given("tradeName", "Test AG") - .given("contactCaption", "Test AG - Board of Directors") - .given("emailAddress", "board-of-directors@test-ag.example.org") + .given("contactCaption", "Test AG - Hamburg") + .given("postalAddress", """ + Shanghai-Allee 1 + 20123 Hamburg + """) + .given("officePhoneNumber", "+49 40 654321-0") + .given("emailAddress", "hamburg@test-ag.example.org") + .doRun() + .keep(); + } + + @Test + @Order(1011) + @Produces(explicitly = "Partner: Michelle Matthieu", implicitly = {"Person: Michelle Matthieu", "Contact: Michelle Matthieu"}) + void shouldCreateNaturalPersonAsPartner() { + new CreatePartner(this) + .given("partnerNumber", 31011) + .given("personType", "NATURAL_PERSON") + .given("givenName", "Michelle") + .given("familyName", "Matthieu") + .given("contactCaption", "Michelle Matthieu") + .given("postalAddress", """ + An der Wandse 34 + 22123 Hamburg + """) + .given("officePhoneNumber", "+49 40 123456") + .given("emailAddress", "michelle.matthieu@example.org") .doRun() .keep(); } @@ -106,6 +135,53 @@ class HsOfficeScenarioTests extends ScenarioTest { .doRun(); } + @Test + @Order(1100) + @Requires("Partner: Michelle Matthieu") + void shouldAmendContactData() { + new AmendContactData(this) + .given("partnerName", "Matthieu") + .given("newEmailAddress", "michelle@matthieu.example.org") + .doRun(); + } + + @Test + @Order(1101) + @Requires("Partner: Michelle Matthieu") + void shouldAddPhoneNumberToContactData() { + new AddPhoneNumberToContactData(this) + .given("partnerName", "Matthieu") + .given("phoneNumberKeyToAdd", "mobile") + .given("phoneNumberToAdd", "+49 152 1234567") + .doRun(); + } + + @Test + @Order(1102) + @Requires("Partner: Michelle Matthieu") + void shouldRemovePhoneNumberFromContactData() { + new RemovePhoneNumberFromContactData(this) + .given("partnerName", "Matthieu") + .given("phoneNumberKeyToRemove", "office") + .doRun(); + } + + @Test + @Order(1103) + @Requires("Partner: Test AG") + void shouldReplaceContactData() { + new ReplaceContactData(this) + .given("partnerName", "Test AG") + .given("newContactCaption", "Test AG - Norden") + .given("newPostalAddress", """ + Am Hafen 11 + 26506 Norden + """) + .given("newOfficePhoneNumber", "+49 4931 654321-0") + .given("newEmailAddress", "norden@test-ag.example.org") + .doRun(); + } + @Test @Order(2010) @Requires("Partner: Test AG") @@ -173,10 +249,18 @@ class HsOfficeScenarioTests extends ScenarioTest { @Produces("SEPA-Mandate: Test AG") void shouldCreateSepaMandateForDebitor() { new CreateSepaMandateForDebitor(this) - .given("debitor", "Test AG") - .given("memberNumberSuffix", "00") - .given("validFrom", "2024-10-15") - .given("membershipFeeBillable", "true") + // existing debitor + .given("debitorNumber", "3101000") + + // new sepa-mandate + .given("mandateReference", "Test AG - main debitor") + .given("mandateAgreement", "2022-10-12") + .given("mandateValidFrom", "2024-10-15") + + // new bank-account + .given("bankAccountHolder", "Test AG - debit bank account") + .given("bankAccountIBAN", "DE02701500000000594937") + .given("bankAccountBIC", "SSKMDEMM") .doRun() .keep(); } @@ -186,17 +270,17 @@ class HsOfficeScenarioTests extends ScenarioTest { @Requires("SEPA-Mandate: Test AG") void shouldInvalidateSepaMandateForDebitor() { new InvalidateSepaMandateForDebitor(this) - .given("sepaMandateUuid", "%{SEPA-Mandate: Test AG}") - .given("validUntil", "2025-09-30") + .given("bankAccountIBAN", "DE02701500000000594937") + .given("mandateValidUntil", "2025-09-30") .doRun(); } @Test @Order(3109) @Requires("SEPA-Mandate: Test AG") - void shouldDeleteSepaMandateForDebitor() { - new DeleteSepaMandateForDebitor(this) - .given("sepaMandateUuid", "%{SEPA-Mandate: Test AG}") + void shouldFinallyDeleteSepaMandateForDebitor() { + new FinallyDeleteSepaMandateForDebitor(this) + .given("bankAccountIBAN", "DE02701500000000594937") .doRun(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java new file mode 100644 index 00000000..ffd9df8e --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java @@ -0,0 +1,23 @@ +package net.hostsharing.hsadminng.hs.office.scenarios; + +import net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse; + +import java.util.function.Consumer; + +public class PathAssertion { + + private final String path; + + public PathAssertion(final String path) { + this.path = path; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Consumer contains(final String resolvableValue) { + return response -> response.path(path).contains(ScenarioTest.resolve(resolvableValue)); + } + + public Consumer doesNotExist() { + return response -> response.path(path).isNull(); // here, null Optional means key not found in JSON + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md index 36e7c3c8..e8384ba2 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md @@ -63,4 +63,34 @@ Here, use-cases can be re-used, usually with different data. ### The Use-Case Itself -The use-case +The use-case is implemented by the `run()`-method which contains HTTP-calls. + +Each HTTP-call is wrapped into either `obtain(...)` to keep the result in a placeholder variable, +the variable name is also used as a title. +Or it's wrapped into a `withTitle(...)` to assign a title. + +The HTTP-call is followed by some assertions, e.g. the HTTP status and JSON-path-expression-matchers. + +Use `${...}` for placeholders which need to be replaced with JSON quotes +(e.g. strings are quoted, numbers are not), +`%{...}` for placeholders which need to be rendered raw +and `&{...}` for placeholders which need to get URI-encoded. + +If `???` is added before the closing brace, the property is optional. +This means, if it's not available in the properties, `null` is used. + +Properties with null-values are removed from the JSON. +If you need to keep a null-value, e.g. to delete a property, +use `NULL` (all caps) in the template (not the variable value). + +A special syntax is the infix `???`-operator like in: `${%{var1???}???%{var2???}%{var3???}}`. +In this case the first non-null value is used. + + + +### The Use-Case Verification + +The verification-step is implemented by the `verify()`-method which usually contains a HTTP-HTTP-call. + +It can also contain a JSON-path verification to check if a certain value is in the result. + 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 efc11c54..4600584a 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 @@ -12,6 +12,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; +import org.testcontainers.shaded.org.apache.commons.lang3.ObjectUtils; import java.lang.reflect.Method; import java.util.HashMap; @@ -35,7 +36,7 @@ public abstract class ScenarioTest extends ContextBasedTest { @Override public String toString() { - return uuid.toString(); + return ObjectUtils.toString(uuid); } } @@ -68,7 +69,6 @@ public abstract class ScenarioTest extends ContextBasedTest { @AfterEach void cleanup() { // final TestInfo testInfo properties.clear(); - // FIXME: Delete all aliases as well to force HTTP GET queries in each scenario? testReport.close(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java index ccb8c96d..aaa8855a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java @@ -1,9 +1,57 @@ package net.hostsharing.hsadminng.hs.office.scenarios; +import org.apache.commons.lang3.StringUtils; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; public class TemplateResolver { + private final static Pattern pattern = Pattern.compile(",(\\s*})", Pattern.MULTILINE); + private static final String IF_NOT_FOUND_SYMBOL = "???"; + + enum PlaceholderPrefix { + RAW('%') { + @Override + String convert(final Object value) { + return value != null ? value.toString() : ""; + } + }, + JSON_QUOTED('$'){ + @Override + String convert(final Object value) { + return jsonQuoted(value); + } + }, + URI_ENCODED('&'){ + @Override + String convert(final Object value) { + return value != null ? URLEncoder.encode(value.toString(), StandardCharsets.UTF_8) : ""; + } + }; + + private final char prefixChar; + + PlaceholderPrefix(final char prefixChar) { + this.prefixChar = prefixChar; + } + + static boolean contains(final char givenChar) { + return Arrays.stream(values()).anyMatch(p -> p.prefixChar == givenChar); + } + + static PlaceholderPrefix ofPrefixChar(final char givenChar) { + return Arrays.stream(values()).filter(p -> p.prefixChar == givenChar).findFirst().orElseThrow(); + } + + abstract String convert(final Object value); + } + private final String template; private final Map properties; private final StringBuilder resolved = new StringBuilder(); @@ -15,18 +63,41 @@ public class TemplateResolver { } String resolve() { - copy(); - return resolved.toString(); + final var resolved = copy(); + final var withoutDroppedLines = dropLinesWithNullProperties(resolved); + final var result = removeDanglingCommas(withoutDroppedLines); + return result; } - private void copy() { + private static String removeDanglingCommas(final String withoutDroppedLines) { + return pattern.matcher(withoutDroppedLines).replaceAll("$1"); + } + + private String dropLinesWithNullProperties(final String text) { + return Arrays.stream(text.split("\n")) + .filter(TemplateResolver::keepLine) + .map(TemplateResolver::keptNullValues) + .collect(Collectors.joining("\n")); + } + + private static boolean keepLine(final String line) { + final var trimmed = line.trim(); + return !trimmed.endsWith("null,") && !trimmed.endsWith("null"); + } + + private static String keptNullValues(final String line) { + return line.replace(": NULL", ": null"); + } + + private String copy() { while (hasMoreChars()) { - if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') { + if (PlaceholderPrefix.contains(currentChar()) && nextChar() == '{') { startPlaceholder(currentChar()); } else { resolved.append(fetchChar()); } } + return resolved.toString(); } private boolean hasMoreChars() { @@ -41,7 +112,7 @@ public class TemplateResolver { if (currentChar() == '}') { --nested; placeholder.append(fetchChar()); - } else if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') { + } else if (PlaceholderPrefix.contains (currentChar()) && nextChar() == '{') { ++nested; placeholder.append(fetchChar()); } else { @@ -50,20 +121,33 @@ public class TemplateResolver { } final var name = new TemplateResolver(placeholder.toString(), properties).resolve(); final var value = propVal(name); - if ( intro == '%') { - resolved.append(value); - } else { - resolved.append(optionallyQuoted(value)); - } + resolved.append( + PlaceholderPrefix.ofPrefixChar(intro).convert(value) + ); skipChar('}'); } - private Object propVal(final String name) { - final var val = properties.get(name); - if (val == null) { - throw new IllegalStateException("Missing required property: " + name); + private Object propVal(final String nameExpression) { + if (nameExpression.endsWith(IF_NOT_FOUND_SYMBOL)) { + final String pureName = nameExpression.substring(0, nameExpression.length() - IF_NOT_FOUND_SYMBOL.length()); + return properties.get(pureName); + } else if (nameExpression.contains(IF_NOT_FOUND_SYMBOL)) { + final var parts = StringUtils.split(nameExpression, IF_NOT_FOUND_SYMBOL); + return Arrays.stream(parts).filter(Objects::nonNull).findFirst().orElseGet(() -> { + if ( parts[parts.length-1].isEmpty() ) { + // => whole expression ends with IF_NOT_FOUND_SYMBOL, thus last null element was optional + return null; + } + // => last alternative element in expression was null and not optional + throw new IllegalStateException("Missing required value in property-chain: " + nameExpression); + }); + } else { + final var val = properties.get(nameExpression); + if (val == null) { + throw new IllegalStateException("Missing required property: " + nameExpression); + } + return val; } - return val; } private void skipChar(final char expectedChar) { @@ -104,35 +188,13 @@ public class TemplateResolver { return template.charAt(position+1); } - private static String optionallyQuoted(final Object value) { + private static String jsonQuoted(final Object value) { return switch (value) { + case null -> null; case Boolean bool -> bool.toString(); case Number number -> number.toString(); case String string -> "\"" + string.replace("\n", "\\n") + "\""; 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/scenarios/TemplateResolverUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java new file mode 100644 index 00000000..2d63e204 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java @@ -0,0 +1,73 @@ +package net.hostsharing.hsadminng.hs.office.scenarios; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class TemplateResolverUnitTest { + + @Test + void resolveTemplate() { + final var resolved = new TemplateResolver(""" + with optional JSON quotes: + + ${boolean}, + ${numeric}, + ${simple placeholder}, + ${nested %{name}}, + ${with-special-chars} + + and without quotes: + + %{boolean}, + %{numeric}, + %{simple placeholder}, + %{nested %{name}}, + %{with-special-chars} + + and uri-encoded: + + &{boolean}, + &{numeric}, + &{simple placeholder}, + &{nested %{name}}, + &{with-special-chars} + """, + Map.ofEntries( + Map.entry("name", "placeholder"), + Map.entry("boolean", true), + Map.entry("numeric", 42), + Map.entry("simple placeholder", "einfach"), + Map.entry("nested placeholder", "verschachtelt"), + Map.entry("with-special-chars", "3&3 AG") + )).resolve(); + + assertThat(resolved).isEqualTo(""" + with optional JSON quotes: + + true, + 42, + "einfach", + "verschachtelt", + "3&3 AG" + + and without quotes: + + true, + 42, + einfach, + verschachtelt, + 3&3 AG + + and uri-encoded: + + true, + 42, + einfach, + verschachtelt, + 3%263+AG + """.trim()); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java index 02123a14..8aba4319 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java @@ -57,7 +57,9 @@ public class TestReport { } public void close() { - markdownReport.close(); + if (markdownReport != null) { + markdownReport.close(); + } } private static Object orderNumber(final Method method) { 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 710e4ae1..77aa7b93 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 @@ -8,6 +8,7 @@ import lombok.Getter; import lombok.SneakyThrows; import net.hostsharing.hsadminng.reflection.AnnotationFinder; import org.apache.commons.collections4.map.LinkedMap; +import org.assertj.core.api.OptionalAssert; import org.hibernate.AssertionFailure; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -19,17 +20,21 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; 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.jupiter.api.Assertions.fail; import static org.junit.platform.commons.util.StringUtils.isBlank; import static org.junit.platform.commons.util.StringUtils.isNotBlank; @@ -80,11 +85,16 @@ public abstract class UseCase> { } }) ); - return run(); + final var response = run(); + verify(); + return response; } protected abstract HttpResponse run(); + protected void verify() { + } + public final UseCase given(final String propName, final Object propValue) { givenProperties.put(propName, propValue); ScenarioTest.putProperty(propName, propValue); @@ -101,26 +111,30 @@ public abstract class UseCase> { final Function extractor, final String... extraInfo) { withTitle(ScenarioTest.resolve(alias), () -> { - http.get().keep(extractor); + final var response = http.get().keep(extractor); Arrays.stream(extraInfo).forEach(testReport::printPara); + return response; }); } public final void obtain(final String alias, final Supplier http, final String... extraInfo) { withTitle(ScenarioTest.resolve(alias), () -> { - http.get().keep(); + final var response = http.get().keep(); Arrays.stream(extraInfo).forEach(testReport::printPara); + return response; }); } - private void withTitle(final String title, final Runnable code) { + public HttpResponse withTitle(final String title, final Supplier code) { this.nextTitle = title; - code.run(); + final var response = code.get(); this.nextTitle = null; + return response; } @SneakyThrows - public final HttpResponse httpGet(final String uriPath) { + public final HttpResponse httpGet(final String uriPathWithPlaceholders) { + final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders); final var request = HttpRequest.newBuilder() .GET() .uri(new URI("http://localhost:" + testSuite.port + uriPath)) @@ -132,7 +146,8 @@ public abstract class UseCase> { } @SneakyThrows - public final HttpResponse httpPost(final String uriPath, final JsonTemplate bodyJsonTemplate) { + public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) { + final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders); final var requestBody = bodyJsonTemplate.resolvePlaceholders(); final var request = HttpRequest.newBuilder() .POST(BodyPublishers.ofString(requestBody)) @@ -146,7 +161,8 @@ public abstract class UseCase> { } @SneakyThrows - public final HttpResponse httpPatch(final String uriPath, final JsonTemplate bodyJsonTemplate) { + public final HttpResponse httpPatch(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) { + final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders); final var requestBody = bodyJsonTemplate.resolvePlaceholders(); final var request = HttpRequest.newBuilder() .method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody)) @@ -160,7 +176,8 @@ public abstract class UseCase> { } @SneakyThrows - public final HttpResponse httpDelete(final String uriPath) { + public final HttpResponse httpDelete(final String uriPathWithPlaceholders) { + final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders); final var request = HttpRequest.newBuilder() .DELETE() .uri(new URI("http://localhost:" + testSuite.port + uriPath)) @@ -172,12 +189,27 @@ public abstract class UseCase> { return new HttpResponse(HttpMethod.DELETE, uriPath, null, response); } + protected PathAssertion path(final String path) { + return new PathAssertion(path); + } + + protected void verify( + final String title, + final Supplier http, + final Consumer... assertions) { + withTitle(ScenarioTest.resolve(title), () -> { + final var response = http.get(); + Arrays.stream(assertions).forEach(assertion -> assertion.accept(response)); + return response; + }); + } + public final UUID uuid(final String alias) { return ScenarioTest.uuid(alias); } public String uriEncoded(final String text) { - return encode(ScenarioTest.resolve(text)); + return encode(ScenarioTest.resolve(text), StandardCharsets.UTF_8); } public static class JsonTemplate { @@ -193,7 +225,7 @@ public abstract class UseCase> { } } - public class HttpResponse { + public final class HttpResponse { @Getter private final java.net.http.HttpResponse response; @@ -232,7 +264,7 @@ public abstract class UseCase> { return this; } - public void keep(final Function extractor) { + public HttpResponse keep(final Function extractor) { final var alias = nextTitle != null ? nextTitle : resultAlias; assertThat(alias).as("cannot keep result, no alias found").isNotNull(); @@ -240,14 +272,16 @@ public abstract class UseCase> { ScenarioTest.putAlias( alias, new ScenarioTest.Alias<>(UseCase.this.getClass(), UUID.fromString(value))); + return this; } - public void keep() { + public HttpResponse keep() { final var alias = nextTitle != null ? nextTitle : resultAlias; assertThat(alias).as("cannot keep result, no alias found").isNotNull(); ScenarioTest.putAlias( alias, new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid)); + return this; } @SneakyThrows @@ -263,7 +297,21 @@ public abstract class UseCase> { @SneakyThrows public String getFromBody(final String path) { - return JsonPath.parse(response.body()).read(path); + return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path)); + } + + @SneakyThrows + public Optional getFromBodyAsOptional(final String path) { + try { + return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path))); + } catch (final Exception e) { + return null; // means the property did not exist at all, not that it was there with value null + } + } + + @SneakyThrows + public OptionalAssert path(final String path) { + return assertThat(getFromBodyAsOptional(path)); } @SneakyThrows @@ -274,6 +322,8 @@ public abstract class UseCase> { testReport.printLine("\n### " + nextTitle + "\n"); } else if (resultAlias != null) { testReport.printLine("\n### " + resultAlias + "\n"); + } else { + fail("please wrap the http...-call in the UseCase using `withTitle(...)`"); } // the request diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java new file mode 100644 index 00000000..d21d6413 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java @@ -0,0 +1,16 @@ +package net.hostsharing.hsadminng.hs.office.scenarios; + +import static org.assertj.core.api.Assumptions.assumeThat; + +public class UseCaseNotImplementedYet extends UseCase { + + public UseCaseNotImplementedYet(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + assumeThat(false).isTrue(); // makes the test gray + return null; + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java new file mode 100644 index 00000000..3e837195 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java @@ -0,0 +1,51 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.contact; + +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import org.springframework.http.HttpStatus; + + +import static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.OK; + +public class AddPhoneNumberToContactData extends UseCase { + + public AddPhoneNumberToContactData(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + + obtain( + "partnerContactUuid", + () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) + .expecting(OK).expecting(JSON), + response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"), + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." + ); + + withTitle("Patch the Additional Phone-Number into the Contact", () -> + httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody(""" + { + "phoneNumbers": { + ${phoneNumberKeyToAdd}: ${phoneNumberToAdd} + } + } + """)) + .expecting(HttpStatus.OK) + ); + + return null; + } + + @Override + protected void verify() { + verify( + "Verify if the New Phone Number Got Added", + () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) + .expecting(OK).expecting(JSON).expectArrayElements(1), + path("[0].contact.phoneNumbers.%{phoneNumberKeyToAdd}").contains("%{phoneNumberToAdd}") + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java new file mode 100644 index 00000000..8f45d83a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java @@ -0,0 +1,44 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.contact; + +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import org.springframework.http.HttpStatus; + +import static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.OK; + +public class AmendContactData extends UseCase { + + public AmendContactData(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + + obtain("partnerContactUuid", () -> + httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) + .expecting(OK).expecting(JSON), + response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"), + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." + ); + + withTitle("Patch the New Phone Number Into the Contact", () -> + httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody(""" + { + "caption": ${newContactCaption???}, + "postalAddress": ${newPostalAddress???}, + "phoneNumbers": { + "office": ${newOfficePhoneNumber???} + }, + "emailAddresses": { + "main": ${newEmailAddress???} + } + } + """)) + .expecting(HttpStatus.OK) + ); + + return null; + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java new file mode 100644 index 00000000..c499ee71 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java @@ -0,0 +1,50 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.contact; + +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import org.springframework.http.HttpStatus; + +import static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.OK; + +public class RemovePhoneNumberFromContactData extends UseCase { + + public RemovePhoneNumberFromContactData(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + + obtain( + "partnerContactUuid", + () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) + .expecting(OK).expecting(JSON), + response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"), + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." + ); + + withTitle("Patch the Additional Phone-Number into the Contact", () -> + httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody(""" + { + "phoneNumbers": { + ${phoneNumberKeyToRemove}: NULL + } + } + """)) + .expecting(HttpStatus.OK) + ); + + return null; + } + + @Override + protected void verify() { + verify( + "Verify if the New Phone Number Got Added", + () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) + .expecting(OK).expecting(JSON).expectArrayElements(1), + path("[0].contact.phoneNumbers.%{phoneNumberKeyToRemove}").doesNotExist() + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java new file mode 100644 index 00000000..b4916dda --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java @@ -0,0 +1,64 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.contact; + +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; + +import static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.OK; + +public class ReplaceContactData extends UseCase { + + public ReplaceContactData(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + + obtain("partnerRelationUuid", () -> + httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) + .expecting(OK).expecting(JSON), + response -> response.expectArrayElements(1).getFromBody("[0].uuid"), + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." + ); + + obtain("Contact: %{newContactCaption}", () -> + httpPost("/api/hs/office/contacts", usingJsonBody(""" + { + "caption": ${newContactCaption}, + "postalAddress": ${newPostalAddress???}, + "phoneNumbers": { + "phone": ${newOfficePhoneNumber???} + }, + "emailAddresses": { + "main": ${newEmailAddress???} + } + } + """)) + .expecting(CREATED).expecting(JSON), + "Please check first if that contact already exists, if so, use it's UUID below." + ); + + withTitle("Replace the Contact-Reference in the Partner-Relation", () -> + httpPatch("/api/hs/office/relations/%{partnerRelationUuid}", usingJsonBody(""" + { + "contactUuid": ${Contact: %{newContactCaption}} + } + """)) + .expecting(OK) + ); + + return null; + } + + @Override + protected void verify() { + verify( + "Verify if the Contact-Relation Got Replaced in the Partner-Relation", + () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) + .expecting(OK).expecting(JSON).expectArrayElements(1), + path("[0].contact.caption").contains("%{newContactCaption}") + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java index e9afdcc2..194d4513 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java @@ -26,7 +26,7 @@ public class CreateExternalDebitorForPartner extends UseCase response.expectArrayElements(1).getFromBody("[0].uuid"), - "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one." + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." ); obtain("BankAccount: Billing GmbH - refund bank account", () -> 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 91a21a00..fc16adb3 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 @@ -19,8 +19,7 @@ public class CreateSelfDebitorForPartner extends UseCase response.expectArrayElements(1).getFromBody("[0].holder.uuid"), - "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one.", - "**HINT**: With production data, you might get multiple results and have to decide which is the right one." + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." ); obtain("BankAccount: Test AG - refund bank account", () -> diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java index 9bbba7a6..606e2320 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.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 CreateSepaMandateForDebitor extends UseCase { @@ -14,12 +15,19 @@ public class CreateSepaMandateForDebitor extends UseCase + httpGet("/api/hs/office/debitors?debitorNumber=&{debitorNumber}") + .expecting(OK).expecting(JSON), + response -> response.expectArrayElements(1).getFromBody("[0].uuid") + ); + obtain("BankAccount: Test AG - debit bank account", () -> httpPost("/api/hs/office/bankaccounts", usingJsonBody(""" { - "holder": "Test AG - debit bank account", - "iban": "DE02701500000000594937", - "bic": "SSKMDEMM" + "holder": ${bankAccountHolder}, + "iban": ${bankAccountIBAN}, + "bic": ${bankAccountBIC} } """)) .expecting(CREATED).expecting(JSON) @@ -29,9 +37,9 @@ public class CreateSepaMandateForDebitor extends UseCase { @Override protected HttpResponse run() { - httpDelete("/api/hs/office/debitors/" + uuid("Debitor: Test AG - delete debitor")) - .expecting(HttpStatus.NO_CONTENT); + withTitle("Delete the Debitor using its UUID", () -> + httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - delete debitor}") + .expecting(HttpStatus.NO_CONTENT) + ); return null; } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteSepaMandateForDebitor.java deleted file mode 100644 index e5c9b94a..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteSepaMandateForDebitor.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.hostsharing.hsadminng.hs.office.scenarios.debitor; - -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import org.springframework.http.HttpStatus; - - -public class DeleteSepaMandateForDebitor extends UseCase { - - public DeleteSepaMandateForDebitor(final ScenarioTest testSuite) { - super(testSuite); - } - - @Override - protected HttpResponse run() { - httpDelete("/api/hs/office/sepamandates/" + uuid("SEPA-Mandate: Test AG")) - .expecting(HttpStatus.NO_CONTENT); - return null; - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java index 82aae503..e5459c88 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java @@ -12,7 +12,7 @@ public class DontDeleteDefaultDebitor extends UseCase @Override protected HttpResponse run() { - httpDelete("/api/hs/office/debitors/" + uuid("Debitor: Test AG - main debitor")) + httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - main debitor}") // TODO.spec: should be CONFLICT or CLIENT_ERROR for Debitor "00" - but how to delete Partners? .expecting(HttpStatus.NO_CONTENT); return null; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java new file mode 100644 index 00000000..224a98f3 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java @@ -0,0 +1,31 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.debitor; + +import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import org.springframework.http.HttpStatus; + +import static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.OK; + +public class FinallyDeleteSepaMandateForDebitor extends UseCase { + + public FinallyDeleteSepaMandateForDebitor(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + + obtain("SEPA-Mandate: %{bankAccountIBAN}", () -> + httpGet("/api/hs/office/sepamandates?iban=&{bankAccountIBAN}") + .expecting(OK).expecting(JSON), + response -> response.expectArrayElements(1).getFromBody("[0].uuid"), + "With production data, the bank-account could be used in multiple SEPA-mandates, make sure to use the right one!" + ); + + // TODO.spec: When to allow actual deletion of SEPA-mandates? Add constraint accordingly. + return withTitle("Delete the SEPA-Mandate by its UUID", () -> httpDelete("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}") + .expecting(HttpStatus.NO_CONTENT) + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java index 9d13706e..3af160c7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java @@ -15,11 +15,20 @@ public class InvalidateSepaMandateForDebitor extends UseCase + httpGet("/api/hs/office/sepamandates?iban=&{bankAccountIBAN}") + .expecting(OK).expecting(JSON), + response -> response.expectArrayElements(1).getFromBody("[0].uuid"), + "With production data, the bank-account could be used in multiple SEPA-mandates, make sure to use the right one!" + ); + + return withTitle("Patch the End of the Mandate into the SEPA-Mandate", () -> + httpPatch("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}", usingJsonBody(""" { - "validUntil": ${validUntil} + "validUntil": ${mandateValidUntil} } """)) - .expecting(OK).expecting(JSON); + .expecting(OK).expecting(JSON) + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java index 6e41ce76..6c1fd1dd 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java @@ -22,7 +22,7 @@ public class AddOperationsContactToPartner extends UseCase response.expectArrayElements(1).getFromBody("[0].uuid"), - "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one." + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." ); obtain("Person: %{operationsContactGivenName} %{operationsContactFamilyName}", () -> @@ -64,4 +64,15 @@ public class AddOperationsContactToPartner extends UseCase httpGet("/api/hs/office/relations?relationType=OPERATIONS&personData=" + uriEncoded( + "%{operationsContactFamilyName}")) + .expecting(OK).expecting(JSON).expectArrayElements(1), + path("[0].contact.caption").contains("%{operationsContactGivenName} %{operationsContactFamilyName}") + ); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java index cb5c8136..1914cbb3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java @@ -22,7 +22,7 @@ public class AddRepresentativeToPartner extends UseCase response.expectArrayElements(1).getFromBody("[0].uuid"), - "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one." + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." ); obtain("Person: %{representativeGivenName} %{representativeFamilyName}", () -> @@ -65,4 +65,14 @@ public class AddRepresentativeToPartner extends UseCase httpGet("/api/hs/office/relations?relationType=REPRESENTATIVE&personData=" + uriEncoded("%{representativeFamilyName}")) + .expecting(OK).expecting(JSON).expectArrayElements(1), + path("[0].contact.caption").contains("%{representativeGivenName} %{representativeFamilyName}") + ); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java index c96acbdf..eb7e7d59 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java @@ -28,22 +28,28 @@ public class CreatePartner extends UseCase { "Even in production data we expect this query to return just a single result." // TODO.impl: add constraint? ); - obtain("Person: %{tradeName}", () -> + obtain("Person: %{%{tradeName???}???%{givenName???} %{familyName???}}", () -> httpPost("/api/hs/office/persons", usingJsonBody(""" { - "personType": ${personType}, - "tradeName": ${tradeName} + "personType": ${personType???}, + "tradeName": ${tradeName???}, + "givenName": ${givenName???}, + "familyName": ${familyName???} } """)) .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) ); - obtain("Contact: %{tradeName} - Board of Directors", () -> + obtain("Contact: %{contactCaption}", () -> httpPost("/api/hs/office/contacts", usingJsonBody(""" { "caption": ${contactCaption}, + "postalAddress": ${postalAddress???}, + "phoneNumbers": { + "office": ${officePhoneNumber???} + }, "emailAddresses": { - "main": ${emailAddress} + "main": ${emailAddress???} } } """)) @@ -55,8 +61,8 @@ public class CreatePartner extends UseCase { "partnerNumber": ${partnerNumber}, "partnerRel": { "anchorUuid": ${Person: Hostsharing eG}, - "holderUuid": ${Person: %{tradeName}}, - "contactUuid": ${Contact: %{tradeName} - Board of Directors} + "holderUuid": ${Person: %{%{tradeName???}???%{givenName???} %{familyName???}}}, + "contactUuid": ${Contact: %{contactCaption}} }, "details": { "registrationOffice": "Registergericht Hamburg", @@ -66,4 +72,13 @@ public class CreatePartner extends UseCase { """)) .expecting(HttpStatus.CREATED).expecting(ContentType.JSON); } + + @Override + protected void verify() { + verify( + "Verify the New Partner Relation", + () -> httpGet("/api/hs/office/relations?relationType=PARTNER&contactData=&{contactCaption}") + .expecting(OK).expecting(JSON).expectArrayElements(1) + ); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java index ae24dfd1..4453d959 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java @@ -18,8 +18,10 @@ public class DeletePartner extends UseCase { @Override protected HttpResponse run() { - httpDelete("/api/hs/office/partners/" + uuid("Partner: Delete AG")) - .expecting(HttpStatus.NO_CONTENT); + withTitle("Delete Partner by its UUID", () -> + httpDelete("/api/hs/office/partners/&{Partner: Delete AG}") + .expecting(HttpStatus.NO_CONTENT) + ); return null; } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java index 56db97e8..4a43503c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java @@ -14,12 +14,14 @@ public class CreatePerson extends UseCase { @Override protected HttpResponse run() { - return httpPost("/api/hs/office/persons", usingJsonBody(""" + return withTitle("Create the Person", () -> + httpPost("/api/hs/office/persons", usingJsonBody(""" { "personType": ${personType}, "tradeName": ${tradeName} } """)) - .expecting(HttpStatus.CREATED).expecting(ContentType.JSON); + .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java index 64584075..c30019e7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java @@ -1,9 +1,10 @@ package net.hostsharing.hsadminng.hs.office.scenarios.subscription; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.http.HttpStatus.OK; @@ -16,14 +17,27 @@ public class RemoveOperationsContactFromPartner extends UseCase - httpGet("/api/hs/office/relations?relationType=OPERATIONS&name=" + uriEncoded("%{operationsContactPerson}")) + obtain("Operations-Contact: %{operationsContactPerson}", + () -> + httpGet("/api/hs/office/relations?relationType=OPERATIONS&name=" + uriEncoded( + "%{operationsContactPerson}")) .expecting(OK).expecting(JSON), response -> response.expectArrayElements(1).getFromBody("[0].uuid"), - "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one." + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." ); - return httpDelete("/api/hs/office/relations/" + uuid("Operations-Contact: %{operationsContactPerson}")) - .expecting(NO_CONTENT); + return withTitle("Delete the Contact", () -> + httpDelete("/api/hs/office/relations/&{Operations-Contact: %{operationsContactPerson}}") + .expecting(NO_CONTENT) + ); + } + + @Override + protected void verify() { + verify( + "Verify the New OPERATIONS Relation", + () -> httpGet("/api/hs/office/relations/&{Operations-Contact: %{operationsContactPerson}}") + .expecting(NOT_FOUND) + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java index 3c84603f..3e4ae74b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java @@ -22,7 +22,7 @@ public class SubscribeToMailinglist extends UseCase { httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}")) .expecting(OK).expecting(JSON), response -> response.expectArrayElements(1).getFromBody("[0].uuid"), - "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one." + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." ); obtain("Person: %{subscriberGivenName} %{subscriberFamilyName}", () -> diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java index 6f059902..dfb17ea1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java @@ -22,10 +22,12 @@ public class UnsubscribeFromMailinglist extends UseCase response.expectArrayElements(1).getFromBody("[0].uuid"), - "In production data this query could result in multiple outputs. In that case, you have to find out which is the right one." + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." ); - return httpDelete("/api/hs/office/relations/" + uuid("Subscription: %{subscriberEMailAddress}")) - .expecting(NO_CONTENT); + return withTitle("Delete the Subscriber-Relation by its UUID", () -> + httpDelete("/api/hs/office/relations/&{Subscription: %{subscriberEMailAddress}}") + .expecting(NO_CONTENT) + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java index 89b25f35..48ed0d9c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java @@ -8,7 +8,6 @@ import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountReposi import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; -import org.json.JSONException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -58,7 +57,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl class ListSepaMandates { @Test - void globalAdmin_canViewAllSepaMandates_ifNoCriteriaGiven() throws JSONException { + void globalAdmin_canViewAllSepaMandates_ifNoCriteriaGiven() { RestAssured // @formatter:off .given() @@ -97,6 +96,36 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl """)); // @formatter:on } + + @Test + void globalAdmin_canFindSepaMandateByName() { + + RestAssured // @formatter:off + .given() + .header("current-subject", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/sepamandates?iban=DE02120300000000202051") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .log().all() + .body("", lenientlyEquals(""" + [ + { + "debitor": { "debitorNumber": 1000111 }, + "bankAccount": { + "iban": "DE02120300000000202051", + "holder": "First GmbH" + }, + "reference": "ref-10001-11", + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + } + ] + """)); + // @formatter:on + } } @Nested diff --git a/src/main/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java b/src/test/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java similarity index 100% rename from src/main/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java rename to src/test/java/net/hostsharing/hsadminng/reflection/AnnotationFinder.java