feature/use-case-acceptance-tests (#116)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/116 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
		| @@ -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. | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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 |                     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: | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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)); | ||||||
|   | |||||||
| @@ -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)); | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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": { | ||||||
|   | |||||||
| @@ -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"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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--; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user