feature/use-case-acceptance-tests #116

Merged
hsh-michaelhoennig merged 49 commits from feature/use-case-acceptance-tests into master 2024-10-30 11:40:46 +01:00
41 changed files with 1868 additions and 49 deletions

View File

@ -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. 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-Tests
Performance-critical scenarios have to be identified and a special performance-test has to be implemented. Performance-critical scenarios have to be identified and a special performance-test has to be implemented.

View File

@ -77,16 +77,13 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both"); "ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null, Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none"); "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, Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
"ERROR: [400] debitorRel.mark must be null"); "ERROR: [400] debitorRel.mark must be null");
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class); final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
if ( body.getDebitorRel() != null ) { if (body.getDebitorRel() != null) {
body.getDebitorRel().setType(DEBITOR.name());
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class); final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
debitorRel.setType(DEBITOR);
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor()); entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder()); entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact()); entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
@ -95,7 +92,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid()); final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse( debitorRelOptional.ifPresentOrElse(
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));}, 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); final var savedEntity = debitorRepo.save(entityToSave);

View File

@ -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;
}
}

View File

@ -17,10 +17,8 @@ components:
minimum: 1000000 minimum: 1000000
maximum: 9999999 maximum: 9999999
debitorNumberSuffix: debitorNumberSuffix:
type: integer type: string
format: int8 pattern: '^[0-9][0-9]$'
minimum: 00
maximum: 99
partner: partner:
$ref: 'hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner' $ref: 'hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
billable: billable:
@ -76,15 +74,13 @@ components:
type: object type: object
properties: properties:
debitorRel: debitorRel:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert' $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationSubInsert'
debitorRelUuid: debitorRelUuid:
type: string type: string
format: uuid format: uuid
debitorNumberSuffix: debitorNumberSuffix:
type: integer type: string
format: int8 pattern: '^[0-9][0-9]$'
minimum: 00
maximum: 99
billable: billable:
type: boolean type: boolean
vatId: vatId:

View File

@ -41,6 +41,7 @@ components:
format: uuid format: uuid
nullable: true nullable: true
# arbitrary relation with explicit type
HsOfficeRelationInsert: HsOfficeRelationInsert:
type: object type: object
properties: properties:
@ -64,3 +65,24 @@ components:
- holderUuid - holderUuid
- type - type
- contactUuid - 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

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.customer WHERE uuid = NEW.customerUuid INTO newCustomer; 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( perform rbac.defineRoleWithGrants(
@ -102,10 +102,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.customer WHERE uuid = OLD.customerUuid INTO oldCustomer; 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; 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 if NEW.customerUuid <> OLD.customerUuid then

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.package WHERE uuid = NEW.packageUuid INTO newPackage; 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( perform rbac.defineRoleWithGrants(
@ -98,10 +98,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.package WHERE uuid = OLD.packageUuid INTO oldPackage; 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; 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 if NEW.packageUuid <> OLD.packageUuid then

View File

@ -38,13 +38,13 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.person WHERE uuid = NEW.holderUuid INTO newHolderPerson; 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; 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; 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( perform rbac.defineRoleWithGrants(

View File

@ -37,10 +37,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel; 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; 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, 'DELETE'), hs_office.relation_OWNER(newPartnerRel));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.relation_TENANT(newPartnerRel)); call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.relation_TENANT(newPartnerRel));
@ -96,16 +96,16 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.relation WHERE uuid = OLD.partnerRelUuid INTO oldPartnerRel; 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; 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; 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; 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 if NEW.partnerRelUuid <> OLD.partnerRelUuid then

View File

@ -44,10 +44,10 @@ begin
WHERE partnerRel.type = 'PARTNER' WHERE partnerRel.type = 'PARTNER'
AND NEW.debitorRelUuid = debitorRel.uuid AND NEW.debitorRelUuid = debitorRel.uuid
INTO newPartnerRel; 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; 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; SELECT * FROM hs_office.bankaccount WHERE uuid = NEW.refundBankAccountUuid INTO newRefundBankAccount;

View File

@ -37,14 +37,14 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.bankaccount WHERE uuid = NEW.bankAccountUuid INTO newBankAccount; 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.* SELECT debitorRel.*
FROM hs_office.relation debitorRel FROM hs_office.relation debitorRel
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid WHERE debitor.uuid = NEW.debitorUuid
INTO newDebitorRel; 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( perform rbac.defineRoleWithGrants(

View File

@ -40,7 +40,7 @@ begin
JOIN hs_office.relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid JOIN hs_office.relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
WHERE partner.uuid = NEW.partnerUuid WHERE partner.uuid = NEW.partnerUuid
INTO newPartnerRel; 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( perform rbac.defineRoleWithGrants(

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.membership WHERE uuid = NEW.membershipUuid INTO newMembership; 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, 'SELECT'), hs_office.membership_AGENT(newMembership));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership)); call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership));

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.membership WHERE uuid = NEW.membershipUuid INTO newMembership; 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, 'SELECT'), hs_office.membership_AGENT(newMembership));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership)); call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership));

View File

@ -37,14 +37,14 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid); call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.debitor WHERE uuid = NEW.debitorUuid INTO newDebitor; 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.* SELECT debitorRel.*
FROM hs_office.relation debitorRel FROM hs_office.relation debitorRel
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid WHERE debitor.uuid = NEW.debitorUuid
INTO newDebitorRel; 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( perform rbac.defineRoleWithGrants(

View File

@ -46,6 +46,7 @@ public class ArchitectureTest {
"..lambda", "..lambda",
"..generated..", "..generated..",
"..persistence..", "..persistence..",
"..reflection",
"..system..", "..system..",
"..validation..", "..validation..",
"..hs.office.bankaccount", "..hs.office.bankaccount",
@ -54,6 +55,7 @@ public class ArchitectureTest {
"..hs.office.coopshares", "..hs.office.coopshares",
"..hs.office.debitor", "..hs.office.debitor",
"..hs.office.membership", "..hs.office.membership",
"..hs.office.scenarios..",
"..hs.migration", "..hs.migration",
"..hs.office.partner", "..hs.office.partner",
"..hs.office.person", "..hs.office.person",
@ -96,7 +98,7 @@ public class ArchitectureTest {
public static final ArchRule testClassesAreProperlyNamed = classes() public static final ArchRule testClassesAreProperlyNamed = classes()
.that().haveSimpleNameEndingWith("Test") .that().haveSimpleNameEndingWith("Test")
.and().doNotHaveModifier(ABSTRACT) .and().doNotHaveModifier(ABSTRACT)
.should().haveNameMatching(".*(UnitTest|RestTest|IntegrationTest|AcceptanceTest|ArchitectureTest)$"); .should().haveNameMatching(".*(UnitTest|RestTest|IntegrationTest|AcceptanceTest|ScenarioTest|ArchitectureTest)$");
@ArchTest @ArchTest
@SuppressWarnings("unused") @SuppressWarnings("unused")

View File

@ -12,7 +12,6 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.json.JSONException;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
@ -76,7 +75,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
class ListDebitors { class ListDebitors {
@Test @Test
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() throws JSONException { void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() {
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -112,7 +111,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
} }
}, },
"debitorNumber": 1000111, "debitorNumber": 1000111,
"debitorNumberSuffix": 11, "debitorNumberSuffix": "11",
"partner": { "partner": {
"partnerNumber": 10001, "partnerNumber": 10001,
"partnerRel": { "partnerRel": {
@ -167,7 +166,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
} }
}, },
"debitorNumber": 1000212, "debitorNumber": 1000212,
"debitorNumberSuffix": 12, "debitorNumberSuffix": "12",
"partner": { "partner": {
"partnerNumber": 10002, "partnerNumber": 10002,
"partnerRel": { "partnerRel": {
@ -201,7 +200,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
} }
}, },
"debitorNumber": 1000313, "debitorNumber": 1000313,
"debitorNumberSuffix": 13, "debitorNumberSuffix": "13",
"partner": { "partner": {
"partnerNumber": 10003, "partnerNumber": 10003,
"partnerRel": { "partnerRel": {
@ -334,7 +333,6 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body(""" .body("""
{ {
"debitorRel": { "debitorRel": {
"type": "DEBITOR",
"anchorUuid": "%s", "anchorUuid": "%s",
"holderUuid": "%s", "holderUuid": "%s",
"contactUuid": "%s" "contactUuid": "%s"
@ -386,7 +384,6 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body(""" .body("""
{ {
"debitorRel": { "debitorRel": {
"type": "DEBITOR",
"anchorUuid": "%s", "anchorUuid": "%s",
"holderUuid": "%s", "holderUuid": "%s",
"contactUuid": "%s" "contactUuid": "%s"
@ -469,7 +466,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
} }
}, },
"debitorNumber": 1000111, "debitorNumber": 1000111,
"debitorNumberSuffix": 11, "debitorNumberSuffix": "11",
"partner": { "partner": {
"partnerNumber": 10001, "partnerNumber": 10001,
"partnerRel": { "partnerRel": {
@ -581,7 +578,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"contact": { "caption": "fourth contact" } "contact": { "caption": "fourth contact" }
}, },
"debitorNumber": 10004${debitorNumberSuffix}, "debitorNumber": 10004${debitorNumberSuffix},
"debitorNumberSuffix": ${debitorNumberSuffix}, "debitorNumberSuffix": "${debitorNumberSuffix}",
"partner": { "partner": {
"partnerNumber": 10004, "partnerNumber": 10004,
"partnerRel": { "partnerRel": {

View File

@ -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();
}
}

View File

@ -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 {};
}

View File

@ -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

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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");
}
public void printPara(final String output) {
printLine("\n" +output + "\n");
}
public void close() {
markdownReport.close();
}
private static Object orderNumber(final Method method) {
return method.getAnnotation(Order.class).value();
}
private String appendUUIDKey(String multilineText) {
final var lines = multilineText.split("\\r?\\n");
final var result = new StringBuilder();
for (String line : lines) {
for (Map.Entry<String, ?> entry : aliases.entrySet()) {
final var uuidString = entry.getValue().toString();
if (line.contains(uuidString)) {
line = line + " // " + entry.getKey();
break; // only add comment for one UUID per row (in our case, there is only one per row)
}
}
result.append(line).append("\n");
}
return result.toString();
}
void silent(final Runnable code) {
silent++;
code.run();
silent--;
}
}

View File

@ -0,0 +1,319 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import io.restassured.http.ContentType;
import lombok.Getter;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.reflection.AnnotationFinder;
import org.apache.commons.collections4.map.LinkedMap;
import org.hibernate.AssertionFailure;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import static java.net.URLEncoder.encode;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.commons.util.StringUtils.isBlank;
import static org.junit.platform.commons.util.StringUtils.isNotBlank;
public abstract class UseCase<T extends UseCase<?>> {
private static final HttpClient client = HttpClient.newHttpClient();
private final ObjectMapper objectMapper = new ObjectMapper();
protected final ScenarioTest testSuite;
private final TestReport testReport;
private final Map<String, Function<String, UseCase<?>>> requirements = new LinkedMap<>();
private final String resultAlias;
private final Map<String, Object> givenProperties = new LinkedHashMap<>();
private String nextTitle; // just temporary to override resultAlias for sub-use-cases
public UseCase(final ScenarioTest testSuite) {
this(testSuite, getResultAliasFromProducesAnnotationInCallStack());
}
public UseCase(final ScenarioTest testSuite, final String resultAlias) {
this.testSuite = testSuite;
this.testReport = testSuite.testReport;
this.resultAlias = resultAlias;
if (resultAlias != null) {
testReport.printPara("### UseCase " + title(resultAlias));
}
}
public final void requires(final String alias, final Function<String, UseCase<?>> useCaseFactory) {
if (!ScenarioTest.containsAlias(alias)) {
requirements.put(alias, useCaseFactory);
}
}
public final HttpResponse doRun() {
testReport.printPara("### Given Properties");
testReport.printLine("""
| name | value |
|------|-------|""");
givenProperties.forEach((key, value) ->
testReport.printLine("| " + key + " | " + value.toString().replace("\n", "<br>") + " |"));
testReport.printLine("");
testReport.silent(() ->
requirements.forEach((alias, factory) -> {
if (!ScenarioTest.containsAlias(alias)) {
factory.apply(alias).run().keep();
}
})
);
return run();
}
protected abstract HttpResponse run();
public final UseCase<T> given(final String propName, final Object propValue) {
givenProperties.put(propName, propValue);
ScenarioTest.putProperty(propName, propValue);
return this;
}
public final JsonTemplate usingJsonBody(final String jsonTemplate) {
return new JsonTemplate(jsonTemplate);
}
public final void obtain(
final String alias,
final Supplier<HttpResponse> http,
final Function<HttpResponse, String> extractor,
final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> {
http.get().keep(extractor);
Arrays.stream(extraInfo).forEach(testReport::printPara);
});
}
public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> {
http.get().keep();
Arrays.stream(extraInfo).forEach(testReport::printPara);
});
}
private void withTitle(final String title, final Runnable code) {
this.nextTitle = title;
code.run();
this.nextTitle = null;
}
@SneakyThrows
public final HttpResponse httpGet(final String uriPath) {
final var request = HttpRequest.newBuilder()
.GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(Duration.ofSeconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.GET, uriPath, null, response);
}
@SneakyThrows
public final HttpResponse httpPost(final String uriPath, final JsonTemplate bodyJsonTemplate) {
final var requestBody = bodyJsonTemplate.resolvePlaceholders();
final var request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString(requestBody))
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(Duration.ofSeconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.POST, uriPath, requestBody, response);
}
@SneakyThrows
public final HttpResponse httpPatch(final String uriPath, final JsonTemplate bodyJsonTemplate) {
final var requestBody = bodyJsonTemplate.resolvePlaceholders();
final var request = HttpRequest.newBuilder()
.method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody))
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(Duration.ofSeconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.PATCH, uriPath, requestBody, response);
}
@SneakyThrows
public final HttpResponse httpDelete(final String uriPath) {
final var request = HttpRequest.newBuilder()
.DELETE()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(Duration.ofSeconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.DELETE, uriPath, null, response);
}
public final UUID uuid(final String alias) {
return ScenarioTest.uuid(alias);
}
public String uriEncoded(final String text) {
return encode(ScenarioTest.resolve(text));
}
public static class JsonTemplate {
private final String template;
private JsonTemplate(final String jsonTemplate) {
this.template = jsonTemplate;
}
String resolvePlaceholders() {
return ScenarioTest.resolve(template);
}
}
public class HttpResponse {
@Getter
private final java.net.http.HttpResponse<String> response;
@Getter
private final HttpStatus status;
private UUID locationUuid;
@SneakyThrows
public HttpResponse(
final HttpMethod httpMethod,
final String uri,
final String requestBody,
final java.net.http.HttpResponse<String> response
) {
this.response = response;
this.status = HttpStatus.valueOf(response.statusCode());
if (this.status == HttpStatus.CREATED) {
final var location = response.headers().firstValue("Location").orElseThrow();
assertThat(location).startsWith("http://localhost:");
locationUuid = UUID.fromString(location.substring(location.lastIndexOf('/') + 1));
}
reportRequestAndResponse(httpMethod, uri, requestBody);
}
public HttpResponse expecting(final HttpStatus httpStatus) {
assertThat(HttpStatus.valueOf(response.statusCode())).isEqualTo(httpStatus);
return this;
}
public HttpResponse expecting(final ContentType contentType) {
assertThat(response.headers().firstValue("content-type"))
.contains(contentType.toString());
return this;
}
public void keep(final Function<HttpResponse, String> extractor) {
final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
final var value = extractor.apply(this);
ScenarioTest.putAlias(
alias,
new ScenarioTest.Alias<>(UseCase.this.getClass(), UUID.fromString(value)));
}
public void keep() {
final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
ScenarioTest.putAlias(
alias,
new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
}
@SneakyThrows
public HttpResponse expectArrayElements(final int expectedElementCount) {
final var rootNode = objectMapper.readTree(response.body());
assertThat(rootNode.isArray()).as("array expected, but got: " + response.body()).isTrue();
final var root = (List<?>) objectMapper.readValue(response.body(), new TypeReference<List<Object>>() {
});
assertThat(root.size()).as("unexpected number of array elements").isEqualTo(expectedElementCount);
return this;
}
@SneakyThrows
public String getFromBody(final String path) {
return JsonPath.parse(response.body()).read(path);
}
@SneakyThrows
private void reportRequestAndResponse(final HttpMethod httpMethod, final String uri, final String requestBody) {
// the title
if (nextTitle != null) {
testReport.printLine("\n### " + nextTitle + "\n");
} else if (resultAlias != null) {
testReport.printLine("\n### " + resultAlias + "\n");
}
// the request
testReport.printLine("```");
testReport.printLine(httpMethod.name() + " " + uri);
testReport.printLine((requestBody != null ? requestBody.trim() : ""));
// the response
testReport.printLine("=> status: " + status + " " + (locationUuid != null ? locationUuid : ""));
if (httpMethod == HttpMethod.GET || status.isError()) {
final var jsonNode = objectMapper.readTree(response.body());
final var prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
testReport.printLine(prettyJson);
}
testReport.printLine("```");
testReport.printLine("");
}
}
protected T self() {
//noinspection unchecked
return (T) this;
}
private static @Nullable String getResultAliasFromProducesAnnotationInCallStack() {
return AnnotationFinder.findCallerAnnotation(Produces.class, Test.class)
.map(produces -> oneOf(produces.value(), produces.explicitly()))
.orElse(null);
}
private static String oneOf(final String one, final String another) {
if (isNotBlank(one) && isBlank(another)) {
return one;
} else if (isBlank(one) && isNotBlank(another)) {
return another;
}
throw new AssertionFailure("exactly one value required, but got '" + one + "' and '" + another + "'");
}
private String title(String resultAlias) {
return getClass().getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1 $2") + " => " + resultAlias;
}
}

View File

@ -0,0 +1,74 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.person.CreatePerson;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
public class CreateExternalDebitorForPartner extends UseCase<CreateExternalDebitorForPartner> {
public CreateExternalDebitorForPartner(final ScenarioTest testSuite) {
super(testSuite);
requires("Person: Billing GmbH", alias -> new CreatePerson(testSuite, alias)
.given("personType", "LEGAL_PERSON")
.given("tradeName", "Billing GmbH")
);
}
@Override
protected HttpResponse run() {
obtain("Person: %{partnerPersonTradeName}", () ->
httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}"))
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
obtain("BankAccount: Billing GmbH - refund bank account", () ->
httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
{
"holder": "Billing GmbH - refund bank account",
"iban": "DE02120300000000202051",
"bic": "BYLADEM1001"
}
"""))
.expecting(CREATED).expecting(JSON)
);
obtain("Contact: Billing GmbH - Test AG billing", () ->
httpPost("/api/hs/office/contacts", usingJsonBody("""
{
"caption": "Billing GmbH, billing for Test AG",
"emailAddresses": {
"main": "test-ag@billing-GmbH.example.com"
}
}
"""))
.expecting(CREATED).expecting(JSON)
);
return httpPost("/api/hs/office/debitors", usingJsonBody("""
{
"debitorRel": {
"anchorUuid": ${Person: %{partnerPersonTradeName}},
"holderUuid": ${Person: Billing GmbH},
"contactUuid": ${Contact: Billing GmbH - Test AG billing}
},
"debitorNumberSuffix": ${debitorNumberSuffix},
"billable": ${billable},
"vatId": ${vatId},
"vatCountryCode": ${vatCountryCode},
"vatBusiness": ${vatBusiness},
"vatReverseCharge": ${vatReverseCharge},
"refundBankAccountUuid": ${BankAccount: Billing GmbH - refund bank account},
"defaultPrefix": ${defaultPrefix}
}
"""))
.expecting(CREATED).expecting(JSON);
}
}

View File

@ -0,0 +1,69 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPartner> {
public CreateSelfDebitorForPartner(final ScenarioTest testSuite, final String resultAlias) {
super(testSuite, resultAlias);
}
@Override
protected HttpResponse run() {
obtain("partnerPersonUuid", () ->
httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerPersonTradeName}"))
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].holder.uuid"),
"In production data this query could result in multiple outputs. In that case, you have to find out which is the right one.",
"**HINT**: With production data, you might get multiple results and have to decide which is the right one."
);
obtain("BankAccount: Test AG - refund bank account", () ->
httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
{
"holder": "Test AG - refund bank account",
"iban": "DE88100900001234567892",
"bic": "BEVODEBB"
}
"""))
.expecting(CREATED).expecting(JSON)
);
obtain("Contact: Test AG - billing department", () ->
httpPost("/api/hs/office/contacts", usingJsonBody("""
{
"caption": ${billingContactCaption},
"emailAddresses": {
"main": ${billingContactEmailAddress}
}
}
"""))
.expecting(CREATED).expecting(JSON)
);
return httpPost("/api/hs/office/debitors", usingJsonBody("""
{
"debitorRel": {
"anchorUuid": ${partnerPersonUuid},
"holderUuid": ${partnerPersonUuid},
"contactUuid": ${Contact: Test AG - billing department}
},
"debitorNumberSuffix": ${debitorNumberSuffix},
"billable": ${billable},
"vatId": ${vatId},
"vatCountryCode": ${vatCountryCode},
"vatBusiness": ${vatBusiness},
"vatReverseCharge": ${vatReverseCharge},
"refundBankAccountUuid": ${BankAccount: Test AG - refund bank account},
"defaultPrefix": ${defaultPrefix}
}
"""))
.expecting(CREATED).expecting(JSON);
}
}

View File

@ -0,0 +1,39 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED;
public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDebitor> {
public CreateSepaMandateForDebitor(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("BankAccount: Test AG - debit bank account", () ->
httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
{
"holder": "Test AG - debit bank account",
"iban": "DE02701500000000594937",
"bic": "SSKMDEMM"
}
"""))
.expecting(CREATED).expecting(JSON)
);
return httpPost("/api/hs/office/sepamandates", usingJsonBody("""
{
"debitorUuid": ${Debitor: Test AG - main debitor},
"bankAccountUuid": ${BankAccount: Test AG - debit bank account},
"reference": "Test AG - main debitor",
"agreement": "2022-10-12",
"validFrom": "2022-10-13"
}
"""))
.expecting(CREATED).expecting(JSON);
}
}

View File

@ -0,0 +1,31 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus;
public class DeleteDebitor extends UseCase<DeleteDebitor> {
public DeleteDebitor(final ScenarioTest testSuite) {
super(testSuite);
requires("Debitor: Test AG - delete debitor", alias -> new CreateSelfDebitorForPartner(testSuite, alias)
.given("partnerPersonTradeName", "Test AG")
.given("billingContactCaption", "Test AG - billing department")
.given("billingContactEmailAddress", "billing@test-ag.example.org")
.given("debitorNumberSuffix", "%{debitorSuffix}")
.given("billable", true)
.given("vatId", "VAT123456")
.given("vatCountryCode", "DE")
.given("vatBusiness", true)
.given("vatReverseCharge", false)
.given("defaultPrefix", "tsy"));
}
@Override
protected HttpResponse run() {
httpDelete("/api/hs/office/debitors/" + uuid("Debitor: Test AG - delete debitor"))
.expecting(HttpStatus.NO_CONTENT);
return null;
}
}

View File

@ -0,0 +1,20 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus;
public class DeleteSepaMandateForDebitor extends UseCase<DeleteSepaMandateForDebitor> {
public DeleteSepaMandateForDebitor(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
httpDelete("/api/hs/office/sepamandates/" + uuid("SEPA-Mandate: Test AG"))
.expecting(HttpStatus.NO_CONTENT);
return null;
}
}

View File

@ -0,0 +1,20 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import org.springframework.http.HttpStatus;
public class DontDeleteDefaultDebitor extends UseCase<DontDeleteDefaultDebitor> {
public DontDeleteDefaultDebitor(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
httpDelete("/api/hs/office/debitors/" + uuid("Debitor: Test AG - main debitor"))
// TODO.spec: should be CONFLICT or CLIENT_ERROR for Debitor "00" - but how to delete Partners?
.expecting(HttpStatus.NO_CONTENT);
return null;
}
}

View File

@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK;
public class InvalidateSepaMandateForDebitor extends UseCase<InvalidateSepaMandateForDebitor> {
public InvalidateSepaMandateForDebitor(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
return httpPatch("/api/hs/office/sepamandates/" + uuid("SEPA-Mandate: Test AG"), usingJsonBody("""
{
"validUntil": ${validUntil}
}
"""))
.expecting(OK).expecting(JSON);
}
}

View File

@ -0,0 +1,29 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus;
public class CreateMembership extends UseCase<CreateMembership> {
public CreateMembership(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("Membership: %{partnerName} 00", () ->
httpPost("/api/hs/office/memberships", usingJsonBody("""
{
"partnerUuid": ${Partner: Test AG},
"memberNumberSuffix": ${memberNumberSuffix},
"validFrom": ${validFrom},
"membershipFeeBillable": ${membershipFeeBillable}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
return null;
}
}

View File

@ -0,0 +1,67 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
public class AddOperationsContactToPartner extends UseCase<AddOperationsContactToPartner> {
public AddOperationsContactToPartner(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("Person: %{partnerPersonTradeName}", () ->
httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}"))
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
obtain("Person: %{operationsContactGivenName} %{operationsContactFamilyName}", () ->
httpPost("/api/hs/office/persons", usingJsonBody("""
{
"personType": "NATURAL_PERSON",
"familyName": ${operationsContactFamilyName},
"givenName": ${operationsContactGivenName}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON),
"Please check first if that person already exists, if so, use it's UUID below.",
"**HINT**: operations contacts are always connected to a partner-person, thus a person which is a holder of a partner-relation."
);
obtain("Contact: %{operationsContactGivenName} %{operationsContactFamilyName}", () ->
httpPost("/api/hs/office/contacts", usingJsonBody("""
{
"caption": "%{operationsContactGivenName} %{operationsContactFamilyName}",
"phoneNumbers": {
"main": ${operationsContactPhoneNumber}
},
"emailAddresses": {
"main": ${operationsContactEMailAddress}
}
}
"""))
.expecting(CREATED).expecting(JSON),
"Please check first if that contact already exists, if so, use it's UUID below."
);
return httpPost("/api/hs/office/relations", usingJsonBody("""
{
"type": "OPERATIONS",
"anchorUuid": ${Person: %{partnerPersonTradeName}},
"holderUuid": ${Person: %{operationsContactGivenName} %{operationsContactFamilyName}},
"contactUuid": ${Contact: %{operationsContactGivenName} %{operationsContactFamilyName}}
}
"""))
.expecting(CREATED).expecting(JSON);
}
}

View File

@ -0,0 +1,68 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
public class AddRepresentativeToPartner extends UseCase<AddRepresentativeToPartner> {
public AddRepresentativeToPartner(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("Person: %{partnerPersonTradeName}", () ->
httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}"))
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
obtain("Person: %{representativeGivenName} %{representativeFamilyName}", () ->
httpPost("/api/hs/office/persons", usingJsonBody("""
{
"personType": "NATURAL_PERSON",
"familyName": ${representativeFamilyName},
"givenName": ${representativeGivenName}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON),
"Please check first if that person already exists, if so, use it's UUID below.",
"**HINT**: A representative is always a natural person and represents a non-natural-person."
);
obtain("Contact: %{representativeGivenName} %{representativeFamilyName}", () ->
httpPost("/api/hs/office/contacts", usingJsonBody("""
{
"caption": "%{representativeGivenName} %{representativeFamilyName}",
"postalAddress": ${representativePostalAddress},
"phoneNumbers": {
"main": ${representativePhoneNumber}
},
"emailAddresses": {
"main": ${representativeEMailAddress}
}
}
"""))
.expecting(CREATED).expecting(JSON),
"Please check first if that contact already exists, if so, use it's UUID below."
);
return httpPost("/api/hs/office/relations", usingJsonBody("""
{
"type": "REPRESENTATIVE",
"anchorUuid": ${Person: %{partnerPersonTradeName}},
"holderUuid": ${Person: %{representativeGivenName} %{representativeFamilyName}},
"contactUuid": ${Contact: %{representativeGivenName} %{representativeFamilyName}}
}
"""))
.expecting(CREATED).expecting(JSON);
}
}

View File

@ -0,0 +1,69 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK;
public class CreatePartner extends UseCase<CreatePartner> {
public CreatePartner(final ScenarioTest testSuite, final String resultAlias) {
super(testSuite, resultAlias);
}
public CreatePartner(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("Person: %{tradeName}", () ->
httpPost("/api/hs/office/persons", usingJsonBody("""
{
"personType": ${personType},
"tradeName": ${tradeName}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
obtain("Contact: %{tradeName} - Board of Directors", () ->
httpPost("/api/hs/office/contacts", usingJsonBody("""
{
"caption": ${contactCaption},
"emailAddresses": {
"main": ${emailAddress}
}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
return httpPost("/api/hs/office/partners", usingJsonBody("""
{
"partnerNumber": ${partnerNumber},
"partnerRel": {
"anchorUuid": ${Person: Hostsharing eG},
"holderUuid": ${Person: %{tradeName}},
"contactUuid": ${Contact: %{tradeName} - Board of Directors}
},
"details": {
"registrationOffice": "Registergericht Hamburg",
"registrationNumber": "1234567"
}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON);
}
}

View File

@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus;
public class DeletePartner extends UseCase<DeletePartner> {
public DeletePartner(final ScenarioTest testSuite) {
super(testSuite);
requires("Partner: Delete AG", alias -> new CreatePartner(testSuite, alias)
.given("personType", "LEGAL_PERSON")
.given("tradeName", "Delete AG")
.given("contactCaption", "Delete AG - Board of Directors")
.given("emailAddress", "board-of-directors@delete-ag.example.org"));
}
@Override
protected HttpResponse run() {
httpDelete("/api/hs/office/partners/" + uuid("Partner: Delete AG"))
.expecting(HttpStatus.NO_CONTENT);
return null;
}
}

View File

@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.hs.office.scenarios.person;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus;
public class CreatePerson extends UseCase<CreatePerson> {
public CreatePerson(final ScenarioTest testSuite, final String resultAlias) {
super(testSuite, resultAlias);
}
@Override
protected HttpResponse run() {
return httpPost("/api/hs/office/persons", usingJsonBody("""
{
"personType": ${personType},
"tradeName": ${tradeName}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON);
}
}

View File

@ -0,0 +1,29 @@
package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import static org.springframework.http.HttpStatus.OK;
public class RemoveOperationsContactFromPartner extends UseCase<RemoveOperationsContactFromPartner> {
public RemoveOperationsContactFromPartner(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("Operations-Contact: %{operationsContactPerson}", () ->
httpGet("/api/hs/office/relations?relationType=OPERATIONS&name=" + uriEncoded("%{operationsContactPerson}"))
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
return httpDelete("/api/hs/office/relations/" + uuid("Operations-Contact: %{operationsContactPerson}"))
.expecting(NO_CONTENT);
}
}

View File

@ -0,0 +1,62 @@
package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
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 SubscribeToMailinglist extends UseCase<SubscribeToMailinglist> {
public SubscribeToMailinglist(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("Person: %{partnerPersonTradeName}", () ->
httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}"))
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
obtain("Person: %{subscriberGivenName} %{subscriberFamilyName}", () ->
httpPost("/api/hs/office/persons", usingJsonBody("""
{
"personType": "NATURAL_PERSON",
"familyName": ${subscriberFamilyName},
"givenName": ${subscriberGivenName}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
obtain("Contact: %{subscriberGivenName} %{subscriberFamilyName}", () ->
httpPost("/api/hs/office/contacts", usingJsonBody("""
{
"caption": "%{subscriberGivenName} %{subscriberFamilyName}",
"emailAddresses": {
"main": ${subscriberEMailAddress}
}
}
"""))
.expecting(CREATED).expecting(JSON)
);
return httpPost("/api/hs/office/relations", usingJsonBody("""
{
"type": "SUBSCRIBER",
"mark": ${mailingList},
"anchorUuid": ${Person: %{partnerPersonTradeName}},
"holderUuid": ${Person: %{subscriberGivenName} %{subscriberFamilyName}},
"contactUuid": ${Contact: %{subscriberGivenName} %{subscriberFamilyName}}
}
"""))
.expecting(CREATED).expecting(JSON);
}
}

View File

@ -0,0 +1,31 @@
package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import static org.springframework.http.HttpStatus.OK;
public class UnsubscribeFromMailinglist extends UseCase<UnsubscribeFromMailinglist> {
public UnsubscribeFromMailinglist(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("Subscription: %{subscriberEMailAddress}", () ->
httpGet("/api/hs/office/relations?relationType=SUBSCRIBER" +
"&mark=" + uriEncoded("%{mailingList}") +
"&contactData=" + uriEncoded("%{subscriberEMailAddress}"))
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
return httpDelete("/api/hs/office/relations/" + uuid("Subscription: %{subscriberEMailAddress}"))
.expecting(NO_CONTENT);
}
}