feature/use-case-acceptance-tests (#116)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: #116 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
parent
c181500a1d
commit
3b94f117fb
@ -90,6 +90,20 @@ Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-cove
|
||||
TODO.test: Complete the Acceptance-Tests test concept.
|
||||
|
||||
|
||||
#### Scenario-Tests
|
||||
|
||||
Our Scenario-tests are induced by business use-cases.
|
||||
They test from the REST API all the way down to the database.
|
||||
|
||||
Most scenario-tests are positive tests, they test if business scenarios do work.
|
||||
But few might be negative tests, which test if specific forbidden data gets rejected.
|
||||
|
||||
Our scenario tests also generate test-reports which contain the REST-API calls needed for each scenario.
|
||||
These reports can be used as examples for the API usage from a business perspective.
|
||||
|
||||
There is an extra document regarding scenario-test, see [Scenario-Tests README](../src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md).
|
||||
|
||||
|
||||
#### Performance-Tests
|
||||
|
||||
Performance-critical scenarios have to be identified and a special performance-test has to be implemented.
|
||||
|
@ -77,16 +77,13 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
|
||||
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
|
||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none");
|
||||
Validate.isTrue(body.getDebitorRel() == null ||
|
||||
body.getDebitorRel().getType() == null || DEBITOR.name().equals(body.getDebitorRel().getType()),
|
||||
"ERROR: [400] debitorRel.type must be '"+DEBITOR.name()+"' or null for default");
|
||||
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
|
||||
"ERROR: [400] debitorRel.mark must be null");
|
||||
|
||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
||||
if ( body.getDebitorRel() != null ) {
|
||||
body.getDebitorRel().setType(DEBITOR.name());
|
||||
if (body.getDebitorRel() != null) {
|
||||
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
||||
debitorRel.setType(DEBITOR);
|
||||
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
||||
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
||||
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
||||
@ -95,7 +92,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
|
||||
debitorRelOptional.ifPresentOrElse(
|
||||
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
|
||||
() -> { throw new ValidationException("Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());});
|
||||
() -> {
|
||||
throw new ValidationException(
|
||||
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
|
||||
});
|
||||
}
|
||||
|
||||
final var savedEntity = debitorRepo.save(entityToSave);
|
||||
|
@ -0,0 +1,44 @@
|
||||
package net.hostsharing.hsadminng.reflection;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
|
||||
@UtilityClass
|
||||
public class AnnotationFinder {
|
||||
|
||||
@SneakyThrows
|
||||
public static <T extends Annotation> Optional<T> findCallerAnnotation(
|
||||
final Class<T> annotationClassToFind,
|
||||
final Class<? extends Annotation> annotationClassToStopLookup
|
||||
) {
|
||||
for (var element : Thread.currentThread().getStackTrace()) {
|
||||
final var clazz = Class.forName(element.getClassName());
|
||||
final var method = getMethodFromStackElement(clazz, element);
|
||||
|
||||
// Check if the method is annotated with the desired annotation
|
||||
if (method != null) {
|
||||
if (method.isAnnotationPresent(annotationClassToFind)) {
|
||||
return Optional.of(method.getAnnotation(annotationClassToFind));
|
||||
} else if (method.isAnnotationPresent(annotationClassToStopLookup)) {
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
return empty();
|
||||
}
|
||||
|
||||
private static Method getMethodFromStackElement(Class<?> clazz, StackTraceElement element) {
|
||||
for (var method : clazz.getDeclaredMethods()) {
|
||||
if (method.getName().equals(element.getMethodName())) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -17,10 +17,8 @@ components:
|
||||
minimum: 1000000
|
||||
maximum: 9999999
|
||||
debitorNumberSuffix:
|
||||
type: integer
|
||||
format: int8
|
||||
minimum: 00
|
||||
maximum: 99
|
||||
type: string
|
||||
pattern: '^[0-9][0-9]$'
|
||||
partner:
|
||||
$ref: 'hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
|
||||
billable:
|
||||
@ -76,15 +74,13 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
debitorRel:
|
||||
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
|
||||
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationSubInsert'
|
||||
debitorRelUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
debitorNumberSuffix:
|
||||
type: integer
|
||||
format: int8
|
||||
minimum: 00
|
||||
maximum: 99
|
||||
type: string
|
||||
pattern: '^[0-9][0-9]$'
|
||||
billable:
|
||||
type: boolean
|
||||
vatId:
|
||||
|
@ -41,6 +41,7 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
|
||||
# arbitrary relation with explicit type
|
||||
HsOfficeRelationInsert:
|
||||
type: object
|
||||
properties:
|
||||
@ -64,3 +65,24 @@ components:
|
||||
- holderUuid
|
||||
- type
|
||||
- contactUuid
|
||||
|
||||
# relation created as a sub-element with implicitly known type
|
||||
HsOfficeRelationSubInsert:
|
||||
type: object
|
||||
properties:
|
||||
anchorUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
holderUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
mark:
|
||||
type: string
|
||||
nullable: true
|
||||
contactUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- anchorUuid
|
||||
- holderUuid
|
||||
- contactUuid
|
||||
|
@ -36,7 +36,7 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM rbactest.customer WHERE uuid = NEW.customerUuid INTO newCustomer;
|
||||
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
|
||||
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s of package', NEW.customerUuid);
|
||||
|
||||
|
||||
perform rbac.defineRoleWithGrants(
|
||||
@ -102,10 +102,10 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM rbactest.customer WHERE uuid = OLD.customerUuid INTO oldCustomer;
|
||||
assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s', OLD.customerUuid);
|
||||
assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s of package', OLD.customerUuid);
|
||||
|
||||
SELECT * FROM rbactest.customer WHERE uuid = NEW.customerUuid INTO newCustomer;
|
||||
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
|
||||
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s of package', NEW.customerUuid);
|
||||
|
||||
|
||||
if NEW.customerUuid <> OLD.customerUuid then
|
||||
|
@ -36,7 +36,7 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM rbactest.package WHERE uuid = NEW.packageUuid INTO newPackage;
|
||||
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
|
||||
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s of domain', NEW.packageUuid);
|
||||
|
||||
|
||||
perform rbac.defineRoleWithGrants(
|
||||
@ -98,10 +98,10 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM rbactest.package WHERE uuid = OLD.packageUuid INTO oldPackage;
|
||||
assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s', OLD.packageUuid);
|
||||
assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s of domain', OLD.packageUuid);
|
||||
|
||||
SELECT * FROM rbactest.package WHERE uuid = NEW.packageUuid INTO newPackage;
|
||||
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
|
||||
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s of domain', NEW.packageUuid);
|
||||
|
||||
|
||||
if NEW.packageUuid <> OLD.packageUuid then
|
||||
|
@ -38,13 +38,13 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM hs_office.person WHERE uuid = NEW.holderUuid INTO newHolderPerson;
|
||||
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid);
|
||||
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s of relation', NEW.holderUuid);
|
||||
|
||||
SELECT * FROM hs_office.person WHERE uuid = NEW.anchorUuid INTO newAnchorPerson;
|
||||
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid);
|
||||
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s of relation', NEW.anchorUuid);
|
||||
|
||||
SELECT * FROM hs_office.contact WHERE uuid = NEW.contactUuid INTO newContact;
|
||||
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid);
|
||||
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s of relation', NEW.contactUuid);
|
||||
|
||||
|
||||
perform rbac.defineRoleWithGrants(
|
||||
|
@ -37,10 +37,10 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM hs_office.relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel;
|
||||
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
|
||||
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s of partner', NEW.partnerRelUuid);
|
||||
|
||||
SELECT * FROM hs_office.partner_details WHERE uuid = NEW.detailsUuid INTO newPartnerDetails;
|
||||
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s', NEW.detailsUuid);
|
||||
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s of partner', NEW.detailsUuid);
|
||||
|
||||
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'DELETE'), hs_office.relation_OWNER(newPartnerRel));
|
||||
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.relation_TENANT(newPartnerRel));
|
||||
@ -96,16 +96,16 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM hs_office.relation WHERE uuid = OLD.partnerRelUuid INTO oldPartnerRel;
|
||||
assert oldPartnerRel.uuid is not null, format('oldPartnerRel must not be null for OLD.partnerRelUuid = %s', OLD.partnerRelUuid);
|
||||
assert oldPartnerRel.uuid is not null, format('oldPartnerRel must not be null for OLD.partnerRelUuid = %s of partner', OLD.partnerRelUuid);
|
||||
|
||||
SELECT * FROM hs_office.relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel;
|
||||
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
|
||||
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s of partner', NEW.partnerRelUuid);
|
||||
|
||||
SELECT * FROM hs_office.partner_details WHERE uuid = OLD.detailsUuid INTO oldPartnerDetails;
|
||||
assert oldPartnerDetails.uuid is not null, format('oldPartnerDetails must not be null for OLD.detailsUuid = %s', OLD.detailsUuid);
|
||||
assert oldPartnerDetails.uuid is not null, format('oldPartnerDetails must not be null for OLD.detailsUuid = %s of partner', OLD.detailsUuid);
|
||||
|
||||
SELECT * FROM hs_office.partner_details WHERE uuid = NEW.detailsUuid INTO newPartnerDetails;
|
||||
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s', NEW.detailsUuid);
|
||||
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s of partner', NEW.detailsUuid);
|
||||
|
||||
|
||||
if NEW.partnerRelUuid <> OLD.partnerRelUuid then
|
||||
|
@ -44,10 +44,10 @@ begin
|
||||
WHERE partnerRel.type = 'PARTNER'
|
||||
AND NEW.debitorRelUuid = debitorRel.uuid
|
||||
INTO newPartnerRel;
|
||||
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
|
||||
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.debitorRelUuid = %s of debitor', NEW.debitorRelUuid);
|
||||
|
||||
SELECT * FROM hs_office.relation WHERE uuid = NEW.debitorRelUuid INTO newDebitorRel;
|
||||
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
|
||||
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorRelUuid = %s of debitor', NEW.debitorRelUuid);
|
||||
|
||||
SELECT * FROM hs_office.bankaccount WHERE uuid = NEW.refundBankAccountUuid INTO newRefundBankAccount;
|
||||
|
||||
|
@ -37,14 +37,14 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM hs_office.bankaccount WHERE uuid = NEW.bankAccountUuid INTO newBankAccount;
|
||||
assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s', NEW.bankAccountUuid);
|
||||
assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s of sepamandate', NEW.bankAccountUuid);
|
||||
|
||||
SELECT debitorRel.*
|
||||
FROM hs_office.relation debitorRel
|
||||
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
|
||||
WHERE debitor.uuid = NEW.debitorUuid
|
||||
INTO newDebitorRel;
|
||||
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
|
||||
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s of sepamandate', NEW.debitorUuid);
|
||||
|
||||
|
||||
perform rbac.defineRoleWithGrants(
|
||||
|
@ -40,7 +40,7 @@ begin
|
||||
JOIN hs_office.relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
|
||||
WHERE partner.uuid = NEW.partnerUuid
|
||||
INTO newPartnerRel;
|
||||
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerUuid = %s', NEW.partnerUuid);
|
||||
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerUuid = %s of membership', NEW.partnerUuid);
|
||||
|
||||
|
||||
perform rbac.defineRoleWithGrants(
|
||||
|
@ -36,7 +36,7 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM hs_office.membership WHERE uuid = NEW.membershipUuid INTO newMembership;
|
||||
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s', NEW.membershipUuid);
|
||||
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s of coopshares', NEW.membershipUuid);
|
||||
|
||||
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.membership_AGENT(newMembership));
|
||||
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership));
|
||||
|
@ -36,7 +36,7 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM hs_office.membership WHERE uuid = NEW.membershipUuid INTO newMembership;
|
||||
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s', NEW.membershipUuid);
|
||||
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s of coopasset', NEW.membershipUuid);
|
||||
|
||||
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.membership_AGENT(newMembership));
|
||||
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership));
|
||||
|
@ -37,14 +37,14 @@ begin
|
||||
call rbac.enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM hs_office.debitor WHERE uuid = NEW.debitorUuid INTO newDebitor;
|
||||
assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
|
||||
assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s of project', NEW.debitorUuid);
|
||||
|
||||
SELECT debitorRel.*
|
||||
FROM hs_office.relation debitorRel
|
||||
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
|
||||
WHERE debitor.uuid = NEW.debitorUuid
|
||||
INTO newDebitorRel;
|
||||
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
|
||||
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s or project', NEW.debitorUuid);
|
||||
|
||||
|
||||
perform rbac.defineRoleWithGrants(
|
||||
|
@ -46,6 +46,7 @@ public class ArchitectureTest {
|
||||
"..lambda",
|
||||
"..generated..",
|
||||
"..persistence..",
|
||||
"..reflection",
|
||||
"..system..",
|
||||
"..validation..",
|
||||
"..hs.office.bankaccount",
|
||||
@ -54,6 +55,7 @@ public class ArchitectureTest {
|
||||
"..hs.office.coopshares",
|
||||
"..hs.office.debitor",
|
||||
"..hs.office.membership",
|
||||
"..hs.office.scenarios..",
|
||||
"..hs.migration",
|
||||
"..hs.office.partner",
|
||||
"..hs.office.person",
|
||||
@ -96,7 +98,7 @@ public class ArchitectureTest {
|
||||
public static final ArchRule testClassesAreProperlyNamed = classes()
|
||||
.that().haveSimpleNameEndingWith("Test")
|
||||
.and().doNotHaveModifier(ABSTRACT)
|
||||
.should().haveNameMatching(".*(UnitTest|RestTest|IntegrationTest|AcceptanceTest|ArchitectureTest)$");
|
||||
.should().haveNameMatching(".*(UnitTest|RestTest|IntegrationTest|AcceptanceTest|ScenarioTest|ArchitectureTest)$");
|
||||
|
||||
@ArchTest
|
||||
@SuppressWarnings("unused")
|
||||
|
@ -12,7 +12,6 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@ -76,7 +75,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
class ListDebitors {
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() throws JSONException {
|
||||
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() {
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
@ -112,7 +111,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
}
|
||||
},
|
||||
"debitorNumber": 1000111,
|
||||
"debitorNumberSuffix": 11,
|
||||
"debitorNumberSuffix": "11",
|
||||
"partner": {
|
||||
"partnerNumber": 10001,
|
||||
"partnerRel": {
|
||||
@ -167,7 +166,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
}
|
||||
},
|
||||
"debitorNumber": 1000212,
|
||||
"debitorNumberSuffix": 12,
|
||||
"debitorNumberSuffix": "12",
|
||||
"partner": {
|
||||
"partnerNumber": 10002,
|
||||
"partnerRel": {
|
||||
@ -201,7 +200,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
}
|
||||
},
|
||||
"debitorNumber": 1000313,
|
||||
"debitorNumberSuffix": 13,
|
||||
"debitorNumberSuffix": "13",
|
||||
"partner": {
|
||||
"partnerNumber": 10003,
|
||||
"partnerRel": {
|
||||
@ -334,7 +333,6 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
.body("""
|
||||
{
|
||||
"debitorRel": {
|
||||
"type": "DEBITOR",
|
||||
"anchorUuid": "%s",
|
||||
"holderUuid": "%s",
|
||||
"contactUuid": "%s"
|
||||
@ -386,7 +384,6 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
.body("""
|
||||
{
|
||||
"debitorRel": {
|
||||
"type": "DEBITOR",
|
||||
"anchorUuid": "%s",
|
||||
"holderUuid": "%s",
|
||||
"contactUuid": "%s"
|
||||
@ -469,7 +466,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
}
|
||||
},
|
||||
"debitorNumber": 1000111,
|
||||
"debitorNumberSuffix": 11,
|
||||
"debitorNumberSuffix": "11",
|
||||
"partner": {
|
||||
"partnerNumber": 10001,
|
||||
"partnerRel": {
|
||||
@ -581,7 +578,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
"contact": { "caption": "fourth contact" }
|
||||
},
|
||||
"debitorNumber": 10004${debitorNumberSuffix},
|
||||
"debitorNumberSuffix": ${debitorNumberSuffix},
|
||||
"debitorNumberSuffix": "${debitorNumberSuffix}",
|
||||
"partner": {
|
||||
"partnerNumber": 10004,
|
||||
"partnerRel": {
|
||||
|
@ -0,0 +1,240 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||
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.CreateSepaMandateForDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteSepaMandateForDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddOperationsContactToPartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.DeletePartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddRepresentativeToPartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.RemoveOperationsContactFromPartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeToMailinglist;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
@Tag("scenarioTest")
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
classes = { HsadminNgApplication.class, JpaAttempt.class },
|
||||
properties = {
|
||||
"spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///scenariosTC}",
|
||||
"spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}",
|
||||
"spring.datasource.password=${HSADMINNG_POSTGRES_ADMIN_PASSWORD:password}",
|
||||
"hsadminng.superuser=${HSADMINNG_SUPERUSER:superuser-alex@hostsharing.net}"
|
||||
}
|
||||
)
|
||||
@DirtiesContext
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class HsOfficeScenarioTests extends ScenarioTest {
|
||||
|
||||
@Test
|
||||
@Order(1010)
|
||||
@Produces(explicitly = "Partner: Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Board of Directors"})
|
||||
void shouldCreatePartner() {
|
||||
new CreatePartner(this)
|
||||
.given("partnerNumber", 31010)
|
||||
.given("personType", "LEGAL_PERSON")
|
||||
.given("tradeName", "Test AG")
|
||||
.given("contactCaption", "Test AG - Board of Directors")
|
||||
.given("emailAddress", "board-of-directors@test-ag.example.org")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1020)
|
||||
@Requires("Person: Test AG")
|
||||
@Produces("Representative: Tracy Trust for Test AG")
|
||||
void shouldAddRepresentativeToPartner() {
|
||||
new AddRepresentativeToPartner(this)
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("representativeFamilyName", "Trust")
|
||||
.given("representativeGivenName", "Tracy")
|
||||
.given("representativePostalAddress", """
|
||||
An der Alster 100
|
||||
20000 Hamburg
|
||||
""")
|
||||
.given("representativePhoneNumber", "+49 40 123456")
|
||||
.given("representativeEMailAddress", "tracy.trust@example.org")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1030)
|
||||
@Requires("Person: Test AG")
|
||||
@Produces("Operations-Contact: Dennis Krause for Test AG")
|
||||
void shouldAddOperationsContactToPartner() {
|
||||
new AddOperationsContactToPartner(this)
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("operationsContactFamilyName", "Krause")
|
||||
.given("operationsContactGivenName", "Dennis")
|
||||
.given("operationsContactPhoneNumber", "+49 9932 587741")
|
||||
.given("operationsContactEMailAddress", "dennis.krause@example.org")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1039)
|
||||
@Requires("Operations-Contact: Dennis Krause for Test AG")
|
||||
void shouldRemoveOperationsContactFromPartner() {
|
||||
new RemoveOperationsContactFromPartner(this)
|
||||
.given("operationsContactPerson", "Dennis Krause")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1090)
|
||||
void shouldDeletePartner() {
|
||||
new DeletePartner(this)
|
||||
.given("partnerNumber", 31020)
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2010)
|
||||
@Requires("Partner: Test AG")
|
||||
@Produces("Debitor: Test AG - main debitor")
|
||||
void shouldCreateSelfDebitorForPartner() {
|
||||
new CreateSelfDebitorForPartner(this, "Debitor: Test AG - main debitor")
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("billingContactCaption", "Test AG - billing department")
|
||||
.given("billingContactEmailAddress", "billing@test-ag.example.org")
|
||||
.given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automatically, but is not yet
|
||||
.given("billable", true)
|
||||
.given("vatId", "VAT123456")
|
||||
.given("vatCountryCode", "DE")
|
||||
.given("vatBusiness", true)
|
||||
.given("vatReverseCharge", false)
|
||||
.given("defaultPrefix", "tst")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2011)
|
||||
@Requires("Person: Test AG")
|
||||
@Produces("Debitor: Billing GmbH")
|
||||
void shouldCreateExternalDebitorForPartner() {
|
||||
new CreateExternalDebitorForPartner(this)
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("billingContactCaption", "Billing GmbH - billing department")
|
||||
.given("billingContactEmailAddress", "billing@test-ag.example.org")
|
||||
.given("debitorNumberSuffix", "01")
|
||||
.given("billable", true)
|
||||
.given("vatId", "VAT123456")
|
||||
.given("vatCountryCode", "DE")
|
||||
.given("vatBusiness", true)
|
||||
.given("vatReverseCharge", false)
|
||||
.given("defaultPrefix", "tsx")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2020)
|
||||
@Requires("Person: Test AG")
|
||||
void shouldDeleteDebitor() {
|
||||
new DeleteDebitor(this)
|
||||
.given("partnerNumber", 31020)
|
||||
.given("debitorSuffix", "02")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2020)
|
||||
@Requires("Debitor: Test AG - main debitor")
|
||||
@Disabled("see TODO.spec in DontDeleteDefaultDebitor")
|
||||
void shouldNotDeleteDefaultDebitor() {
|
||||
new DontDeleteDefaultDebitor(this)
|
||||
.given("partnerNumber", 31020)
|
||||
.given("debitorSuffix", "00")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3100)
|
||||
@Requires("Debitor: Test AG - main debitor")
|
||||
@Produces("SEPA-Mandate: Test AG")
|
||||
void shouldCreateSepaMandateForDebitor() {
|
||||
new CreateSepaMandateForDebitor(this)
|
||||
.given("debitor", "Test AG")
|
||||
.given("memberNumberSuffix", "00")
|
||||
.given("validFrom", "2024-10-15")
|
||||
.given("membershipFeeBillable", "true")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3108)
|
||||
@Requires("SEPA-Mandate: Test AG")
|
||||
void shouldInvalidateSepaMandateForDebitor() {
|
||||
new InvalidateSepaMandateForDebitor(this)
|
||||
.given("sepaMandateUuid", "%{SEPA-Mandate: Test AG}")
|
||||
.given("validUntil", "2025-09-30")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3109)
|
||||
@Requires("SEPA-Mandate: Test AG")
|
||||
void shouldDeleteSepaMandateForDebitor() {
|
||||
new DeleteSepaMandateForDebitor(this)
|
||||
.given("sepaMandateUuid", "%{SEPA-Mandate: Test AG}")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4000)
|
||||
@Requires("Partner: Test AG")
|
||||
void shouldCreateMembershipForPartner() {
|
||||
new CreateMembership(this)
|
||||
.given("partnerName", "Test AG")
|
||||
.given("memberNumberSuffix", "00")
|
||||
.given("validFrom", "2024-10-15")
|
||||
.given("membershipFeeBillable", "true")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5000)
|
||||
@Requires("Person: Test AG")
|
||||
@Produces("Subscription: Michael Miller to operations-announce")
|
||||
void shouldSubscribeNewPersonAndContactToMailinglist() {
|
||||
new SubscribeToMailinglist(this)
|
||||
// TODO.spec: do we need the personType? or is an operational contact always a natural person? what about distribution lists?
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("subscriberFamilyName", "Miller")
|
||||
.given("subscriberGivenName", "Michael")
|
||||
.given("subscriberEMailAddress", "michael.miller@example.org")
|
||||
.given("mailingList", "operations-announce")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5001)
|
||||
@Requires("Subscription: Michael Miller to operations-announce")
|
||||
void shouldUnsubscribeNewPersonAndContactToMailinglist() {
|
||||
new UnsubscribeFromMailinglist(this)
|
||||
.given("mailingList", "operations-announce")
|
||||
.given("subscriberEMailAddress", "michael.miller@example.org")
|
||||
.doRun();
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Target(METHOD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface Produces {
|
||||
String value() default ""; // same as explicitly, makes it possible to omit the property name
|
||||
String explicitly() default ""; // same as value
|
||||
String[] implicitly() default {};
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
# UseCase-Tests
|
||||
|
||||
We define UseCase-tests as test for business-scenarios.
|
||||
They test positive (successful) scenarios by using the REST-API.
|
||||
|
||||
Running these tests also creates test-reports which can be used as documentation about the necessary REST-calls for each scenario.
|
||||
|
||||
Clarification: Acceptance tests also test at the REST-API level but are more technical and also test negative (error-) scenarios.
|
||||
|
||||
## ... extends ScenarioTest
|
||||
|
||||
Each test-method in subclasses of ScenarioTest describes a business-scenario,
|
||||
each utilizing a main-use-case and given example data for the scenario.
|
||||
|
||||
To reduce the number of API-calls, intermediate results can be re-used.
|
||||
This is controlled by two annotations:
|
||||
|
||||
### @Produces(....)
|
||||
|
||||
This annotation tells the test-runner that this scenario produces certain business object for re-use.
|
||||
The UUID of the new business objects are stored in a key-value map using the provided keys.
|
||||
|
||||
There are two variants of this annotation:
|
||||
|
||||
#### A Single Business Object
|
||||
```
|
||||
@Produces("key")
|
||||
```
|
||||
|
||||
This variant is used when there is just a single business-object produced by the use-case.
|
||||
|
||||
#### Multiple Business Objects
|
||||
|
||||
```
|
||||
@Produces(explicitly = "main-key", implicitly = {"other-key", ...})
|
||||
```
|
||||
|
||||
This variant is used when multiple business-objects are produced by the use-case,
|
||||
e.g. a Relation, a Person and a Contact.
|
||||
The UUID of the business-object produced by the main-use-case gets stored as the key after "explicitly",
|
||||
the others are listed after "implicitly";
|
||||
if there is just one, leave out the surrounding braces.
|
||||
|
||||
### @Requires(...)
|
||||
|
||||
This annotation tells the test-runner that which business objects are required before this scenario can run.
|
||||
|
||||
Each subset must be produced by the same producer-method.
|
||||
|
||||
|
||||
## ... extends UseCase
|
||||
|
||||
These classes consist of two parts:
|
||||
|
||||
### Prerequisites of the Use-Case
|
||||
|
||||
The constructor may create prerequisites via `required(...)`.
|
||||
These do not really belong to the use-case itself,
|
||||
e.g. create business objects which, in the context of that use-case, would already exist.
|
||||
|
||||
This is similar to @Requires(...) just that no other test scenario produces this prerequisite.
|
||||
Here, use-cases can be re-used, usually with different data.
|
||||
|
||||
### The Use-Case Itself
|
||||
|
||||
The use-case
|
@ -0,0 +1,13 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Target(METHOD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface Requires {
|
||||
String value();
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.apache.commons.collections4.SetUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public abstract class ScenarioTest extends ContextBasedTest {
|
||||
|
||||
final static String RUN_AS_USER = "superuser-alex@hostsharing.net"; // TODO.test: use global:AGENT when implemented
|
||||
|
||||
record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return uuid.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private final static Map<String, Alias<?>> aliases = new HashMap<>();
|
||||
private final static Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
public final TestReport testReport = new TestReport(aliases);
|
||||
|
||||
@LocalServerPort
|
||||
Integer port;
|
||||
|
||||
@Autowired
|
||||
HsOfficePersonRepository personRepo;
|
||||
|
||||
@Autowired
|
||||
JpaAttempt jpaAttempt;
|
||||
|
||||
@SneakyThrows
|
||||
@BeforeEach
|
||||
void init(final TestInfo testInfo) {
|
||||
createHostsharingPerson();
|
||||
try {
|
||||
testInfo.getTestMethod().ifPresent(this::callRequiredProducers);
|
||||
testReport.createTestLogMarkdownFile(testInfo);
|
||||
} catch (Exception exc) {
|
||||
throw exc;
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() { // final TestInfo testInfo
|
||||
properties.clear();
|
||||
// FIXME: Delete all aliases as well to force HTTP GET queries in each scenario?
|
||||
testReport.close();
|
||||
}
|
||||
|
||||
private void createHostsharingPerson() {
|
||||
jpaAttempt.transacted(() ->
|
||||
{
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
aliases.put(
|
||||
"Person: Hostsharing eG",
|
||||
new Alias<>(
|
||||
null,
|
||||
personRepo.findPersonByOptionalNameLike("Hostsharing eG")
|
||||
.stream()
|
||||
.map(HsOfficePersonEntity::getUuid)
|
||||
.reduce(Reducer::toSingleElement).orElseThrow())
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void callRequiredProducers(final Method currentTestMethod) {
|
||||
final var testMethodRequired = Optional.of(currentTestMethod)
|
||||
.map(m -> m.getAnnotation(Requires.class))
|
||||
.map(Requires::value)
|
||||
.orElse(null);
|
||||
if (testMethodRequired != null) {
|
||||
for (Method potentialProducerMethod : getClass().getDeclaredMethods()) {
|
||||
final var producesAnnot = potentialProducerMethod.getAnnotation(Produces.class);
|
||||
if (producesAnnot != null) {
|
||||
final var testMethodProduces = allOf(
|
||||
producesAnnot.value(),
|
||||
producesAnnot.explicitly(),
|
||||
producesAnnot.implicitly());
|
||||
// @formatter:off
|
||||
if ( // that method can produce something required
|
||||
testMethodProduces.contains(testMethodRequired) &&
|
||||
|
||||
// and it does not produce anything we already have (would cause errors)
|
||||
SetUtils.intersection(testMethodProduces, knowVariables().keySet()).isEmpty()
|
||||
) {
|
||||
// then we recursively produce the pre-requisites of the producer method
|
||||
callRequiredProducers(potentialProducerMethod);
|
||||
|
||||
// and finally we call the producer method
|
||||
potentialProducerMethod.invoke(this);
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> allOf(final String value, final String explicitly, final String[] implicitly) {
|
||||
final var all = new HashSet<String>();
|
||||
if (!value.isEmpty()) {
|
||||
all.add(value);
|
||||
}
|
||||
if (!explicitly.isEmpty()) {
|
||||
all.add(explicitly);
|
||||
}
|
||||
all.addAll(asList(implicitly));
|
||||
return all;
|
||||
}
|
||||
|
||||
static boolean containsAlias(final String alias) {
|
||||
return aliases.containsKey(alias);
|
||||
}
|
||||
|
||||
static UUID uuid(final String nameWithPlaceholders) {
|
||||
final var resoledName = resolve(nameWithPlaceholders);
|
||||
final UUID alias = ofNullable(knowVariables().get(resoledName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null);
|
||||
assertThat(alias).as("alias '" + resoledName + "' not found in aliases nor in properties [" +
|
||||
knowVariables().keySet().stream().map(v -> "'" + v + "'").collect(Collectors.joining(", ")) + "]"
|
||||
).isNotNull();
|
||||
return alias;
|
||||
}
|
||||
|
||||
static void putAlias(final String name, final Alias<?> value) {
|
||||
aliases.put(name, value);
|
||||
}
|
||||
|
||||
static void putProperty(final String name, final Object value) {
|
||||
properties.put(name, (value instanceof String string) ? resolveTyped(string) : value);
|
||||
}
|
||||
|
||||
static Map<String, Object> knowVariables() {
|
||||
final var map = new LinkedHashMap<String, Object>();
|
||||
ScenarioTest.aliases.forEach((key, value) -> map.put(key, value.uuid()));
|
||||
map.putAll(ScenarioTest.properties);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static String resolve(final String text) {
|
||||
final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public static Object resolveTyped(final String text) {
|
||||
final var resolved = resolve(text);
|
||||
try {
|
||||
return UUID.fromString(resolved);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// ignore and just use the String value
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class TemplateResolver {
|
||||
|
||||
private final String template;
|
||||
private final Map<String, Object> properties;
|
||||
private final StringBuilder resolved = new StringBuilder();
|
||||
private int position = 0;
|
||||
|
||||
public TemplateResolver(final String template, final Map<String, Object> properties) {
|
||||
this.template = template;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
String resolve() {
|
||||
copy();
|
||||
return resolved.toString();
|
||||
}
|
||||
|
||||
private void copy() {
|
||||
while (hasMoreChars()) {
|
||||
if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') {
|
||||
startPlaceholder(currentChar());
|
||||
} else {
|
||||
resolved.append(fetchChar());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasMoreChars() {
|
||||
return position < template.length();
|
||||
}
|
||||
|
||||
private void startPlaceholder(final char intro) {
|
||||
skipChars(intro + "{");
|
||||
int nested = 0;
|
||||
final var placeholder = new StringBuilder();
|
||||
while (nested > 0 || currentChar() != '}') {
|
||||
if (currentChar() == '}') {
|
||||
--nested;
|
||||
placeholder.append(fetchChar());
|
||||
} else if ((currentChar() == '$' || currentChar() == '%') && nextChar() == '{') {
|
||||
++nested;
|
||||
placeholder.append(fetchChar());
|
||||
} else {
|
||||
placeholder.append(fetchChar());
|
||||
}
|
||||
}
|
||||
final var name = new TemplateResolver(placeholder.toString(), properties).resolve();
|
||||
final var value = propVal(name);
|
||||
if ( intro == '%') {
|
||||
resolved.append(value);
|
||||
} else {
|
||||
resolved.append(optionallyQuoted(value));
|
||||
}
|
||||
skipChar('}');
|
||||
}
|
||||
|
||||
private Object propVal(final String name) {
|
||||
final var val = properties.get(name);
|
||||
if (val == null) {
|
||||
throw new IllegalStateException("Missing required property: " + name);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private void skipChar(final char expectedChar) {
|
||||
if (currentChar() != expectedChar) {
|
||||
throw new IllegalStateException("expected '" + expectedChar + "' but got '" + currentChar() + "'");
|
||||
}
|
||||
++position;
|
||||
}
|
||||
|
||||
private void skipChars(final String expectedChars) {
|
||||
final var nextChars = template.substring(position, position + expectedChars.length());
|
||||
if ( !nextChars.equals(expectedChars) ) {
|
||||
throw new IllegalStateException("expected '" + expectedChars + "' but got '" + nextChars + "'");
|
||||
}
|
||||
position += expectedChars.length();
|
||||
}
|
||||
|
||||
private char fetchChar() {
|
||||
if ((position+1) > template.length()) {
|
||||
throw new IllegalStateException("no more characters. resolved so far: " + resolved);
|
||||
}
|
||||
final var currentChar = currentChar();
|
||||
++position;
|
||||
return currentChar;
|
||||
}
|
||||
|
||||
private char currentChar() {
|
||||
if (position >= template.length()) {
|
||||
throw new IllegalStateException("no more characters, maybe closing bracelet missing in template: '''\n" + template + "\n'''");
|
||||
}
|
||||
return template.charAt(position);
|
||||
}
|
||||
|
||||
private char nextChar() {
|
||||
if ((position+1) >= template.length()) {
|
||||
throw new IllegalStateException("no more characters. resolved so far: " + resolved);
|
||||
}
|
||||
return template.charAt(position+1);
|
||||
}
|
||||
|
||||
private static String optionallyQuoted(final Object value) {
|
||||
return switch (value) {
|
||||
case Boolean bool -> bool.toString();
|
||||
case Number number -> number.toString();
|
||||
case String string -> "\"" + string.replace("\n", "\\n") + "\"";
|
||||
default -> "\"" + value + "\"";
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(
|
||||
new TemplateResolver("""
|
||||
etwas davor,
|
||||
|
||||
${einfacher Platzhalter},
|
||||
${verschachtelter %{Name}},
|
||||
|
||||
und nochmal ohne Quotes:
|
||||
|
||||
%{einfacher Platzhalter},
|
||||
%{verschachtelter %{Name}},
|
||||
|
||||
etwas danach.
|
||||
""",
|
||||
Map.ofEntries(
|
||||
Map.entry("Name", "placeholder"),
|
||||
Map.entry("einfacher Platzhalter", "simple placeholder"),
|
||||
Map.entry("verschachtelter placeholder", "nested placeholder")
|
||||
)).resolve());
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class TestReport {
|
||||
|
||||
private final Map<String, ?> aliases;
|
||||
private final StringBuilder markdownLog = new StringBuilder(); // records everything for debugging purposes
|
||||
|
||||
private PrintWriter markdownReport;
|
||||
private int silent; // do not print anything to test-report if >0
|
||||
|
||||
public TestReport(final Map<String, ?> aliases) {
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException {
|
||||
final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow();
|
||||
final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow();
|
||||
assertThat(new File("doc/scenarios/").isDirectory() || new File("doc/scenarios/").mkdirs()).as("mkdir doc/scenarios/").isTrue();
|
||||
markdownReport = new PrintWriter(new FileWriter("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md"));
|
||||
print("## Scenario #" + testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " +
|
||||
testMethodName.replaceAll("([a-z])([A-Z]+)", "$1 $2"));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void print(final String output) {
|
||||
|
||||
final var outputWithCommentsForUuids = appendUUIDKey(output);
|
||||
|
||||
// for tests executed due to @Requires/@Produces there is no markdownFile yet
|
||||
if (markdownReport != null && silent == 0) {
|
||||
markdownReport.print(outputWithCommentsForUuids);
|
||||
}
|
||||
|
||||
// but the debugLog should contain all output, even if silent
|
||||
markdownLog.append(outputWithCommentsForUuids);
|
||||
}
|
||||
|
||||
public void printLine(final String output) {
|
||||
print(output + "\n");
|
||||