feature/use-case-acceptance-tests #116
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
|
||||
@ -10,14 +9,10 @@ import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.apache.commons.collections4.SetUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@ -36,16 +31,19 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
||||
|
||||
final static String RUN_AS_USER = "superuser-alex@hostsharing.net"; // TODO.test: use global:AGENT when implemented
|
||||
|
||||
@Getter
|
||||
private PrintWriter markdownFile;
|
||||
record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {
|
||||
|
||||
private StringBuilder debugLog = new StringBuilder();
|
||||
|
||||
record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {}
|
||||
@Override
|
||||
public String toString() {
|
||||
return uuid.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private final static Map<String, Alias<?>> aliases = new HashMap<>();
|
||||
private final static Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
public final TestReport testReport = new TestReport(aliases);
|
||||
|
||||
@LocalServerPort
|
||||
Integer port;
|
||||
|
||||
@ -61,43 +59,16 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
||||
createHostsharingPerson();
|
||||
try {
|
||||
testInfo.getTestMethod().ifPresent(this::callRequiredProducers);
|
||||
createTestLogMarkdownFile(testInfo);
|
||||
testReport.createTestLogMarkdownFile(testInfo);
|
||||
} catch (Exception exc) {
|
||||
throw exc;
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
void cleanup() { // final TestInfo testInfo
|
||||
properties.clear();
|
||||
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");
|
||||
testReport.close();
|
||||
}
|
||||
|
||||
private void createHostsharingPerson() {
|
||||
@ -117,13 +88,6 @@ 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
|
||||
private void callRequiredProducers(final Method currentTestMethod) {
|
||||
final var testMethodRequired = Optional.of(currentTestMethod)
|
||||
|
@ -0,0 +1,84 @@
|
||||
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 (markdownFile != null && silent == 0) { // FIXME: check if markdownFile can even be null
|
||||
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--;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -35,13 +36,15 @@ import static org.junit.platform.commons.util.StringUtils.isNotBlank;
|
||||
public abstract class UseCase<T extends UseCase<?>> {
|
||||
|
||||
private static final HttpClient client = HttpClient.newHttpClient();
|
||||
final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
protected final ScenarioTest testSuite;
|
||||
private final TestReport testReport;
|
||||
private final Map<String, Function<String, UseCase<?>>> requirements = new LinkedMap<>();
|
||||
private final String resultAlias;
|
||||
private final Map<String, Object> givenProperties = new LinkedHashMap<>();
|
||||
private String nextTitle; // FIXME: ugly
|
||||
|
||||
private String nextTitle; // just temporary
|
||||
|
||||
public UseCase(final ScenarioTest testSuite) {
|
||||
this(testSuite, getResultAliasFromProducesAnnotationInCallStack());
|
||||
@ -49,9 +52,10 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
|
||||
public UseCase(final ScenarioTest testSuite, final String resultAlias) {
|
||||
this.testSuite = testSuite;
|
||||
this.testReport = testSuite.testReport;
|
||||
this.resultAlias = resultAlias;
|
||||
if (resultAlias != null) {
|
||||
printPara("### UseCase " + title(resultAlias));
|
||||
testReport.printPara("### UseCase " + title(resultAlias));
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,19 +66,23 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
}
|
||||
|
||||
public final HttpResponse doRun() {
|
||||
printPara("### Given Properties");
|
||||
printLine("""
|
||||
testReport.printPara("### Given Properties");
|
||||
testReport.printLine("""
|
||||
| name | value |
|
||||
|------|-------|""");
|
||||
givenProperties.forEach((key, value) -> printLine("| " + key + " | " + value.toString().replace("\n", "<br>") + " |"));
|
||||
printLine("");
|
||||
givenProperties.forEach((key, value) ->
|
||||
testReport.printLine("| " + key + " | " + value.toString().replace("\n", "<br>") + " |"));
|
||||
testReport.printLine("");
|
||||
testReport.silent(() ->
|
||||
requirements.forEach((alias, factory) -> {
|
||||
if (!ScenarioTest.containsAlias(alias)) {
|
||||
factory.apply(alias).run().keep();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
return run();
|
||||
}
|
||||
|
||||
protected abstract HttpResponse run();
|
||||
|
||||
public final UseCase<T> given(final String propName, final Object propValue) {
|
||||
@ -87,15 +95,27 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
return new JsonTemplate(jsonTemplate);
|
||||
}
|
||||
|
||||
public final void keep(final String alias, final Supplier<HttpResponse> http, final Function<HttpResponse, String> extractor) {
|
||||
this.nextTitle = ScenarioTest.resolve(alias);
|
||||
public final void keep(
|
||||
final String alias,
|
||||
final Supplier<HttpResponse> http,
|
||||
final Function<HttpResponse, String> extractor,
|
||||
final String... extraInfo) {
|
||||
withTitle(ScenarioTest.resolve(alias), () -> {
|
||||
http.get().keep(extractor);
|
||||
this.nextTitle = null;
|
||||
Arrays.stream(extraInfo).forEach(testReport::printPara);
|
||||
});
|
||||
}
|
||||
|
||||
public final void keep(final String alias, final Supplier<HttpResponse> http) {
|
||||
this.nextTitle = ScenarioTest.resolve(alias);
|
||||
public final void keep(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) {
|
||||
withTitle(ScenarioTest.resolve(alias), () -> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -199,21 +219,23 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
}
|
||||
|
||||
if (nextTitle != null) {
|
||||
printLine("\n### " + nextTitle + "\n");
|
||||
testReport.printLine("\n### " + nextTitle + "\n");
|
||||
} else if (resultAlias != null) {
|
||||
printLine("\n### " + resultAlias + "\n");
|
||||
testReport.printLine("\n### " + resultAlias + "\n");
|
||||
}
|
||||
printLine("```");
|
||||
printLine(httpMethod.name() + " " + uri);
|
||||
printLine((requestBody != null ? requestBody : "") + "=> status: " + status + " " +
|
||||
|
||||
// FIXME: maybe refactor into TestReport?
|
||||
testReport.printLine("```");
|
||||
testReport.printLine(httpMethod.name() + " " + uri);
|
||||
testReport.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);
|
||||
testReport.printLine(prettyJson);
|
||||
}
|
||||
printLine("```");
|
||||
printLine("");
|
||||
testReport.printLine("```");
|
||||
testReport.printLine("");
|
||||
}
|
||||
|
||||
public HttpResponse expecting(final HttpStatus httpStatus) {
|
||||
@ -262,18 +284,6 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
}
|
||||
}
|
||||
|
||||
public void print(final String output) {
|
||||
testSuite.print(output);
|
||||
}
|
||||
|
||||
public void printLine(final String output) {
|
||||
testSuite.printLine(output);
|
||||
}
|
||||
|
||||
public void printPara(final String output) {
|
||||
testSuite.printPara(output);
|
||||
}
|
||||
|
||||
protected T self() {
|
||||
//noinspection unchecked
|
||||
return (T) this;
|
||||
|
@ -18,10 +18,10 @@ public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPar
|
||||
keep("partnerPersonUuid", () ->
|
||||
httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerPersonTradeName}"))
|
||||
.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", () ->
|
||||
httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
|
||||
|
||||
import io.restassured.http.ContentType;
|
||||
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 org.springframework.http.HttpStatus;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
@ -16,7 +16,8 @@ public class AddOperationsContactToPartner extends UseCase<AddOperationsContactT
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
keep("Person: %{operationsContactGivenName} %{operationsContactFamilyName}", () ->
|
||||
keep("Person: %{operationsContactGivenName} %{operationsContactFamilyName}",
|
||||
() ->
|
||||
httpPost("/api/hs/office/persons", usingJsonBody("""
|
||||
{
|
||||
"personType": "NATURAL_PERSON",
|
||||
@ -24,10 +25,10 @@ public class AddOperationsContactToPartner extends UseCase<AddOperationsContactT
|
||||
"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}", () ->
|
||||
httpPost("/api/hs/office/contacts", usingJsonBody("""
|
||||
@ -41,9 +42,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("""
|
||||
{
|
||||
|
@ -24,10 +24,10 @@ public class AddRepresentativeToPartner extends UseCase<AddRepresentativeToPartn
|
||||
"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}", () ->
|
||||
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("""
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user