Compare commits

..

2 Commits

15 changed files with 184 additions and 71 deletions

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

@ -6,9 +6,10 @@ import com.jayway.jsonpath.JsonPath;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
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;
@ -25,12 +26,15 @@ 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.Function; import java.util.function.Function;
import java.util.function.Supplier; 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;
@ -107,33 +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 validate(
// final String title,
// final Supplier<HttpResponse> http,
// final Function<HttpResponse, String> extractor,
// final String... extraInfo) {
// withTitle(ScenarioTest.resolve(title), () -> {
// http.get();
// Arrays.stream(extraInfo).forEach(testReport::printPara);
// });
// }
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;
}); });
} }
private 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
@ -193,6 +189,21 @@ public abstract class UseCase<T extends UseCase<?>> {
return new HttpResponse(HttpMethod.DELETE, uriPath, null, response); return new HttpResponse(HttpMethod.DELETE, uriPath, null, response);
} }
protected PathAssertion path(final String path) {
return new PathAssertion(path);
}
protected void validate(
final String title,
final Supplier<UseCase.HttpResponse> http,
final Consumer<UseCase.HttpResponse> assertion) {
withTitle(ScenarioTest.resolve(title), () -> {
final var response = http.get();
assertion.accept(response);
return response;
});
}
public final UUID uuid(final String alias) { public final UUID uuid(final String alias) {
return ScenarioTest.uuid(alias); return ScenarioTest.uuid(alias);
} }
@ -214,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;
@ -253,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();
@ -261,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
@ -288,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
@ -300,6 +322,8 @@ public abstract class UseCase<T extends UseCase<?>> {
testReport.printLine("\n### " + nextTitle + "\n"); testReport.printLine("\n### " + nextTitle + "\n");
} else if (resultAlias != null) { } else if (resultAlias != null) {
testReport.printLine("\n### " + resultAlias + "\n"); testReport.printLine("\n### " + resultAlias + "\n");
} else {
fail("please wrap the http...-call in the UseCase using `withTitle(...)`");
} }
// the request // the request

View File

@ -4,8 +4,6 @@ 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 org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import java.util.function.Consumer;
import java.util.function.Supplier;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.HttpStatus.OK;
@ -27,14 +25,16 @@ public class AddPhoneNumberToContactData extends UseCase<AddPhoneNumberToContact
"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."
); );
withTitle("Patch the Additional Phone-Number into the Contact", () ->
httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody(""" httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
{ {
"phoneNumbers": { "phoneNumbers": {
${newOfficePhoneNumberKey}: ${newOfficePhoneNumber} ${phoneNumberKeyToAdd}: ${phoneNumberToAdd}
} }
} }
""")) """))
.expecting(HttpStatus.OK); .expecting(HttpStatus.OK)
);
return null; return null;
} }
@ -42,23 +42,10 @@ public class AddPhoneNumberToContactData extends UseCase<AddPhoneNumberToContact
@Override @Override
protected void verify() { protected void verify() {
validate( validate(
"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}")
); );
} }
private PathAssertion path(final String path) {
return new PathAssertion(path);
}
private void validate(
final String title,
final Supplier<HttpResponse> http,
final Consumer<UseCase.HttpResponse> assertion) {
testSuite.testReport.printPara("### " + title);
final var response = http.get();
assertion.accept(response);
}
} }

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." "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(""" httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
{ {
"caption": ${newContactCaption???}, "caption": ${newContactCaption???},
@ -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

@ -40,13 +40,25 @@ public class ReplaceContactData extends UseCase<ReplaceContactData> {
"Please check first if that contact already exists, if so, use it's UUID below." "Please check first if that contact already exists, if so, use it's UUID below."
); );
withTitle("Replace the Contact-Reference in the Partner-Relation", () ->
httpPatch("/api/hs/office/relations/%{partnerRelationUuid}", usingJsonBody(""" httpPatch("/api/hs/office/relations/%{partnerRelationUuid}", usingJsonBody("""
{ {
"contactUuid": ${Contact: %{newContactCaption}} "contactUuid": ${Contact: %{newContactCaption}}
} }
""")) """))
.expecting(OK); .expecting(OK)
);
return null; return null;
} }
@Override
protected void verify() {
validate(
"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").contains("%{newContactCaption}")
);
}
} }

View File

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