feature/use-case-acceptance-tests-2 #117

Merged
hsh-michaelhoennig merged 38 commits from feature/use-case-acceptance-tests-2 into master 2024-11-05 13:58:39 +01:00
15 changed files with 147 additions and 40 deletions
Showing only changes of commit 42c4d4102e - Show all commits

View File

@ -3,6 +3,7 @@ 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;
@ -150,8 +151,18 @@ class HsOfficeScenarioTests extends ScenarioTest {
void shouldAddPhoneNumberToContactData() {
new AddPhoneNumberToContactData(this)
.given("partnerName", "Matthieu")
.given("newOfficePhoneNumberKey", "mobile")
.given("newOfficePhoneNumber", "+49 152 1234567")
.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();
}

View File

@ -76,6 +76,7 @@ public class TemplateResolver {
private String dropLinesWithNullProperties(final String text) {
return Arrays.stream(text.split("\n"))
.filter(TemplateResolver::keepLine)
.map(TemplateResolver::keptNullValues)
.collect(Collectors.joining("\n"));
}
@ -84,6 +85,10 @@ public class TemplateResolver {
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 (PlaceholderPrefix.contains(currentChar()) && nextChar() == '{') {

View File

@ -9,7 +9,7 @@ import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.office.scenarios.contact.PathAssertion;
import net.hostsharing.hsadminng.reflection.AnnotationFinder;
import org.apache.commons.collections4.map.LinkedMap;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.OptionalAssert;
import org.hibernate.AssertionFailure;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
@ -26,6 +26,7 @@ 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;
@ -33,6 +34,7 @@ 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;
@ -109,22 +111,25 @@ public abstract class UseCase<T extends UseCase<?>> {
final Function<HttpResponse, String> 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<HttpResponse> 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;
});
}
public void withTitle(final String title, final Runnable code) {
public HttpResponse withTitle(final String title, final Supplier<HttpResponse> code) {
this.nextTitle = title;
code.run();
final var response = code.get();
this.nextTitle = null;
return response;
}
@SneakyThrows
@ -190,11 +195,12 @@ public abstract class UseCase<T extends UseCase<?>> {
protected void validate(
final String title,
final Supplier<UseCase<?>.HttpResponse> http,
final Consumer<UseCase<?>.HttpResponse> assertion) {
final Supplier<UseCase.HttpResponse> http,
final Consumer<UseCase.HttpResponse> assertion) {
withTitle(ScenarioTest.resolve(title), () -> {
final var response = http.get();
assertion.accept(response);
return response;
});
}
@ -219,7 +225,7 @@ public abstract class UseCase<T extends UseCase<?>> {
}
}
public class HttpResponse {
public final class HttpResponse {
@Getter
private final java.net.http.HttpResponse<String> response;
@ -258,7 +264,7 @@ public abstract class UseCase<T extends UseCase<?>> {
return this;
}
public void keep(final Function<HttpResponse, String> extractor) {
public HttpResponse keep(final Function<HttpResponse, String> extractor) {
final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
@ -266,14 +272,16 @@ public abstract class UseCase<T extends 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
@ -293,8 +301,17 @@ public abstract class UseCase<T extends UseCase<?>> {
}
@SneakyThrows
public AbstractStringAssert<?> path(final String path) {
return assertThat(getFromBody(path));
public Optional<String> 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<String> path(final String path) {
return assertThat(getFromBodyAsOptional(path));
}
@SneakyThrows
@ -306,7 +323,7 @@ public abstract class UseCase<T extends UseCase<?>> {
} else if (resultAlias != null) {
testReport.printLine("\n### " + resultAlias + "\n");
} else {
testReport.printLine("\n### FIXME\n");
fail("please wrap the http...-call in the UseCase using `withTitle(...)`");
}
// the request

View File

@ -29,7 +29,7 @@ public class AddPhoneNumberToContactData extends UseCase<AddPhoneNumberToContact
httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
{
"phoneNumbers": {
${newOfficePhoneNumberKey}: ${newOfficePhoneNumber}
${phoneNumberKeyToAdd}: ${phoneNumberToAdd}
}
}
"""))
@ -45,7 +45,7 @@ public class AddPhoneNumberToContactData extends UseCase<AddPhoneNumberToContact
"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.%{newOfficePhoneNumberKey}").isEqualTo("%{newOfficePhoneNumber}")
path("[0].contact.phoneNumbers.%{phoneNumberKeyToAdd}").contains("%{phoneNumberToAdd}")
);
}
}

View File

@ -23,6 +23,7 @@ public class AmendContactData extends UseCase<AmendContactData> {
"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???},
@ -35,7 +36,8 @@ public class AmendContactData extends UseCase<AmendContactData> {
}
}
"""))
.expecting(HttpStatus.OK);
.expecting(HttpStatus.OK)
);
return null;
}

View File

@ -2,6 +2,7 @@ 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 net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse;
import java.util.function.Consumer;
@ -13,7 +14,12 @@ public class PathAssertion {
this.path = path;
}
public Consumer<UseCase<?>.HttpResponse> isEqualTo(final String resolvableValue) {
return response -> response.path(path).isEqualTo(ScenarioTest.resolve(resolvableValue));
@SuppressWarnings({ "unchecked", "rawtypes" })
public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) {
return response -> response.path(path).contains(ScenarioTest.resolve(resolvableValue));
}
public Consumer<HttpResponse> doesNotExist() {
return response -> response.path(path).isNull(); // here, null Optional means key not found in JSON
}
}

View File

@ -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<RemovePhoneNumberFromContactData> {
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() {
validate(
"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()
);
}
}

View File

@ -58,7 +58,7 @@ public class ReplaceContactData extends UseCase<ReplaceContactData> {
"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").isEqualTo("%{newContactCaption}")
path("[0].contact.caption").contains("%{newContactCaption}")
);
}
}

View File

@ -24,8 +24,10 @@ public class DeleteDebitor extends UseCase<DeleteDebitor> {
@Override
protected HttpResponse run() {
withTitle("Delete the Debitor using its UUID", () ->
httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - delete debitor}")
.expecting(HttpStatus.NO_CONTENT);
.expecting(HttpStatus.NO_CONTENT)
);
return null;
}
}

View File

@ -24,8 +24,8 @@ public class FinallyDeleteSepaMandateForDebitor extends UseCase<FinallyDeleteSep
);
// TODO.spec: When to allow actual deletion of SEPA-mandates? Add constraint accordingly.
httpDelete("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}")
.expecting(HttpStatus.NO_CONTENT);
return null;
return withTitle("Delete the SEPA-Mandate by its UUID", () -> httpDelete("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}")
.expecting(HttpStatus.NO_CONTENT)
);
}
}

View File

@ -22,11 +22,13 @@ public class InvalidateSepaMandateForDebitor extends UseCase<InvalidateSepaManda
"With production data, the bank-account could be used in multiple SEPA-mandates, make sure to use the right one!"
);
return httpPatch("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}", usingJsonBody("""
return withTitle("Patch the End of the Mandate into the SEPA-Mandate", () ->
httpPatch("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}", usingJsonBody("""
{
"validUntil": ${mandateValidUntil}
}
"""))
.expecting(OK).expecting(JSON);
.expecting(OK).expecting(JSON)
);
}
}

View File

@ -18,8 +18,10 @@ public class DeletePartner extends UseCase<DeletePartner> {
@Override
protected HttpResponse run() {
withTitle("Delete Partner by its UUID", () ->
httpDelete("/api/hs/office/partners/&{Partner: Delete AG}")
.expecting(HttpStatus.NO_CONTENT);
.expecting(HttpStatus.NO_CONTENT)
);
return null;
}
}

View File

@ -14,12 +14,14 @@ public class CreatePerson extends UseCase<CreatePerson> {
@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)
);
}
}

View File

@ -1,7 +1,7 @@
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.NO_CONTENT;
@ -16,14 +16,20 @@ public class RemoveOperationsContactFromPartner extends UseCase<RemoveOperations
@Override
protected HttpResponse run() {
obtain("Operations-Contact: %{operationsContactPerson}", () ->
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."
);
return httpDelete("/api/hs/office/relations/&{Operations-Contact: %{operationsContactPerson}}")
.expecting(NO_CONTENT);
withTitle("Delete the Contact", () ->
httpDelete("/api/hs/office/relations/&{Operations-Contact: %{operationsContactPerson}}")
.expecting(NO_CONTENT)
);
return null;
}
}

View File

@ -25,7 +25,9 @@ public class UnsubscribeFromMailinglist extends UseCase<UnsubscribeFromMailingli
"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/&{Subscription: %{subscriberEMailAddress}}")
.expecting(NO_CONTENT);
return withTitle("Delete the Subscriber-Relation by its UUID", () ->
httpDelete("/api/hs/office/relations/&{Subscription: %{subscriberEMailAddress}}")
.expecting(NO_CONTENT)
);
}
}