feature/add-scenario-test-for-deceased-partner-with-community-of-heirs #137
@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.config;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import org.openapitools.jackson.nullable.JsonNullableModule;
|
import org.openapitools.jackson.nullable.JsonNullableModule;
|
||||||
@ -14,6 +15,10 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class JsonObjectMapperConfiguration {
|
public class JsonObjectMapperConfiguration {
|
||||||
|
|
||||||
|
public static ObjectMapper build() {
|
||||||
|
return new JsonObjectMapperConfiguration().customObjectMapper().build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Primary
|
@Primary
|
||||||
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
||||||
|
@ -29,6 +29,7 @@ import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddOperationsContac
|
|||||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddRepresentativeToPartner;
|
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddRepresentativeToPartner;
|
||||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner;
|
import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner;
|
||||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.DeletePartner;
|
import net.hostsharing.hsadminng.hs.office.scenarios.partner.DeletePartner;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.scenarios.partner.ReplaceDeceasedPartnerWithCommunityOfHeirs;
|
||||||
import net.hostsharing.hsadminng.hs.office.scenarios.person.ShouldUpdatePersonData;
|
import net.hostsharing.hsadminng.hs.office.scenarios.person.ShouldUpdatePersonData;
|
||||||
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.RemoveOperationsContactFromPartner;
|
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.RemoveOperationsContactFromPartner;
|
||||||
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeExistingPersonAndContactToMailinglist;
|
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeExistingPersonAndContactToMailinglist;
|
||||||
@ -93,6 +94,8 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
""")
|
""")
|
||||||
.given("officePhoneNumber", "+49 40 654321-0")
|
.given("officePhoneNumber", "+49 40 654321-0")
|
||||||
.given("emailAddress", "hamburg@test-ag.example.org")
|
.given("emailAddress", "hamburg@test-ag.example.org")
|
||||||
|
.given("registrationOffice", "Registergericht Hamburg")
|
||||||
|
.given("registrationNumber", "1234567")
|
||||||
.doRun()
|
.doRun()
|
||||||
.keep();
|
.keep();
|
||||||
}
|
}
|
||||||
@ -118,6 +121,9 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
""")
|
""")
|
||||||
.given("officePhoneNumber", "+49 40 123456")
|
.given("officePhoneNumber", "+49 40 123456")
|
||||||
.given("emailAddress", "michelle.matthieu@example.org")
|
.given("emailAddress", "michelle.matthieu@example.org")
|
||||||
|
.given("birthday", "1951-03-25")
|
||||||
|
.given("birthPlace", "Neustadt a.d.R.")
|
||||||
|
.given("birthName", "Eichbaum")
|
||||||
.doRun()
|
.doRun()
|
||||||
.keep();
|
.keep();
|
||||||
}
|
}
|
||||||
@ -221,16 +227,17 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
new ReplaceContactData(scenarioTest)
|
new ReplaceContactData(scenarioTest)
|
||||||
.given("partnerName", "Test AG")
|
.given("partnerName", "Test AG")
|
||||||
.given("newContactCaption", "Test AG - China")
|
.given("newContactCaption", "Test AG - China")
|
||||||
.given("newPostalAddress", """
|
.given(
|
||||||
"firm": "Test AG",
|
"newPostalAddress", """
|
||||||
"name": "Fi Zhong-Kha",
|
"firm": "Test AG",
|
||||||
"building": "Thi Chi Koh Building",
|
"name": "Fi Zhong-Kha",
|
||||||
"street": "No.2 Commercial Second Street",
|
"building": "Thi Chi Koh Building",
|
||||||
"district": "Niushan Wei Wu",
|
"street": "No.2 Commercial Second Street",
|
||||||
"city": "Dongguan City",
|
"district": "Niushan Wei Wu",
|
||||||
"province": "Guangdong Province",
|
"city": "Dongguan City",
|
||||||
"country": "China"
|
"province": "Guangdong Province",
|
||||||
""")
|
"country": "China"
|
||||||
|
""")
|
||||||
.given("newOfficePhoneNumber", "++15 999 654321")
|
.given("newOfficePhoneNumber", "++15 999 654321")
|
||||||
.given("newEmailAddress", "norden@test-ag.example.org")
|
.given("newEmailAddress", "norden@test-ag.example.org")
|
||||||
.doRun();
|
.doRun();
|
||||||
@ -257,6 +264,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
@Order(20)
|
@Order(20)
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
class DebitorScenarios {
|
class DebitorScenarios {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(2010)
|
@Order(2010)
|
||||||
@Requires("Partner: P-31010 - Test AG")
|
@Requires("Partner: P-31010 - Test AG")
|
||||||
@ -601,4 +609,31 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
.doRun();
|
.doRun();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@Order(60)
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
class PartnerDeceasedScenarios {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(6010)
|
||||||
|
@Requires("Partner: P-31011 - Michelle Matthieu")
|
||||||
|
void shouldReplaceDeceasedPartnerByCommunityOfHeirs() {
|
||||||
|
new ReplaceDeceasedPartnerWithCommunityOfHeirs(scenarioTest)
|
||||||
|
.given("partnerNumber", "P-31011")
|
||||||
|
.given("dateOfDeath", "2024-11-15")
|
||||||
|
.given("representativeGivenName", "Lena")
|
||||||
|
.given("representativeFamilyName", "Stadland")
|
||||||
|
.given(
|
||||||
|
"communityOfHeirsPostalAddress", """
|
||||||
|
"street": "Im Wischer 14",
|
||||||
|
"zipcode": "22987",
|
||||||
|
"city": "Hamburg",
|
||||||
|
"country": "Germany"
|
||||||
|
""")
|
||||||
|
.given("communityOfHeirsOfficePhoneNumber", "+49 40 666666")
|
||||||
|
.given("communityOfHeirsEmailAddress", "lena.stadland@example.org")
|
||||||
|
.doRun();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,8 +69,11 @@ public class CreatePartner extends UseCase<CreatePartner> {
|
|||||||
"contact.uuid": ${Contact: %{contactCaption}}
|
"contact.uuid": ${Contact: %{contactCaption}}
|
||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
"registrationOffice": "Registergericht Hamburg",
|
"birthday": ${birthday???},
|
||||||
"registrationNumber": "1234567"
|
"birthPlace": ${birthPlace???},
|
||||||
|
"birthName": ${birthName???},
|
||||||
|
"registrationOffice": ${registrationOffice???},
|
||||||
|
"registrationNumber": ${registrationNumber???}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
|
@ -0,0 +1,139 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import static io.restassured.http.ContentType.JSON;
|
||||||
|
import static org.springframework.http.HttpStatus.CREATED;
|
||||||
|
import static org.springframework.http.HttpStatus.OK;
|
||||||
|
|
||||||
|
public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceDeceasedPartnerWithCommunityOfHeirs> {
|
||||||
|
|
||||||
|
public ReplaceDeceasedPartnerWithCommunityOfHeirs(final ScenarioTest testSuite) {
|
||||||
|
super(testSuite);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpResponse run() {
|
||||||
|
|
||||||
|
obtain("Person: Hostsharing eG", () ->
|
||||||
|
httpGet("/api/hs/office/persons?name=Hostsharing+eG")
|
||||||
|
.expecting(OK).expecting(JSON),
|
||||||
|
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
|
||||||
|
"Even in production data we expect this query to return just a single result." // TODO.impl: add constraint?
|
||||||
|
);
|
||||||
|
|
||||||
|
obtain("Partner: %{partnerNumber}", () ->
|
||||||
|
httpGet("/api/hs/office/partners/%{partnerNumber}")
|
||||||
|
.reportWithResponse().expecting(OK).expecting(JSON),
|
||||||
|
response -> response.getFromBody("uuid"),
|
||||||
|
"Even in production data we expect this query to return just a single result." // TODO.impl: add constraint?
|
||||||
|
)
|
||||||
|
.extractValue("partnerRel.holder.familyName", "familyNameOfDeceasedPerson")
|
||||||
|
.extractValue("partnerRel.holder.givenName", "givenNameOfDeceasedPerson")
|
||||||
|
.extractUuidAlias("partnerRel.holder.uuid", "Person: %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}");
|
||||||
|
|
||||||
|
obtain("Partner-Relation: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () ->
|
||||||
|
httpPost("/api/hs/office/relations", usingJsonBody("""
|
||||||
|
{
|
||||||
|
"type": "PARTNER",
|
||||||
|
"anchor.uuid": ${Person: Hostsharing eG},
|
||||||
|
"holder": {
|
||||||
|
"personType": "UNINCORPORATED_FIRM",
|
||||||
|
"tradeName": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"caption": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
||||||
|
"postalAddress": {
|
||||||
|
"name": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
|
||||||
|
"co": "%{representativeGivenName} %{representativeFamilyName}",
|
||||||
|
%{communityOfHeirsPostalAddress}
|
||||||
|
},
|
||||||
|
"phoneNumbers": {
|
||||||
|
"office": ${communityOfHeirsOfficePhoneNumber}
|
||||||
|
},
|
||||||
|
"emailAddresses": {
|
||||||
|
"main": ${communityOfHeirsEmailAddress}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""))
|
||||||
|
.reportWithResponse().expecting(CREATED).expecting(JSON)
|
||||||
|
)
|
||||||
|
.extractUuidAlias("contact.uuid", "Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}")
|
||||||
|
.extractUuidAlias("holder.uuid", "Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}");
|
||||||
|
|
||||||
|
obtain("Representative-Relation: %{representativeGivenName} %{representativeFamilyName} for Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () ->
|
||||||
|
httpPost("/api/hs/office/relations", usingJsonBody("""
|
||||||
|
{
|
||||||
|
"type": "REPRESENTATIVE",
|
||||||
|
"anchor.uuid": ${Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}},
|
||||||
|
"holder": {
|
||||||
|
"personType": "NATURAL_PERSON",
|
||||||
|
"givenName": ${representativeGivenName},
|
||||||
|
"familyName": ${representativeFamilyName}
|
||||||
|
},
|
||||||
|
"contact.uuid": ${Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}
|
||||||
|
}
|
||||||
|
"""))
|
||||||
|
.reportWithResponse().expecting(CREATED).expecting(JSON)
|
||||||
|
).extractUuidAlias("holder.uuid", "Person: %{representativeGivenName} %{representativeFamilyName}");
|
||||||
|
|
||||||
|
obtain("Partner: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () ->
|
||||||
|
httpPatch("/api/hs/office/partners/%{Partner: %{partnerNumber}}", usingJsonBody("""
|
||||||
|
{
|
||||||
|
"partnerRel.uuid": ${Partner-Relation: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}
|
||||||
|
}
|
||||||
|
"""))
|
||||||
|
.expecting(HttpStatus.OK)
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO.test: missing steps Debitor, Membership, Coop-Shares+Assets
|
||||||
|
|
||||||
|
// Debitors
|
||||||
|
|
||||||
|
// die Erbengemeinschaft wird als Anchor-Person (Partner) in die Debitor-Relations eingetragen
|
||||||
|
// der neue Rechnungsempfänger (z.B. auch ggf. Rechtsanwalt) wird als Holder-Person (Debitor-Person) in die Debitor-Relations eingetragen -- oder neu?
|
||||||
|
|
||||||
|
// Membership
|
||||||
|
|
||||||
|
// intro: die Mitgliedschaft geht juristisch gesehen auf die Erbengemeinschaft über
|
||||||
|
|
||||||
|
// die bisherige Mitgliedschaft als DECEASED mit Ende-Datum=Todesdatum markieren
|
||||||
|
|
||||||
|
// eine neue Mitgliedschaft (-00) mit dem Start-Datum=Todesdatum+1 anlegen
|
||||||
|
|
||||||
|
// die Geschäftsanteile per share-tx: TRANSFER→ADOPT an die Erbengemeinschaft übertragen
|
||||||
|
// die Geschäftsguthaben per asset-tx: TRANSFER→ADOPT an die Erbengemeinschaft übertragen
|
||||||
|
|
||||||
|
// outro: die Erbengemeinschaft hat eine Frist von 6 Monaten, um die Mitgliedschaft einer Person zu übertragen
|
||||||
|
// →nächster "Drecksfall"
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void verify(final UseCase<ReplaceDeceasedPartnerWithCommunityOfHeirs>.HttpResponse response) {
|
||||||
|
verify(
|
||||||
|
"Verify the Updated Partner",
|
||||||
|
() -> httpGet("/api/hs/office/partners/%{partnerNumber}")
|
||||||
|
.expecting(OK).expecting(JSON).expectObject(),
|
||||||
|
path("partnerRel.holder.tradeName").contains("Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"),
|
||||||
|
path("[0].holder.familyName").contains("%{representativeFamilyName}")
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO.test: Verify Debitor, Membership, Coop-Shares and Coop-Assets once implemented
|
||||||
|
}
|
||||||
|
}
|
@ -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 + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ import java.math.BigDecimal;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -50,16 +49,7 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
return Optional.of(currentTestMethodProduces.pop());
|
return Optional.of(currentTestMethodProduces.pop());
|
||||||
}
|
}
|
||||||
|
|
||||||
record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {
|
private final static Map<String, UUID> aliases = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return Objects.toString(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private final static Map<String, Alias<?>> aliases = new HashMap<>();
|
|
||||||
|
|
||||||
private final static Map<String, Object> properties = new HashMap<>();
|
private final static Map<String, Object> properties = new HashMap<>();
|
||||||
public final TestReport testReport = new TestReport(aliases);
|
public final TestReport testReport = new TestReport(aliases);
|
||||||
@ -90,18 +80,7 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void afterScenario(final TestInfo testInfo) { // final TestInfo testInfo
|
void afterScenario(final TestInfo testInfo) { // final TestInfo testInfo
|
||||||
testInfo.getTestMethod() .ifPresent(currentTestMethod -> {
|
verifyProduceDeclaration(testInfo);
|
||||||
// FIXME: extract to method
|
|
||||||
final var producesAnnot = currentTestMethod.getAnnotation(Produces.class);
|
|
||||||
if (producesAnnot != null && producesAnnot.permanent()) {
|
|
||||||
final var testMethodProduces = producedAliases(producesAnnot);
|
|
||||||
testMethodProduces.forEach(declaredAlias ->
|
|
||||||
assertThat(knowVariables().containsKey(declaredAlias))
|
|
||||||
.as("@Producer method " + currentTestMethod.getName() +
|
|
||||||
" did declare but not produce \"" + declaredAlias + "\"")
|
|
||||||
.isTrue() );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
properties.clear();
|
properties.clear();
|
||||||
testReport.close();
|
testReport.close();
|
||||||
@ -111,14 +90,11 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
jpaAttempt.transacted(() ->
|
jpaAttempt.transacted(() ->
|
||||||
{
|
{
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
aliases.put(
|
putAlias(
|
||||||
"Person: Hostsharing eG",
|
"Person: Hostsharing eG",
|
||||||
new Alias<>(
|
personRepo.findPersonByOptionalNameLike("Hostsharing eG").stream()
|
||||||
null,
|
|
||||||
personRepo.findPersonByOptionalNameLike("Hostsharing eG")
|
|
||||||
.stream()
|
|
||||||
.map(HsOfficePersonRbacEntity::getUuid)
|
.map(HsOfficePersonRbacEntity::getUuid)
|
||||||
.reduce(Reducer::toSingleElement).orElseThrow())
|
.reduce(Reducer::toSingleElement).orElseThrow()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -195,6 +171,20 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void verifyProduceDeclaration(final TestInfo testInfo) {
|
||||||
|
testInfo.getTestMethod().ifPresent(currentTestMethod -> {
|
||||||
|
final var producesAnnot = currentTestMethod.getAnnotation(Produces.class);
|
||||||
|
if (producesAnnot != null && producesAnnot.permanent()) {
|
||||||
|
final var testMethodProduces = producedAliases(producesAnnot);
|
||||||
|
testMethodProduces.forEach(declaredAlias ->
|
||||||
|
assertThat(knowVariables().containsKey(declaredAlias))
|
||||||
|
.as("@Producer method " + currentTestMethod.getName() +
|
||||||
|
" did declare but not produce \"" + declaredAlias + "\"")
|
||||||
|
.isTrue() );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static boolean containsAlias(final String alias) {
|
static boolean containsAlias(final String alias) {
|
||||||
return aliases.containsKey(alias);
|
return aliases.containsKey(alias);
|
||||||
}
|
}
|
||||||
@ -210,7 +200,7 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
return alias;
|
return alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void putAlias(final String name, final Alias<?> value) {
|
static void putAlias(final String name, final UUID value) {
|
||||||
aliases.put(name, value);
|
aliases.put(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +214,7 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
|
|
||||||
static Map<String, Object> knowVariables() {
|
static Map<String, Object> knowVariables() {
|
||||||
final var map = new LinkedHashMap<String, Object>();
|
final var map = new LinkedHashMap<String, Object>();
|
||||||
ScenarioTest.aliases.forEach((key, value) -> map.put(key, value.uuid()));
|
map.putAll(ScenarioTest.aliases);
|
||||||
map.putAll(ScenarioTest.properties);
|
map.putAll(ScenarioTest.properties);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package net.hostsharing.hsadminng.hs.scenarios;
|
package net.hostsharing.hsadminng.hs.scenarios;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||||
import net.hostsharing.hsadminng.system.SystemProcess;
|
import net.hostsharing.hsadminng.system.SystemProcess;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.junit.jupiter.api.Order;
|
import org.junit.jupiter.api.Order;
|
||||||
@ -15,6 +18,7 @@ import java.text.SimpleDateFormat;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import static java.lang.String.join;
|
import static java.lang.String.join;
|
||||||
@ -23,10 +27,12 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
public class TestReport {
|
public class TestReport {
|
||||||
|
|
||||||
public static final File BUILD_DOC_SCENARIOS = new File("build/doc/scenarios");
|
public static final File BUILD_DOC_SCENARIOS = new File("build/doc/scenarios");
|
||||||
private final static File markdownLogFile = new File(BUILD_DOC_SCENARIOS, ".last-debug-log.md");
|
|
||||||
public static final SimpleDateFormat MM_DD_YYYY_HH_MM_SS = new SimpleDateFormat("MM-dd-yyyy hh:mm:ss");
|
public static final SimpleDateFormat MM_DD_YYYY_HH_MM_SS = new SimpleDateFormat("MM-dd-yyyy hh:mm:ss");
|
||||||
|
|
||||||
private final Map<String, ?> aliases;
|
private static final File markdownLogFile = new File(BUILD_DOC_SCENARIOS, ".last-debug-log.md");
|
||||||
|
private static final ObjectMapper objectMapper = JsonObjectMapperConfiguration.build();
|
||||||
|
|
||||||
|
private final Map<String, UUID> aliases;
|
||||||
private final PrintWriter markdownLog; // records everything for debugging purposes
|
private final PrintWriter markdownLog; // records everything for debugging purposes
|
||||||
private File markdownReportFile;
|
private File markdownReportFile;
|
||||||
private PrintWriter markdownReport; // records only the use-case under test, without its pre-requisites
|
private PrintWriter markdownReport; // records only the use-case under test, without its pre-requisites
|
||||||
@ -38,7 +44,7 @@ public class TestReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public TestReport(final Map<String, ?> aliases) {
|
public TestReport(final Map<String, UUID> aliases) {
|
||||||
this.aliases = aliases;
|
this.aliases = aliases;
|
||||||
this.markdownLog = new PrintWriter(new FileWriter(markdownLogFile));
|
this.markdownLog = new PrintWriter(new FileWriter(markdownLogFile));
|
||||||
}
|
}
|
||||||
@ -76,6 +82,11 @@ public class TestReport {
|
|||||||
printLine("\n" +output + "\n");
|
printLine("\n" +output + "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public void printJson(final String json) {
|
||||||
|
printLine(prettyJson(json));
|
||||||
|
}
|
||||||
|
|
||||||
void silent(final Runnable code) {
|
void silent(final Runnable code) {
|
||||||
silent++;
|
silent++;
|
||||||
code.run();
|
code.run();
|
||||||
@ -100,6 +111,14 @@ public class TestReport {
|
|||||||
return convertedTestMethodName.replaceAll(": should ", ": ");
|
return convertedTestMethodName.replaceAll(": should ", ": ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String prettyJson(final String json) throws JsonProcessingException {
|
||||||
|
if (json == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
final var jsonNode = objectMapper.readTree(json);
|
||||||
|
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
|
||||||
|
}
|
||||||
|
|
||||||
private String asClickableLink(final File file) {
|
private String asClickableLink(final File file) {
|
||||||
return file.toURI().toString().replace("file:/", "file:///");
|
return file.toURI().toString().replace("file:/", "file:///");
|
||||||
}
|
}
|
||||||
@ -113,9 +132,8 @@ public class TestReport {
|
|||||||
final var result = new StringBuilder();
|
final var result = new StringBuilder();
|
||||||
|
|
||||||
for (String line : lines) {
|
for (String line : lines) {
|
||||||
for (Map.Entry<String, ?> entry : aliases.entrySet()) {
|
for (Map.Entry<String, UUID> entry : aliases.entrySet()) {
|
||||||
final var uuidString = entry.getValue().toString();
|
if ( entry.getValue() != null && line.contains(entry.getValue().toString())) {
|
||||||
if (line.contains(uuidString)) {
|
|
||||||
line = line + " // " + entry.getKey();
|
line = line + " // " + entry.getKey();
|
||||||
break; // only add comment for one UUID per row (in our case, there is only one per row)
|
break; // only add comment for one UUID per row (in our case, there is only one per row)
|
||||||
}
|
}
|
||||||
@ -151,4 +169,8 @@ public class TestReport {
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSilent() {
|
||||||
|
return silent > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,29 +122,33 @@ 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);
|
||||||
|
response.optionallyReportRequestAndResponse();
|
||||||
Arrays.stream(extraInfo).forEach(testReport::printPara);
|
Arrays.stream(extraInfo).forEach(testReport::printPara);
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) {
|
public final HttpResponse obtain(final String alias, final Supplier<HttpResponse> httpCall, final String... extraInfo) {
|
||||||
withTitle(alias, () -> {
|
return withTitle(alias, () -> {
|
||||||
final var response = http.get().keep();
|
final var response = httpCall.get().keep();
|
||||||
|
response.optionallyReportRequestAndResponse();
|
||||||
Arrays.stream(extraInfo).forEach(testReport::printPara);
|
Arrays.stream(extraInfo).forEach(testReport::printPara);
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponse withTitle(final String resolvableTitle, final Supplier<HttpResponse> code) {
|
public HttpResponse withTitle(final String resolvableTitle, final Supplier<HttpResponse> httpCall, final String... extraInfo) {
|
||||||
this.nextTitle = resolvableTitle;
|
this.nextTitle = resolvableTitle;
|
||||||
final var response = code.get();
|
final var response = httpCall.get();
|
||||||
|
response.optionallyReportRequestAndResponse();
|
||||||
|
Arrays.stream(extraInfo).forEach(testReport::printPara);
|
||||||
this.nextTitle = null;
|
this.nextTitle = null;
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -261,6 +265,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 +278,9 @@ public abstract class UseCase<T extends UseCase<?>> {
|
|||||||
@Getter
|
@Getter
|
||||||
private UUID locationUuid;
|
private UUID locationUuid;
|
||||||
|
|
||||||
|
private boolean reportGenerated = false;
|
||||||
|
private boolean reportGeneratedWithResponse = false;
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public HttpResponse(
|
public HttpResponse(
|
||||||
final HttpMethod httpMethod,
|
final HttpMethod httpMethod,
|
||||||
@ -277,6 +288,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,40 +298,42 @@ 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();
|
||||||
|
|
||||||
final var value = extractor.apply(this);
|
final var value = extractor.apply(this);
|
||||||
ScenarioTest.putAlias(
|
ScenarioTest.putAlias(alias, UUID.fromString(value));
|
||||||
alias,
|
|
||||||
new ScenarioTest.Alias<>(UseCase.this.getClass(), UUID.fromString(value)));
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponse keepAs(final String alias) {
|
public HttpResponse keepAs(final String alias) {
|
||||||
ScenarioTest.putAlias(
|
optionallyReportRequestAndResponse();
|
||||||
nonNullAlias(alias),
|
|
||||||
new ScenarioTest.Alias<>(UseCase.this.getClass(), 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();
|
||||||
|
|
||||||
@ -326,6 +342,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();
|
||||||
|
|
||||||
@ -335,6 +353,15 @@ public abstract class UseCase<T extends UseCase<?>> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public HttpResponse expectObject() {
|
||||||
|
optionallyReportRequestAndResponse();
|
||||||
|
|
||||||
|
final var rootNode = objectMapper.readTree(response.body());
|
||||||
|
assertThat(rootNode.isArray()).as("object expected, but got array: " + response.body()).isFalse();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public <V> V getFromBody(final String path) {
|
public <V> V getFromBody(final String path) {
|
||||||
final var body = response.body();
|
final var body = response.body();
|
||||||
@ -357,14 +384,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 (reportGenerated) {
|
||||||
|
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(...)`");
|
||||||
}
|
}
|
||||||
@ -372,17 +408,34 @@ public abstract class UseCase<T extends UseCase<?>> {
|
|||||||
// the request
|
// the request
|
||||||
testReport.printLine("```");
|
testReport.printLine("```");
|
||||||
testReport.printLine(httpMethod.name() + " " + uri);
|
testReport.printLine(httpMethod.name() + " " + uri);
|
||||||
testReport.printLine((requestBody != null ? requestBody.trim() : ""));
|
testReport.printJson(requestBody);
|
||||||
|
|
||||||
// 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()) {
|
||||||
final var jsonNode = objectMapper.readTree(response.body());
|
testReport.printJson(response.body());
|
||||||
final var prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
|
this.reportGeneratedWithResponse = true;
|
||||||
testReport.printLine(prettyJson);
|
|
||||||
}
|
}
|
||||||
testReport.printLine("```");
|
testReport.printLine("```");
|
||||||
testReport.printLine("");
|
testReport.printLine("");
|
||||||
|
this.reportGenerated = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private void optionallyReportRequestAndResponse() {
|
||||||
|
if (!reportGenerated) {
|
||||||
|
reportRequestAndResponse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyResponseReported(final String action) {
|
||||||
|
if (!reportGenerated) {
|
||||||
|
throw new IllegalStateException("report not generated yet, but expected for `" + action + "`");
|
||||||
|
}
|
||||||
|
if (!reportGeneratedWithResponse) {
|
||||||
|
throw new IllegalStateException("report without response, but response report required for `" + action + "`");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String nonNullAlias(final String alias) {
|
private String nonNullAlias(final String alias) {
|
||||||
@ -391,6 +444,24 @@ public abstract class UseCase<T extends UseCase<?>> {
|
|||||||
final var onlyVisibleInGeneratedMarkdownNotInSource = new String(new char[]{'F', 'I', 'X', 'M', 'E'});
|
final var onlyVisibleInGeneratedMarkdownNotInSource = new String(new char[]{'F', 'I', 'X', 'M', 'E'});
|
||||||
return alias == null ? "unknown alias -- " + onlyVisibleInGeneratedMarkdownNotInSource : alias;
|
return alias == null ? "unknown alias -- " + onlyVisibleInGeneratedMarkdownNotInSource : alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpResponse extractUuidAlias(final String jsonPath, final String resolvableName) {
|
||||||
|
verifyResponseReported("extractUuidAlias");
|
||||||
|
|
||||||
|
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) {
|
||||||
|
verifyResponseReported("extractValue");
|
||||||
|
|
||||||
|
final var resolvedName = ScenarioTest.resolve(resolvableName, DROP_COMMENTS);
|
||||||
|
final var resolvedJsonPath = getFromBodyAsOptional(jsonPath).givenAsString();
|
||||||
|
ScenarioTest.putProperty(resolvedName, resolvedJsonPath);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected T self() {
|
protected T self() {
|
||||||
|
Loading…
Reference in New Issue
Block a user