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)$
+
+
+
+
$title$
+
+ $for(author)$
+ $author$
+ $endfor$
+ $if(date)$
+ $date$
+ $endif$
+
+
+
+
+ $endif$
+
+
+ $if(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