feature/add-scenario-test-for-deceased-partner-with-community-of-heirs #137

Merged
5 changed files with 90 additions and 67 deletions
Showing only changes of commit 9b58997fb7 - Show all commits

View File

@ -634,8 +634,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
""") """)
.given("communityOfHeirsOfficePhoneNumber", "+49 40 666666") .given("communityOfHeirsOfficePhoneNumber", "+49 40 666666")
.given("communityOfHeirsEmailAddress", "lena.stadland@example.org") .given("communityOfHeirsEmailAddress", "lena.stadland@example.org")
.doRun() .doRun();
.keep();
} }
} }
} }

View File

@ -31,38 +31,7 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
.expecting(OK).expecting(JSON), .expecting(OK).expecting(JSON),
response -> response.getFromBody("uuid"), response -> response.getFromBody("uuid"),
"Even in production data we expect this query to return just a single result." // TODO.impl: add constraint? "Even in production data we expect this query to return just a single result." // TODO.impl: add constraint?
); ).extractValue("partnerRel.holder.uuid", "Person: %{nameOfDeceasedPerson}");
obtain("Person: Erbengemeinschaft %{nameOfDeceasedPerson}", () ->
httpPost("/api/hs/office/persons", usingJsonBody("""
{
"personType": "UNINCORPORATED_FIRM",
"tradeName": "Erbengemeinschaft %{nameOfDeceasedPerson}",
"givenName": ${givenName???},
"familyName": ${familyName???}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
// obtain("Contact: Erbengemeinschaft %{nameOfDeceasedPerson}", () ->
// httpPost("/api/hs/office/contacts", usingJsonBody("""
// {
// "caption": "Erbengemeinschaft %{nameOfDeceasedPerson}",
// "postalAddress": {
// "name": "Erbengemeinschaft %{nameOfDeceasedPerson}",
// %{communityOfHeirsPostalAddress}
// },
// "phoneNumbers": {
// "office": ${communityOfHeirsOfficePhoneNumber}
// },
// "emailAddresses": {
// "main": ${communityOfHeirsEmailAddress}
// }
// }
// """))
// .expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
// );
obtain("Partner-Relation: Erbengemeinschaft %{nameOfDeceasedPerson}", () -> obtain("Partner-Relation: Erbengemeinschaft %{nameOfDeceasedPerson}", () ->
httpPost("/api/hs/office/relations", usingJsonBody(""" httpPost("/api/hs/office/relations", usingJsonBody("""
@ -70,9 +39,10 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
"type": "PARTNER", "type": "PARTNER",
"anchor.uuid": ${Person: Hostsharing eG}, "anchor.uuid": ${Person: Hostsharing eG},
"holder": { "holder": {
"personType": "NATURAL_PERSON", "personType": "UNINCORPORATED_FIRM",
"givenName": ${representativeGivenName}, "tradeName": "Erbengemeinschaft %{nameOfDeceasedPerson}",
"familyName": ${representativeFamilyName} "givenName": ${givenName???},
"familyName": ${familyName???}
}, },
"contact": { "contact": {
"caption": "Erbengemeinschaft %{nameOfDeceasedPerson}", "caption": "Erbengemeinschaft %{nameOfDeceasedPerson}",
@ -89,26 +59,10 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
} }
} }
""")) """))
.expecting(CREATED).expecting(JSON) .reportWithResponse().expecting(CREATED).expecting(JSON)
) )
.extract("contact.uuid", "Contact: Erbengemeinschaft %{nameOfDeceasedPerson}") .extractUuidAlias("contact.uuid", "Contact: Erbengemeinschaft %{nameOfDeceasedPerson}")
.extract("holder.uuid", "Person: Erbengemeinschaft %{nameOfDeceasedPerson}"); .extractUuidAlias("holder.uuid", "Person: Erbengemeinschaft %{nameOfDeceasedPerson}");
// Repräsentanten Relation zur Erbengemeinschaft für die zuvor erzeugte Person zur Erbengemeinschaft anlegen
// obtain("Person: %{representativeGivenName} %{representativeFamilyName}", () ->
// httpPost("/api/hs/office/persons", usingJsonBody("""
// {
// "personType": "NATURAL_PERSON",
// "givenName": ${representativeGivenName},
// "familyName": ${representativeFamilyName}
// }
// """))
// .expecting(HttpStatus.CREATED).expecting(ContentType.JSON),
// "A community of heirs needs at least one representative.",
// "If there are more than one, each needs their own contact"
// );
obtain("Representative-Relation: %{representativeGivenName} %{representativeFamilyName} for Erbengemeinschaft %{nameOfDeceasedPerson}", () -> obtain("Representative-Relation: %{representativeGivenName} %{representativeFamilyName} for Erbengemeinschaft %{nameOfDeceasedPerson}", () ->
httpPost("/api/hs/office/relations", usingJsonBody(""" httpPost("/api/hs/office/relations", usingJsonBody("""
@ -125,7 +79,7 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
""")) """))
.expecting(CREATED).expecting(JSON), .expecting(CREATED).expecting(JSON),
"" ""
); ).extractUuidAlias("holder.uuid", "Person: %{representativeGivenName} %{representativeFamilyName}");
obtain("Partner-Relation: Erbengemeinschaft %{nameOfDeceasedPerson}", () -> obtain("Partner-Relation: Erbengemeinschaft %{nameOfDeceasedPerson}", () ->
httpPost("/api/hs/office/relations", usingJsonBody(""" httpPost("/api/hs/office/relations", usingJsonBody("""
@ -148,6 +102,8 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
.expecting(HttpStatus.OK) .expecting(HttpStatus.OK)
); );
// TODO.test: missing steps Debitor, Membership, Coop-Shares+Assets
// Debitors // Debitors
// die Erbengemeinschaft wird als Anchor-Person (Partner) in die Debitor-Relations eingetragen // die Erbengemeinschaft wird als Anchor-Person (Partner) in die Debitor-Relations eingetragen
@ -179,6 +135,17 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
path("partnerRel.holder.tradeName").contains("Erbengemeinschaft %{nameOfDeceasedPerson}") path("partnerRel.holder.tradeName").contains("Erbengemeinschaft %{nameOfDeceasedPerson}")
); );
// verify ex-partner relation // TODO.test: Verify the EX_PARTNER-Relation, once we fixed the anchor problem, see HsOfficePartnerController
// (net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerController.optionallyCreateExPartnerRelation)
verify(
"Verify the Representative-Relation",
() -> httpGet("/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: %{representativeGivenName} %{representativeFamilyName}}")
.expecting(OK).expecting(JSON).expectArrayElements(1),
path("[0].anchor.tradeName").contains("Erbengemeinschaft %{nameOfDeceasedPerson}"),
path("[0].holder.familyName").contains("%{representativeFamilyName}")
);
// TODO.test: Verify Debitor, Membership, Coop-Shares and Coop-Assets once implemented
} }
} }

View File

@ -1,5 +1,7 @@
package net.hostsharing.hsadminng.hs.scenarios; package net.hostsharing.hsadminng.hs.scenarios;
import java.util.Objects;
import java.util.UUID;
public final class JsonOptional<V> { public final class JsonOptional<V> {
@ -41,4 +43,12 @@ public final class JsonOptional<V> {
} }
return jsonValue == null ? null : jsonValue.toString(); return jsonValue == null ? null : jsonValue.toString();
} }
public UUID givenUUID() {
try {
return UUID.fromString(Objects.toString(jsonValue));
} catch(final IllegalArgumentException e) {
throw new ClassCastException("expected a UUID, but got '" + jsonValue + "'");
}
}
} }

View File

@ -169,4 +169,8 @@ public class TestReport {
return "unknown"; return "unknown";
} }
} }
public boolean isSilent() {
return silent > 0;
}
} }

View File

@ -122,12 +122,12 @@ public abstract class UseCase<T extends UseCase<?>> {
return new JsonTemplate(jsonTemplate); return new JsonTemplate(jsonTemplate);
} }
public final void obtain( public final HttpResponse obtain(
final String title, final String title,
final Supplier<HttpResponse> http, final Supplier<HttpResponse> http,
final Function<HttpResponse, String> extractor, final Function<HttpResponse, String> extractor,
final String... extraInfo) { final String... extraInfo) {
withTitle(title, () -> { return withTitle(title, () -> {
final var response = 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; return response;
@ -261,6 +261,10 @@ public abstract class UseCase<T extends UseCase<?>> {
public final class HttpResponse { public final class HttpResponse {
private final HttpMethod httpMethod;
private final String uri;
private final String requestBody;
@Getter @Getter
private final java.net.http.HttpResponse<String> response; private final java.net.http.HttpResponse<String> response;
@ -270,6 +274,8 @@ public abstract class UseCase<T extends UseCase<?>> {
@Getter @Getter
private UUID locationUuid; private UUID locationUuid;
private boolean reported = false;
@SneakyThrows @SneakyThrows
public HttpResponse( public HttpResponse(
final HttpMethod httpMethod, final HttpMethod httpMethod,
@ -277,6 +283,9 @@ public abstract class UseCase<T extends UseCase<?>> {
final String requestBody, final String requestBody,
final java.net.http.HttpResponse<String> response final java.net.http.HttpResponse<String> response
) { ) {
this.httpMethod = httpMethod;
this.uri = uri;
this.requestBody = requestBody;
this.response = response; this.response = response;
this.status = HttpStatus.valueOf(response.statusCode()); this.status = HttpStatus.valueOf(response.statusCode());
if (this.status == HttpStatus.CREATED) { if (this.status == HttpStatus.CREATED) {
@ -284,22 +293,24 @@ public abstract class UseCase<T extends UseCase<?>> {
assertThat(location).startsWith("http://localhost:"); assertThat(location).startsWith("http://localhost:");
locationUuid = UUID.fromString(location.substring(location.lastIndexOf('/') + 1)); locationUuid = UUID.fromString(location.substring(location.lastIndexOf('/') + 1));
} }
reportRequestAndResponse(httpMethod, uri, requestBody);
} }
public HttpResponse expecting(final HttpStatus httpStatus) { public HttpResponse expecting(final HttpStatus httpStatus) {
optionallyReportRequestAndResponse();
assertThat(HttpStatus.valueOf(response.statusCode())).isEqualTo(httpStatus); assertThat(HttpStatus.valueOf(response.statusCode())).isEqualTo(httpStatus);
return this; return this;
} }
public HttpResponse expecting(final ContentType contentType) { public HttpResponse expecting(final ContentType contentType) {
optionallyReportRequestAndResponse();
assertThat(response.headers().firstValue("content-type")) assertThat(response.headers().firstValue("content-type"))
.contains(contentType.toString()); .contains(contentType.toString());
return this; return this;
} }
public HttpResponse keep(final Function<HttpResponse, String> extractor) { public HttpResponse keep(final Function<HttpResponse, String> extractor) {
optionallyReportRequestAndResponse();
final var alias = nextTitle != null ? ScenarioTest.resolve(nextTitle, DROP_COMMENTS) : resultAlias; final var alias = nextTitle != null ? ScenarioTest.resolve(nextTitle, DROP_COMMENTS) : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull(); assertThat(alias).as("cannot keep result, no alias found").isNotNull();
@ -309,11 +320,15 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public HttpResponse keepAs(final String alias) { public HttpResponse keepAs(final String alias) {
optionallyReportRequestAndResponse();
ScenarioTest.putAlias(nonNullAlias(alias), locationUuid); ScenarioTest.putAlias(nonNullAlias(alias), locationUuid);
return this; return this;
} }
public HttpResponse keep() { public HttpResponse keep() {
optionallyReportRequestAndResponse();
final var alias = nextTitle != null ? ScenarioTest.resolve(nextTitle, DROP_COMMENTS) : resultAlias; final var alias = nextTitle != null ? ScenarioTest.resolve(nextTitle, DROP_COMMENTS) : resultAlias;
assertThat(alias).as("cannot keep result, no title or alias found for locationUuid: " + locationUuid).isNotNull(); assertThat(alias).as("cannot keep result, no title or alias found for locationUuid: " + locationUuid).isNotNull();
@ -322,6 +337,8 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows @SneakyThrows
public HttpResponse expectArrayElements(final int expectedElementCount) { public HttpResponse expectArrayElements(final int expectedElementCount) {
optionallyReportRequestAndResponse();
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();
@ -333,6 +350,8 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows @SneakyThrows
public HttpResponse expectObject() { public HttpResponse expectObject() {
optionallyReportRequestAndResponse();
final var rootNode = objectMapper.readTree(response.body()); final var rootNode = objectMapper.readTree(response.body());
assertThat(rootNode.isArray()).as("object expected, but got array: " + response.body()).isFalse(); assertThat(rootNode.isArray()).as("object expected, but got array: " + response.body()).isFalse();
return this; return this;
@ -360,14 +379,23 @@ public abstract class UseCase<T extends UseCase<?>> {
return assertThat(getFromBodyAsOptional(path).givenAsString()); return assertThat(getFromBodyAsOptional(path).givenAsString());
} }
public HttpResponse reportWithResponse() {
return reportRequestAndResponse(true);
}
@SneakyThrows @SneakyThrows
private void reportRequestAndResponse(final HttpMethod httpMethod, final String uri, final String requestBody) { private HttpResponse reportRequestAndResponse(final boolean unconditionallyWithResponse) {
if (reported) {
throw new IllegalStateException("request report already generated");
}
// the title // the title
if (nextTitle != null) { if (nextTitle != null) {
testReport.printLine("\n### " + ScenarioTest.resolve(nextTitle, KEEP_COMMENTS) + "\n"); testReport.printPara("### " + ScenarioTest.resolve(nextTitle, KEEP_COMMENTS));
} else if (resultAlias != null) { } else if (resultAlias != null) {
testReport.printLine("\n### Create " + resultAlias + "\n"); testReport.printPara("### Create " + resultAlias);
} else if (testReport.isSilent()) {
testReport.printPara("### Untitled Section");
} else { } else {
fail("please wrap the http...-call in the UseCase using `withTitle(...)`"); fail("please wrap the http...-call in the UseCase using `withTitle(...)`");
} }
@ -379,11 +407,19 @@ public abstract class UseCase<T extends UseCase<?>> {
// the response // the response
testReport.printLine("=> status: " + status + " " + (locationUuid != null ? locationUuid : "")); testReport.printLine("=> status: " + status + " " + (locationUuid != null ? locationUuid : ""));
if (httpMethod == HttpMethod.GET || status.isError()) { if (unconditionallyWithResponse || httpMethod == HttpMethod.GET || status.isError()) {
testReport.printJson(response.body()); testReport.printJson(response.body());
} }
testReport.printLine("```"); testReport.printLine("```");
testReport.printLine(""); testReport.printLine("");
return this;
}
@SneakyThrows
private void optionallyReportRequestAndResponse() {
if (!reported) {
reportRequestAndResponse(false);
}
} }
private String nonNullAlias(final String alias) { private String nonNullAlias(final String alias) {
@ -393,7 +429,14 @@ public abstract class UseCase<T extends UseCase<?>> {
return alias == null ? "unknown alias -- " + onlyVisibleInGeneratedMarkdownNotInSource : alias; return alias == null ? "unknown alias -- " + onlyVisibleInGeneratedMarkdownNotInSource : alias;
} }
public HttpResponse extract(final String jsonPath, final String resolvableName) { public HttpResponse extractUuidAlias(final String jsonPath, final String resolvableName) {
final var resolvedName = ScenarioTest.resolve(resolvableName, DROP_COMMENTS);
final var resolvedJsonPath = getFromBodyAsOptional(jsonPath).givenUUID();
ScenarioTest.putAlias(resolvedName, resolvedJsonPath);
return this;
}
public HttpResponse extractValue(final String jsonPath, final String resolvableName) {
final var resolvedName = ScenarioTest.resolve(resolvableName, DROP_COMMENTS); final var resolvedName = ScenarioTest.resolve(resolvableName, DROP_COMMENTS);
final var resolvedJsonPath = getFromBodyAsOptional(jsonPath).givenAsString(); final var resolvedJsonPath = getFromBodyAsOptional(jsonPath).givenAsString();
ScenarioTest.putProperty(resolvedName, resolvedJsonPath); ScenarioTest.putProperty(resolvedName, resolvedJsonPath);