Compare commits

...

4 Commits

Author SHA1 Message Date
Michael Hoennig
2eb1359fdd archive scenario-test reports 2024-10-30 15:05:17 +01:00
Michael Hoennig
c04da1ec4c generate HTML 2024-10-30 14:53:08 +01:00
Michael Hoennig
e3027579e7 explicitly fetch Debutor in CreateSepaMandateForDebitor 2024-10-30 14:15:23 +01:00
Michael Hoennig
1cb0ea1018 resolve placeholders in uriPath 2024-10-30 14:14:47 +01:00
9 changed files with 296 additions and 48 deletions

3
Jenkinsfile vendored
View File

@ -45,6 +45,9 @@ pipeline {
sourcePattern: 'src/main/java' sourcePattern: 'src/main/java'
) )
// archive scenario-test reports
archiveArtifacts artifacts: 'doc/scenarios/*.html', allowEmptyArchive: true
// cleanup workspace // cleanup workspace
cleanWs() cleanWs()
} }

124
doc/scenarios/template.html Normal file
View File

@ -0,0 +1,124 @@
<!doctype html>
<html $if(lang)$ lang="$lang$" $endif$>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--[if lt IE 9]>
<script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<!-- <link rel="stylesheet" type="text/css" href="template.css" /> -->
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/template.css" />
<link href="https://vjs.zencdn.net/5.4.4/video-js.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
<!-- <script type='text/javascript' src='menu/js/jquery.cookie.js'></script> -->
<!-- <script type='text/javascript' src='menu/js/jquery.hoverIntent.minified.js'></script> -->
<!-- <script type='text/javascript' src='menu/js/jquery.dcjqaccordion.2.7.min.js'></script> -->
<!-- <link href="menu/css/skins/blue.css" rel="stylesheet" type="text/css" /> -->
<!-- <link href="menu/css/skins/graphite.css" rel="stylesheet" type="text/css" /> -->
<!-- <link href="menu/css/skins/grey.css" rel="stylesheet" type="text/css" /> -->
<!-- <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> -->
<!-- <script src="script.js"></script> -->
<!-- <script src="jquery.sticky-kit.js "></script> -->
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.cookie.js'></script>
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.hoverIntent.minified.js'></script>
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.dcjqaccordion.2.7.min.js'></script>
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/blue.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/graphite.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/grey.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/ryangrose/easy-pandoc-templates@948e28e5/css/elegant_bootstrap.css" rel="stylesheet" type="text/css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/script.js"></script>
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/jquery.sticky-kit.js"></script>
<meta name="generator" content="pandoc" />
$for(author-meta)$
<meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$
<meta name="date" content="$date-meta$" />
$endif$
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
<style type="text/css">code{white-space: pre;}</style>
$if(quotes)$
<style type="text/css">q { quotes: "“" "”" "" ""; }</style>
$endif$
$if(highlighting-css)$
<style type="text/css">
$highlighting-css$
</style>
$endif$
$for(css)$
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$/>
$endfor$
$if(math)$
$math$
$endif$
$for(header-includes)$
$header-includes$
$endfor$
</head>
<body>
$if(title)$
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container">
<span class="doc-title">$title$</span>
<ul class="nav pull-right doc-info">
$for(author)$
<li><p class="navbar-text">$author$</p></li>
$endfor$
$if(date)$
<li><p class="navbar-text">$date$</p></li>
$endif$
</ul>
</div>
</div>
</div>
$endif$
<div class="container">
<div class="row">
$if(toc)$
<div id="$idprefix$TOC" class="span3">
<div class="well toc">
$toc$
</div>
</div>
$endif$
<div class="span$if(toc)$9$else$12$endif$">
$if(abstract)$
<H1>$abstract-title$</H1>
$abstract$
$endif$
$for(include-before)$
$include-before$
$endfor$
$body$
$for(include-after)$
$include-after$
$endfor$
</div>
</div>
</div>
<script src="https://vjs.zencdn.net/5.4.4/video.js"></script>
</body>
</html>

11
doc/scenarios/to-html Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
# This script loops over all markdown (.md) files in the current directory
# and converts each to an HTML file using pandoc using template.html.
# Origin of the template (GPL v3.0):
# https://github.com/ryangrose/easy-pandoc-templates/blob/master/html/easy_template.html
for file in *.md; do
pandoc "$file" --template template.html -o "${file%.md}.html"
done

View File

@ -173,10 +173,18 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Produces("SEPA-Mandate: Test AG") @Produces("SEPA-Mandate: Test AG")
void shouldCreateSepaMandateForDebitor() { void shouldCreateSepaMandateForDebitor() {
new CreateSepaMandateForDebitor(this) new CreateSepaMandateForDebitor(this)
.given("debitor", "Test AG") // existing debitor
.given("memberNumberSuffix", "00") .given("debitorNumber", "3101000")
.given("validFrom", "2024-10-15")
.given("membershipFeeBillable", "true") // 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() .doRun()
.keep(); .keep();
} }
@ -186,8 +194,9 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Requires("SEPA-Mandate: Test AG") @Requires("SEPA-Mandate: Test AG")
void shouldInvalidateSepaMandateForDebitor() { void shouldInvalidateSepaMandateForDebitor() {
new InvalidateSepaMandateForDebitor(this) new InvalidateSepaMandateForDebitor(this)
.given("sepaMandateUuid", "%{SEPA-Mandate: Test AG}") .given("debitorNumberNumber", "31010")
.given("validUntil", "2025-09-30") .given("bankAccountIBAN", "DE02701500000000594937")
.given("mandateValidUntil", "2025-09-30")
.doRun(); .doRun();
} }

View File

@ -12,6 +12,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.testcontainers.shaded.org.apache.commons.lang3.ObjectUtils;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.HashMap; import java.util.HashMap;
@ -35,7 +36,7 @@ public abstract class ScenarioTest extends ContextBasedTest {
@Override @Override
public String toString() { public String toString() {
return uuid.toString(); return ObjectUtils.toString(uuid);
} }
} }

View File

@ -1,9 +1,49 @@
package net.hostsharing.hsadminng.hs.office.scenarios; package net.hostsharing.hsadminng.hs.office.scenarios;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
public class TemplateResolver { public class TemplateResolver {
enum PlaceholderPrefix {
RAW('%') {
@Override
String convert(final Object value) {
return value.toString();
}
},
JSON_QUOTED('$'){
@Override
String convert(final Object value) {
return jsonQuoted(value);
}
},
URI_ENCODED('&'){
@Override
String convert(final Object value) {
return 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 String template;
private final Map<String, Object> properties; private final Map<String, Object> properties;
private final StringBuilder resolved = new StringBuilder(); private final StringBuilder resolved = new StringBuilder();
@ -21,7 +61,7 @@ public class TemplateResolver {
private void copy() { private void copy() {
while (hasMoreChars()) { while (hasMoreChars()) {
if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') { if (PlaceholderPrefix.contains(currentChar()) && nextChar() == '{') {
startPlaceholder(currentChar()); startPlaceholder(currentChar());
} else { } else {
resolved.append(fetchChar()); resolved.append(fetchChar());
@ -41,7 +81,7 @@ public class TemplateResolver {
if (currentChar() == '}') { if (currentChar() == '}') {
--nested; --nested;
placeholder.append(fetchChar()); placeholder.append(fetchChar());
} else if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') { } else if (PlaceholderPrefix.contains (currentChar()) && nextChar() == '{') {
++nested; ++nested;
placeholder.append(fetchChar()); placeholder.append(fetchChar());
} else { } else {
@ -50,11 +90,9 @@ public class TemplateResolver {
} }
final var name = new TemplateResolver(placeholder.toString(), properties).resolve(); final var name = new TemplateResolver(placeholder.toString(), properties).resolve();
final var value = propVal(name); final var value = propVal(name);
if ( intro == '%') { resolved.append(
resolved.append(value); PlaceholderPrefix.ofPrefixChar(intro).convert(value)
} else { );
resolved.append(optionallyQuoted(value));
}
skipChar('}'); skipChar('}');
} }
@ -104,7 +142,7 @@ public class TemplateResolver {
return template.charAt(position+1); return template.charAt(position+1);
} }
private static String optionallyQuoted(final Object value) { private static String jsonQuoted(final Object value) {
return switch (value) { return switch (value) {
case Boolean bool -> bool.toString(); case Boolean bool -> bool.toString();
case Number number -> number.toString(); case Number number -> number.toString();
@ -112,27 +150,4 @@ public class TemplateResolver {
default -> "\"" + value + "\""; 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());
}
} }

View File

@ -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
""");
}
}

View File

@ -120,7 +120,8 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
@SneakyThrows @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() final var request = HttpRequest.newBuilder()
.GET() .GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath)) .uri(new URI("http://localhost:" + testSuite.port + uriPath))
@ -132,7 +133,8 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
@SneakyThrows @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 requestBody = bodyJsonTemplate.resolvePlaceholders();
final var request = HttpRequest.newBuilder() final var request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString(requestBody)) .POST(BodyPublishers.ofString(requestBody))
@ -146,7 +148,8 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
@SneakyThrows @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 requestBody = bodyJsonTemplate.resolvePlaceholders();
final var request = HttpRequest.newBuilder() final var request = HttpRequest.newBuilder()
.method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody)) .method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody))
@ -160,7 +163,8 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
@SneakyThrows @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() final var request = HttpRequest.newBuilder()
.DELETE() .DELETE()
.uri(new URI("http://localhost:" + testSuite.port + uriPath)) .uri(new URI("http://localhost:" + testSuite.port + uriPath))

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDebitor> { public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDebitor> {
@ -14,12 +15,19 @@ public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDeb
@Override @Override
protected HttpResponse run() { protected HttpResponse run() {
obtain("Debitor: Test AG - main debitor", () ->
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", () -> obtain("BankAccount: Test AG - debit bank account", () ->
httpPost("/api/hs/office/bankaccounts", usingJsonBody(""" httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
{ {
"holder": "Test AG - debit bank account", "holder": ${bankAccountHolder},
"iban": "DE02701500000000594937", "iban": ${bankAccountIBAN},
"bic": "SSKMDEMM" "bic": ${bankAccountBIC}
} }
""")) """))
.expecting(CREATED).expecting(JSON) .expecting(CREATED).expecting(JSON)
@ -29,9 +37,9 @@ public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDeb
{ {
"debitorUuid": ${Debitor: Test AG - main debitor}, "debitorUuid": ${Debitor: Test AG - main debitor},
"bankAccountUuid": ${BankAccount: Test AG - debit bank account}, "bankAccountUuid": ${BankAccount: Test AG - debit bank account},
"reference": "Test AG - main debitor", "reference": ${mandateReference},
"agreement": "2022-10-12", "agreement": ${mandateAgreement},
"validFrom": "2022-10-13" "validFrom": ${mandateValidFrom}
} }
""")) """))
.expecting(CREATED).expecting(JSON); .expecting(CREATED).expecting(JSON);