Compare commits

..

No commits in common. "6d2591265aeb47ae683c43355a5107ec5f07076b" and "d55fea7851168a1efa1d00fbbc07899b27cdf542" have entirely different histories.

7 changed files with 159 additions and 195 deletions

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.office.scenarios; package net.hostsharing.hsadminng.hs.office.scenarios;
import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
@ -9,10 +10,14 @@ import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.apache.commons.collections4.SetUtils; import org.apache.commons.collections4.SetUtils;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
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 java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -31,19 +36,16 @@ public abstract class ScenarioTest extends ContextBasedTest {
final static String RUN_AS_USER = "superuser-alex@hostsharing.net"; // TODO.test: use global:AGENT when implemented final static String RUN_AS_USER = "superuser-alex@hostsharing.net"; // TODO.test: use global:AGENT when implemented
record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) { @Getter
private PrintWriter markdownFile;
@Override private StringBuilder debugLog = new StringBuilder();
public String toString() {
return uuid.toString(); record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {}
}
}
private final static Map<String, Alias<?>> aliases = new HashMap<>(); private final static Map<String, Alias<?>> aliases = new HashMap<>();
private final static Map<String, Object> properties = new HashMap<>(); private final static Map<String, Object> properties = new HashMap<>();
public final TestReport testReport = new TestReport(aliases);
@LocalServerPort @LocalServerPort
Integer port; Integer port;
@ -59,16 +61,43 @@ public abstract class ScenarioTest extends ContextBasedTest {
createHostsharingPerson(); createHostsharingPerson();
try { try {
testInfo.getTestMethod().ifPresent(this::callRequiredProducers); testInfo.getTestMethod().ifPresent(this::callRequiredProducers);
testReport.createTestLogMarkdownFile(testInfo); createTestLogMarkdownFile(testInfo);
} catch (Exception exc) { } catch (Exception exc) {
throw exc; throw exc;
} }
} }
@AfterEach @AfterEach
void cleanup() { // final TestInfo testInfo void cleanup() {
properties.clear(); properties.clear();
testReport.close(); if (markdownFile != null) {
markdownFile.close();
} else {
toString();
}
debugLog = new StringBuilder();
}
@SneakyThrows
void print(final String output) {
final var outputWithCommentsForUuids = UUIDAppender.appendUUIDKey(aliases, output.replace("+", "\\+"));
// for tests executed due to @Requires/@Produces there is no markdownFile yet
if (markdownFile != null) {
markdownFile.print(outputWithCommentsForUuids);
}
// but the debugLog should contain all output
debugLog.append(outputWithCommentsForUuids);
}
void printLine(final String output) {
print(output + "\n");
}
void printPara(final String output) {
printLine("\n" +output + "\n");
} }
private void createHostsharingPerson() { private void createHostsharingPerson() {
@ -88,6 +117,13 @@ public abstract class ScenarioTest extends ContextBasedTest {
); );
} }
private void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException {
final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow();
final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow();
markdownFile = new PrintWriter(new FileWriter("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md"));
print("## Scenario: " + testMethodName.replaceAll("([a-z])([A-Z]+)", "$1 $2"));
}
@SneakyThrows @SneakyThrows
private void callRequiredProducers(final Method currentTestMethod) { private void callRequiredProducers(final Method currentTestMethod) {
final var testMethodRequired = Optional.of(currentTestMethod) final var testMethodRequired = Optional.of(currentTestMethod)

View File

@ -1,85 +0,0 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.TestInfo;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class TestReport {
private final Map<String, ?> aliases;
private final StringBuilder debugLog = new StringBuilder(); // records everything for debugging purposes
private PrintWriter markdownFile;
private int silent; // do not print anything to test-report if >0
public TestReport(final Map<String, ?> aliases) {
this.aliases = aliases;
}
public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException {
final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow();
final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow();
assertThat(new File("doc/scenarios/").isDirectory() || new File("doc/scenarios/").mkdirs()).as("mkdir doc/scenarios/").isTrue();
markdownFile = new PrintWriter(new FileWriter("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md"));
print("## Scenario: " + testMethodName.replaceAll("([a-z])([A-Z]+)", "$1 $2"));
}
@SneakyThrows
public void print(final String output) {
final var outputWithCommentsForUuids = appendUUIDKey(output.replace("+", "\\+"));
// for tests executed due to @Requires/@Produces there is no markdownFile yet
if (silent == 0) {
markdownFile.print(outputWithCommentsForUuids);
}
// but the debugLog should contain all output, even if silent
debugLog.append(outputWithCommentsForUuids);
}
public void printLine(final String output) {
print(output + "\n");
}
public void printPara(final String output) {
printLine("\n" +output + "\n");
}
public void close() {
markdownFile.close();
}
private String appendUUIDKey(String multilineText) {
final var lines = multilineText.split("\\r?\\n");
final var result = new StringBuilder();
for (String line : lines) {
for (Map.Entry<String, ?> entry : aliases.entrySet()) {
final var uuidString = entry.getValue().toString();
if (line.contains(uuidString)) {
line = line + " // " + entry.getKey();
break; // only add comment for one UUID per row (in our case, there is only one per row)
}
}
result.append(line).append("\n");
}
return result.toString();
}
void silent(final Runnable code) {
silent++;
code.run();
silent--;
}
}

View File

@ -0,0 +1,34 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
import java.util.Map;
public class UUIDAppender {
public static String appendUUIDKey(Map<String, ScenarioTest.Alias<?>> uuidMap, String multilineText) {
// Split the multiline text into lines
String[] lines = multilineText.split("\\r?\\n");
// StringBuilder to build the resulting multiline text
StringBuilder result = new StringBuilder();
// Iterate over each line
for (String line : lines) {
// Iterate over the map to find if the line contains a UUID
for (Map.Entry<String, ScenarioTest.Alias<?>> entry : uuidMap.entrySet()) {
String uuidString = entry.getValue().uuid().toString();
// If the line contains the UUID, append the key at the end
if (line.contains(uuidString)) {
line = line + " // " + entry.getKey();
break; // Exit once we've matched one UUID
}
}
// Append the (possibly modified) line to the result
result.append(line).append("\n");
}
// Return the final modified text
return result.toString();
}
}

View File

@ -20,7 +20,6 @@ import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -36,15 +35,13 @@ import static org.junit.platform.commons.util.StringUtils.isNotBlank;
public abstract class UseCase<T extends UseCase<?>> { public abstract class UseCase<T extends UseCase<?>> {
private static final HttpClient client = HttpClient.newHttpClient(); private static final HttpClient client = HttpClient.newHttpClient();
private final ObjectMapper objectMapper = new ObjectMapper(); final ObjectMapper objectMapper = new ObjectMapper();
protected final ScenarioTest testSuite; protected final ScenarioTest testSuite;
private final TestReport testReport;
private final Map<String, Function<String, UseCase<?>>> requirements = new LinkedMap<>(); private final Map<String, Function<String, UseCase<?>>> requirements = new LinkedMap<>();
private final String resultAlias; private final String resultAlias;
private final Map<String, Object> givenProperties = new LinkedHashMap<>(); private final Map<String, Object> givenProperties = new LinkedHashMap<>();
private String nextTitle; // FIXME: ugly
private String nextTitle; // just temporary to override resultAlias for sub-use-cases
public UseCase(final ScenarioTest testSuite) { public UseCase(final ScenarioTest testSuite) {
this(testSuite, getResultAliasFromProducesAnnotationInCallStack()); this(testSuite, getResultAliasFromProducesAnnotationInCallStack());
@ -52,10 +49,9 @@ public abstract class UseCase<T extends UseCase<?>> {
public UseCase(final ScenarioTest testSuite, final String resultAlias) { public UseCase(final ScenarioTest testSuite, final String resultAlias) {
this.testSuite = testSuite; this.testSuite = testSuite;
this.testReport = testSuite.testReport;
this.resultAlias = resultAlias; this.resultAlias = resultAlias;
if (resultAlias != null) { if (resultAlias != null) {
testReport.printPara("### UseCase " + title(resultAlias)); printPara("### UseCase " + title(resultAlias));
} }
} }
@ -66,23 +62,19 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public final HttpResponse doRun() { public final HttpResponse doRun() {
testReport.printPara("### Given Properties"); printPara("### Given Properties");
testReport.printLine(""" printLine("""
| name | value | | name | value |
|------|-------|"""); |------|-------|""");
givenProperties.forEach((key, value) -> givenProperties.forEach((key, value) -> printLine("| " + key + " | " + value.toString().replace("\n", "<br>") + " |"));
testReport.printLine("| " + key + " | " + value.toString().replace("\n", "<br>") + " |")); printLine("");
testReport.printLine("");
testReport.silent(() ->
requirements.forEach((alias, factory) -> { requirements.forEach((alias, factory) -> {
if (!ScenarioTest.containsAlias(alias)) { if (!ScenarioTest.containsAlias(alias)) {
factory.apply(alias).run().keep(); factory.apply(alias).run().keep();
} }
}) });
);
return run(); return run();
} }
protected abstract HttpResponse run(); protected abstract HttpResponse run();
public final UseCase<T> given(final String propName, final Object propValue) { public final UseCase<T> given(final String propName, final Object propValue) {
@ -95,27 +87,15 @@ public abstract class UseCase<T extends UseCase<?>> {
return new JsonTemplate(jsonTemplate); return new JsonTemplate(jsonTemplate);
} }
public final void keep( public final void keep(final String alias, final Supplier<HttpResponse> http, final Function<HttpResponse, String> extractor) {
final String alias, this.nextTitle = ScenarioTest.resolve(alias);
final Supplier<HttpResponse> http,
final Function<HttpResponse, String> extractor,
final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> {
http.get().keep(extractor); http.get().keep(extractor);
Arrays.stream(extraInfo).forEach(testReport::printPara); this.nextTitle = null;
});
} }
public final void keep(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) { public final void keep(final String alias, final Supplier<HttpResponse> http) {
withTitle(ScenarioTest.resolve(alias), () -> { this.nextTitle = ScenarioTest.resolve(alias);
http.get().keep(); http.get().keep();
Arrays.stream(extraInfo).forEach(testReport::printPara);
});
}
private void withTitle(final String title, final Runnable code) {
this.nextTitle = title;
code.run();
this.nextTitle = null; this.nextTitle = null;
} }
@ -218,7 +198,22 @@ public abstract class UseCase<T extends UseCase<?>> {
locationUuid = UUID.fromString(location.substring(location.lastIndexOf('/') + 1)); locationUuid = UUID.fromString(location.substring(location.lastIndexOf('/') + 1));
} }
reportRequestAndResponse(httpMethod, uri, requestBody); if (nextTitle != null) {
printLine("\n### " + nextTitle + "\n");
} else if (resultAlias != null) {
printLine("\n### " + resultAlias + "\n");
}
printLine("```");
printLine(httpMethod.name() + " " + uri);
printLine((requestBody != null ? requestBody : "") + "=> status: " + status + " " +
(locationUuid != null ? locationUuid : ""));
if (httpMethod == HttpMethod.GET || !status.is2xxSuccessful()) {
final var jsonNode = objectMapper.readTree(response.body());
final var prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
printLine(prettyJson);
}
printLine("```");
printLine("");
} }
public HttpResponse expecting(final HttpStatus httpStatus) { public HttpResponse expecting(final HttpStatus httpStatus) {
@ -252,7 +247,6 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows @SneakyThrows
public HttpResponse expectArrayElements(final int expectedElementCount) { public HttpResponse expectArrayElements(final int expectedElementCount) {
// FIXME: use JsonPath?
final var rootNode = objectMapper.readTree(response.body()); final var rootNode = objectMapper.readTree(response.body());
assertThat(rootNode.isArray()).as("array expected, but got: " + response.body()).isTrue(); assertThat(rootNode.isArray()).as("array expected, but got: " + response.body()).isTrue();
@ -266,32 +260,18 @@ public abstract class UseCase<T extends UseCase<?>> {
public String getFromBody(final String path) { public String getFromBody(final String path) {
return JsonPath.parse(response.body()).read(path); return JsonPath.parse(response.body()).read(path);
} }
@SneakyThrows
private void reportRequestAndResponse(final HttpMethod httpMethod, final String uri, final String requestBody) {
// the title
if (nextTitle != null) {
testReport.printLine("\n### " + nextTitle + "\n");
} else if (resultAlias != null) {
testReport.printLine("\n### " + resultAlias + "\n");
} }
// the request public void print(final String output) {
testReport.printLine("```"); testSuite.print(output);
testReport.printLine(httpMethod.name() + " " + uri); }
testReport.printLine((requestBody != null ? requestBody.trim() : ""));
// the response + "=> status: " + status + " " + public void printLine(final String output) {
testReport.printLine(locationUuid != null ? locationUuid.toString() : ""); testSuite.printLine(output);
if (httpMethod == HttpMethod.GET || status.isError()) {
final var jsonNode = objectMapper.readTree(response.body());
final var prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
testReport.printLine(prettyJson);
}
testReport.printLine("```");
testReport.printLine("");
} }
public void printPara(final String output) {
testSuite.printPara(output);
} }
protected T self() { protected T self() {
@ -308,7 +288,7 @@ public abstract class UseCase<T extends UseCase<?>> {
private static String oneOf(final String one, final String another) { private static String oneOf(final String one, final String another) {
if (isNotBlank(one) && isBlank(another)) { if (isNotBlank(one) && isBlank(another)) {
return one; return one;
} else if (isBlank(one) && isNotBlank(another)) { } else if ( isBlank(one) && isNotBlank(another)) {
return another; return another;
} }
throw new AssertionFailure("exactly one value required, but got '" + one + "' and '" + another + "'"); throw new AssertionFailure("exactly one value required, but got '" + one + "' and '" + another + "'");

View File

@ -18,10 +18,10 @@ public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPar
keep("partnerPersonUuid", () -> keep("partnerPersonUuid", () ->
httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerPersonTradeName}")) httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerPersonTradeName}"))
.expecting(OK).expecting(JSON), .expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].holder.uuid"), response -> response.expectArrayElements(1).getFromBody("[0].holder.uuid")
"From that output above, we're taking the UUID of the holder of the first result element.",
"**HINT**: With production data, you might get multiple results and have to decide which is the right one."
); );
printPara("From that output above, we're taking the UUID of the holder of the first result element.");
printPara("**HINT**: With production data, you might get multiple results and have to decide which is the right one.");
keep("BankAccount: Test AG - refund bank account", () -> keep("BankAccount: Test AG - refund bank account", () ->
httpPost("/api/hs/office/bankaccounts", usingJsonBody(""" httpPost("/api/hs/office/bankaccounts", usingJsonBody("""

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner; package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
@ -16,8 +16,7 @@ public class AddOperationsContactToPartner extends UseCase<AddOperationsContactT
@Override @Override
protected HttpResponse run() { protected HttpResponse run() {
keep("Person: %{operationsContactGivenName} %{operationsContactFamilyName}", keep("Person: %{operationsContactGivenName} %{operationsContactFamilyName}", () ->
() ->
httpPost("/api/hs/office/persons", usingJsonBody(""" httpPost("/api/hs/office/persons", usingJsonBody("""
{ {
"personType": "NATURAL_PERSON", "personType": "NATURAL_PERSON",
@ -25,10 +24,10 @@ public class AddOperationsContactToPartner extends UseCase<AddOperationsContactT
"givenName": ${operationsContactGivenName} "givenName": ${operationsContactGivenName}
} }
""")) """))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON), .expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
"Please check first if that person already exists, if so, use it's UUID below.",
"**HINT**: operations contacts are always connected to a partner-person, thus a person which is a holder of a partner-relation."
); );
printPara("Please check first if that person already exists, if so, use it's UUID below.");
printPara("**HINT**: operations contacts are always connected to a partner-person, thus a person which is a holder of a partner-relation.");
keep("Contact: %{operationsContactGivenName} %{operationsContactFamilyName}", () -> keep("Contact: %{operationsContactGivenName} %{operationsContactFamilyName}", () ->
httpPost("/api/hs/office/contacts", usingJsonBody(""" httpPost("/api/hs/office/contacts", usingJsonBody("""
@ -42,9 +41,9 @@ public class AddOperationsContactToPartner extends UseCase<AddOperationsContactT
} }
} }
""")) """))
.expecting(CREATED).expecting(JSON), .expecting(CREATED).expecting(JSON)
"Please check first if that contact already exists, if so, use it's UUID below."
); );
printPara("Please check first if that contact already exists, if so, use it's UUID below.");
return httpPost("/api/hs/office/relations", usingJsonBody(""" return httpPost("/api/hs/office/relations", usingJsonBody("""
{ {

View File

@ -24,10 +24,10 @@ public class AddRepresentativeToPartner extends UseCase<AddRepresentativeToPartn
"givenName": ${representativeGivenName} "givenName": ${representativeGivenName}
} }
""")) """))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON), .expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
"Please check first if that person already exists, if so, use it's UUID below.",
"**HINT**: A representative is always a natural person and represents a non-natural-person."
); );
printPara("Please check first if that person already exists, if so, use it's UUID below.");
printPara("**HINT**: A representative is always a natural person and represents a non-natural-person.");
keep("Contact: %{representativeGivenName} %{representativeFamilyName}", () -> keep("Contact: %{representativeGivenName} %{representativeFamilyName}", () ->
httpPost("/api/hs/office/contacts", usingJsonBody(""" httpPost("/api/hs/office/contacts", usingJsonBody("""
@ -42,9 +42,9 @@ public class AddRepresentativeToPartner extends UseCase<AddRepresentativeToPartn
} }
} }
""")) """))
.expecting(CREATED).expecting(JSON), .expecting(CREATED).expecting(JSON)
"Please check first if that contact already exists, if so, use it's UUID below."
); );
printPara("Please check first if that contact already exists, if so, use it's UUID below.");
return httpPost("/api/hs/office/relations", usingJsonBody(""" return httpPost("/api/hs/office/relations", usingJsonBody("""
{ {