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.HsadminNgApplication;
import net.hostsharing.hsadminng.hs.office.scenarios.contact.AddPhoneNumberToContactData; 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.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.contact.ReplaceContactData;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateExternalDebitorForPartner; 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.CreateSelfDebitorForPartner;
@ -150,8 +151,18 @@ class HsOfficeScenarioTests extends ScenarioTest {
void shouldAddPhoneNumberToContactData() { void shouldAddPhoneNumberToContactData() {
new AddPhoneNumberToContactData(this) new AddPhoneNumberToContactData(this)
.given("partnerName", "Matthieu") .given("partnerName", "Matthieu")
.given("newOfficePhoneNumberKey", "mobile") .given("phoneNumberKeyToAdd", "mobile")
.given("newOfficePhoneNumber", "+49 152 1234567") .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(); .doRun();
} }

View File

@ -76,6 +76,7 @@ public class TemplateResolver {
private String dropLinesWithNullProperties(final String text) { private String dropLinesWithNullProperties(final String text) {
return Arrays.stream(text.split("\n")) return Arrays.stream(text.split("\n"))
.filter(TemplateResolver::keepLine) .filter(TemplateResolver::keepLine)
.map(TemplateResolver::keptNullValues)
.collect(Collectors.joining("\n")); .collect(Collectors.joining("\n"));
} }
@ -84,6 +85,10 @@ public class TemplateResolver {
return !trimmed.endsWith("null,") && !trimmed.endsWith("null"); return !trimmed.endsWith("null,") && !trimmed.endsWith("null");
} }
private static String keptNullValues(final String line) {
return line.replace(": NULL", ": null");
}
private String copy() { private String copy() {
while (hasMoreChars()) { while (hasMoreChars()) {
if (PlaceholderPrefix.contains(currentChar()) && nextChar() == '{') { 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.hs.office.scenarios.contact.PathAssertion;
import net.hostsharing.hsadminng.reflection.AnnotationFinder; import net.hostsharing.hsadminng.reflection.AnnotationFinder;
import org.apache.commons.collections4.map.LinkedMap; 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.hibernate.AssertionFailure;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -26,6 +26,7 @@ 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;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
@ -33,6 +34,7 @@ import java.util.function.Supplier;
import static java.net.URLEncoder.encode; import static java.net.URLEncoder.encode;
import static org.assertj.core.api.Assertions.assertThat; 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.isBlank;
import static org.junit.platform.commons.util.StringUtils.isNotBlank; 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 Function<HttpResponse, String> extractor,
final String... extraInfo) { final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> { withTitle(ScenarioTest.resolve(alias), () -> {
http.get().keep(extractor); final var response = http.get().keep(extractor);
Arrays.stream(extraInfo).forEach(testReport::printPara); Arrays.stream(extraInfo).forEach(testReport::printPara);
return response;
}); });
} }
public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) { public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> { withTitle(ScenarioTest.resolve(alias), () -> {
http.get().keep(); final var response = http.get().keep();
Arrays.stream(extraInfo).forEach(testReport::printPara); 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; this.nextTitle = title;
code.run(); final var response = code.get();
this.nextTitle = null; this.nextTitle = null;
return response;
} }
@SneakyThrows @SneakyThrows
@ -190,11 +195,12 @@ public abstract class UseCase<T extends UseCase<?>> {
protected void validate( protected void validate(
final String title, final String title,
final Supplier<UseCase<?>.HttpResponse> http, final Supplier<UseCase.HttpResponse> http,
final Consumer<UseCase<?>.HttpResponse> assertion) { final Consumer<UseCase.HttpResponse> assertion) {
withTitle(ScenarioTest.resolve(title), () -> { withTitle(ScenarioTest.resolve(title), () -> {
final var response = http.get(); final var response = http.get();
assertion.accept(response); assertion.accept(response);
return response;
}); });
} }
@ -219,7 +225,7 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
} }
public class HttpResponse { public final class HttpResponse {
@Getter @Getter
private final java.net.http.HttpResponse<String> response; private final java.net.http.HttpResponse<String> response;
@ -258,7 +264,7 @@ public abstract class UseCase<T extends UseCase<?>> {
return this; 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; final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull(); assertThat(alias).as("cannot keep result, no alias found").isNotNull();
@ -266,14 +272,16 @@ public abstract class UseCase<T extends UseCase<?>> {
ScenarioTest.putAlias( ScenarioTest.putAlias(
alias, alias,
new ScenarioTest.Alias<>(UseCase.this.getClass(), UUID.fromString(value))); new ScenarioTest.Alias<>(UseCase.this.getClass(), UUID.fromString(value)));
return this;
} }
public void keep() { public HttpResponse keep() {
final var alias = nextTitle != null ? nextTitle : resultAlias; final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull(); assertThat(alias).as("cannot keep result, no alias found").isNotNull();
ScenarioTest.putAlias( ScenarioTest.putAlias(
alias, alias,
new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid)); new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
return this;
} }
@SneakyThrows @SneakyThrows
@ -293,8 +301,17 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
@SneakyThrows @SneakyThrows
public AbstractStringAssert<?> path(final String path) { public Optional<String> getFromBodyAsOptional(final String path) {
return assertThat(getFromBody(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 @SneakyThrows
@ -306,7 +323,7 @@ public abstract class UseCase<T extends UseCase<?>> {
} else if (resultAlias != null) { } else if (resultAlias != null) {
testReport.printLine("\n### " + resultAlias + "\n"); testReport.printLine("\n### " + resultAlias + "\n");
} else { } else {
testReport.printLine("\n### FIXME\n"); fail("please wrap the http...-call in the UseCase using `withTitle(...)`");
} }
// the request // the request

View File

@ -29,7 +29,7 @@ public class AddPhoneNumberToContactData extends UseCase<AddPhoneNumberToContact
httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody(""" httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
{ {
"phoneNumbers": { "phoneNumbers": {
${newOfficePhoneNumberKey}: ${newOfficePhoneNumber} ${phoneNumberKeyToAdd}: ${phoneNumberToAdd}
} }
} }
""")) """))
@ -45,7 +45,7 @@ public class AddPhoneNumberToContactData extends UseCase<AddPhoneNumberToContact
"Verify if the New Phone Number Got Added", "Verify if the New Phone Number Got Added",
() -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
.expecting(OK).expecting(JSON).expectArrayElements(1), .expecting(OK).expecting(JSON).expectArrayElements(1),
path("[0].contact.phoneNumbers.%{newOfficePhoneNumberKey}").isEqualTo("%{newOfficePhoneNumber}") path("[0].contact.phoneNumbers.%{phoneNumberKeyToAdd}").contains("%{phoneNumberToAdd}")
); );
} }
} }

View File

@ -23,7 +23,8 @@ 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." "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
); );
httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody(""" withTitle("Patch the New Phone Number Into the Contact", () ->
httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
{ {
"caption": ${newContactCaption???}, "caption": ${newContactCaption???},
"postalAddress": ${newPostalAddress???}, "postalAddress": ${newPostalAddress???},
@ -35,7 +36,8 @@ public class AmendContactData extends UseCase<AmendContactData> {
} }
} }
""")) """))
.expecting(HttpStatus.OK); .expecting(HttpStatus.OK)
);
return null; 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.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -13,7 +14,12 @@ public class PathAssertion {
this.path = path; this.path = path;
} }
public Consumer<UseCase<?>.HttpResponse> isEqualTo(final String resolvableValue) { @SuppressWarnings({ "unchecked", "rawtypes" })
return response -> response.path(path).isEqualTo(ScenarioTest.resolve(resolvableValue)); 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", "Verify if the Contact-Relation Got Replaced in the Partner-Relation",
() -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}")) () -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
.expecting(OK).expecting(JSON).expectArrayElements(1), .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 @Override
protected HttpResponse run() { protected HttpResponse run() {
httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - delete debitor}") withTitle("Delete the Debitor using its UUID", () ->
.expecting(HttpStatus.NO_CONTENT); httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - delete debitor}")
.expecting(HttpStatus.NO_CONTENT)
);
return null; 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. // TODO.spec: When to allow actual deletion of SEPA-mandates? Add constraint accordingly.
httpDelete("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}") return withTitle("Delete the SEPA-Mandate by its UUID", () -> httpDelete("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}")
.expecting(HttpStatus.NO_CONTENT); .expecting(HttpStatus.NO_CONTENT)
return null; );
} }
} }

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!" "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} "validUntil": ${mandateValidUntil}
} }
""")) """))
.expecting(OK).expecting(JSON); .expecting(OK).expecting(JSON)
);
} }
} }

View File

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

View File

@ -14,12 +14,14 @@ public class CreatePerson extends UseCase<CreatePerson> {
@Override @Override
protected HttpResponse run() { protected HttpResponse run() {
return httpPost("/api/hs/office/persons", usingJsonBody(""" return withTitle("Create the Person", () ->
httpPost("/api/hs/office/persons", usingJsonBody("""
{ {
"personType": ${personType}, "personType": ${personType},
"tradeName": ${tradeName} "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; 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.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.http.HttpStatus.NO_CONTENT;
@ -16,14 +16,20 @@ public class RemoveOperationsContactFromPartner extends UseCase<RemoveOperations
@Override @Override
protected HttpResponse run() { protected HttpResponse run() {
obtain("Operations-Contact: %{operationsContactPerson}", () -> obtain("Operations-Contact: %{operationsContactPerson}",
httpGet("/api/hs/office/relations?relationType=OPERATIONS&name=" + uriEncoded("%{operationsContactPerson}")) () ->
httpGet("/api/hs/office/relations?relationType=OPERATIONS&name=" + uriEncoded(
"%{operationsContactPerson}"))
.expecting(OK).expecting(JSON), .expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"), 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/&{Operations-Contact: %{operationsContactPerson}}") withTitle("Delete the Contact", () ->
.expecting(NO_CONTENT); 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." "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}}") return withTitle("Delete the Subscriber-Relation by its UUID", () ->
.expecting(NO_CONTENT); httpDelete("/api/hs/office/relations/&{Subscription: %{subscriberEMailAddress}}")
.expecting(NO_CONTENT)
);
} }
} }