diff --git a/Jenkinsfile b/Jenkinsfile index fc29e5c3..9b569801 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -76,7 +76,10 @@ pipeline { sh ''' ./gradlew convertMarkdownToHtml ''' - archiveArtifacts artifacts: 'doc/scenarios/*.html', allowEmptyArchive: true + archiveArtifacts artifacts: + 'build/doc/scenarios/*.html, ' + + 'build/reports/dependency-license/dependencies-without-allowed-license.json', + allowEmptyArchive: true // cleanup workspace cleanWs() diff --git a/README.md b/README.md index ed922084..6be7fbe3 100644 --- a/README.md +++ b/README.md @@ -60,36 +60,40 @@ If you have at least Docker and the Java JDK installed in appropriate versions a cd your-hsadmin-ng-directory - source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew' - gw # initially downloads the configured Gradle version into the project + source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew' + gw # initially downloads the configured Gradle version into the project - gw test # compiles and runs unit- and integration-tests + gw test # compiles and runs unit- and integration-tests - takes >10min even on a fast machine + gw scenarioTests # compiles and scenario-tests - takes ~1min on a decent machine # if the container has not been built yet, run this: pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432 - # if the container has been built already, run this: + # if the container has been built already and you want to keep the data, run this: pg-sql-start gw bootRun # compiles and runs the application on localhost:8080 # the following command should reply with "pong": - curl http://localhost:8080/api/ping + curl -f http://localhost:8080/api/ping # the following command should return a JSON array with just all customers: - curl \ + curl -f\ -H 'current-subject: superuser-alex@hostsharing.net' \ - http://localhost:8080/api/test/customers + http://localhost:8080/api/test/customers \ + | jq # just if `jq` is installed, to prettyprint the output # the following command should return a JSON array with just all packages visible for the admin of the customer yyy: - curl \ + curl -f\ -H 'current-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \ - http://localhost:8080/api/test/packages + http://localhost:8080/api/test/packages \ + | jq # add a new customer - curl \ + curl -f\ -H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \ -d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \ - -X POST http://localhost:8080/api/test/customers + -X POST http://localhost:8080/api/test/customers \ + | jq If you wonder who 'superuser-alex@hostsharing.net' and 'superuser-fran@hostsharing.net' are and where the data comes from: Mike and Sven are just example global admin accounts as part of the example data which is automatically inserted in Testcontainers and Development environments. diff --git a/build.gradle b/build.gradle index fc287915..b409fc37 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2' implementation 'org.springdoc:springdoc-openapi:2.6.0' implementation 'org.postgresql:postgresql:42.7.4' diff --git a/etc/allowed-licenses.json b/etc/allowed-licenses.json index 65aa236e..ff50a78f 100644 --- a/etc/allowed-licenses.json +++ b/etc/allowed-licenses.json @@ -43,6 +43,11 @@ { "moduleLicense": "WTFPL" }, + { + "moduleLicense": "Public Domain, per Creative Commons CC0", + "moduleVersion": "2.0.3" + }, + { "moduleLicense": null, "#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE", diff --git a/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java new file mode 100644 index 00000000..3585dd8c --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java @@ -0,0 +1,26 @@ +package net.hostsharing.hsadminng.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + + @Bean + @Profile("!test") + public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { + return http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/api/**").permitAll() // TODO.impl: implement authentication + .requestMatchers("/actuator/**").permitAll() + .anyRequest().authenticated() + ) + .build(); + } + +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index 24a1919c..8288d7c1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -27,11 +27,13 @@ import java.util.UUID; import java.util.function.BiConsumer; import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.REVERSAL; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.TRANSFER; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.CLEARING; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DEPOSIT; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DISBURSAL; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.LOSS; -import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.TRANSFER; +import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull; @RestController public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { @@ -66,7 +68,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse fromValueDate, toValueDate); - final var resources = mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + final var resources = mapper.mapList( + entities, + HsOfficeCoopAssetsTransactionResource.class, + ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(resources); } @@ -106,7 +111,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse if (result.isEmpty()) { return ResponseEntity.notFound().build(); } - return ResponseEntity.ok(mapper.map(result.get(), HsOfficeCoopAssetsTransactionResource.class)); + final var resource = mapper.map( + result.get(), + HsOfficeCoopAssetsTransactionResource.class, + ENTITY_TO_RESOURCE_POSTMAPPER); + return ResponseEntity.ok(resource); } @@ -131,7 +140,8 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse private static void validateCreditTransaction( final HsOfficeCoopAssetsTransactionInsertResource requestBody, final ArrayList violations) { - if (List.of(DISBURSAL, TRANSFER, CLEARING, LOSS).contains(requestBody.getTransactionType()) + if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS) + .contains(requestBody.getTransactionType()) && requestBody.getAssetValue().signum() > 0) { violations.add("for %s, assetValue must be negative but is \"%.2f\"".formatted( requestBody.getTransactionType(), requestBody.getAssetValue())); @@ -147,57 +157,108 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse } } + // TODO.refa: this logic needs to get extracted to a service final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setMembershipUuid(entity.getMembership().getUuid()); resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber()); - if (entity.getReversalAssetTx() != null) { - resource.getReversalAssetTx().setRevertedAssetTxUuid(entity.getUuid()); - resource.getReversalAssetTx().setMembershipUuid(entity.getMembership().getUuid()); - resource.getReversalAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber()); - } + withNonNull( + resource.getReversalAssetTx(), reversalAssetTxResource -> { + reversalAssetTxResource.setMembershipUuid(entity.getMembership().getUuid()); + reversalAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber()); + reversalAssetTxResource.setRevertedAssetTxUuid(entity.getUuid()); + withNonNull( + entity.getAdoptionAssetTx(), adoptionAssetTx -> + reversalAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid())); + withNonNull( + entity.getTransferAssetTx(), transferAssetTxResource -> + reversalAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid())); + }); - if (entity.getRevertedAssetTx() != null) { - resource.getRevertedAssetTx().setReversalAssetTxUuid(entity.getUuid()); - resource.getRevertedAssetTx().setMembershipUuid(entity.getMembership().getUuid()); - resource.getRevertedAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber()); - } + withNonNull( + resource.getRevertedAssetTx(), revertAssetTxResource -> { + revertAssetTxResource.setMembershipUuid(entity.getMembership().getUuid()); + revertAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber()); + revertAssetTxResource.setReversalAssetTxUuid(entity.getUuid()); + withNonNull( + entity.getRevertedAssetTx().getAdoptionAssetTx(), adoptionAssetTx -> + revertAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid())); + withNonNull( + entity.getRevertedAssetTx().getTransferAssetTx(), transferAssetTxResource -> + revertAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid())); + }); - if (entity.getAdoptionAssetTx() != null) { - resource.getAdoptionAssetTx().setTransferAssetTxUuid(entity.getUuid()); - resource.getAdoptionAssetTx().setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid()); - resource.getAdoptionAssetTx().setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber()); - } + withNonNull( + resource.getAdoptionAssetTx(), adoptionAssetTxResource -> { + adoptionAssetTxResource.setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid()); + adoptionAssetTxResource.setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber()); + adoptionAssetTxResource.setTransferAssetTxUuid(entity.getUuid()); + withNonNull( + entity.getAdoptionAssetTx().getReversalAssetTx(), reversalAssetTx -> + adoptionAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid())); + }); - if (entity.getTransferAssetTx() != null) { - resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid()); - resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid()); - resource.getTransferAssetTx().setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber()); - } + withNonNull( + resource.getTransferAssetTx(), transferAssetTxResource -> { + resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid()); + resource.getTransferAssetTx() + .setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber()); + resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid()); + withNonNull( + entity.getTransferAssetTx().getReversalAssetTx(), reversalAssetTx -> + transferAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid())); + }); }; + // TODO.refa: this logic needs to get extracted to a service final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { if (resource.getMembershipUuid() != null) { - final HsOfficeMembershipEntity membership = ofNullable(emw.find(HsOfficeMembershipEntity.class, resource.getMembershipUuid())) - .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] membership.uuid %s not found".formatted( + final HsOfficeMembershipEntity membership = ofNullable(emw.find( + HsOfficeMembershipEntity.class, + resource.getMembershipUuid())) + .orElseThrow(() -> new EntityNotFoundException("membership.uuid %s not found".formatted( resource.getMembershipUuid()))); entity.setMembership(membership); } - if (resource.getRevertedAssetTxUuid() != null) { + + if (entity.getTransactionType() == REVERSAL) { + if (resource.getRevertedAssetTxUuid() == null) { + throw new ValidationException("REVERSAL asset transaction requires revertedAssetTx.uuid"); + } final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid()) - .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedEntityUuid %s not found".formatted( + .orElseThrow(() -> new EntityNotFoundException("revertedAssetTx.uuid %s not found".formatted( resource.getRevertedAssetTxUuid()))); + revertedAssetTx.setReversalAssetTx(entity); entity.setRevertedAssetTx(revertedAssetTx); if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) { throw new ValidationException("given assetValue=" + resource.getAssetValue() + " but must be negative value from reverted asset tx: " + revertedAssetTx.getAssetValue()); } + + if (revertedAssetTx.getTransactionType() == TRANSFER) { + final var adoptionAssetTx = revertedAssetTx.getAdoptionAssetTx(); + final var adoptionReversalAssetTx = HsOfficeCoopAssetsTransactionEntity.builder() + .transactionType(REVERSAL) + .membership(adoptionAssetTx.getMembership()) + .revertedAssetTx(adoptionAssetTx) + .assetValue(adoptionAssetTx.getAssetValue().negate()) + .comment(resource.getComment()) + .reference(resource.getReference()) + .valueDate(resource.getValueDate()) + .build(); + adoptionAssetTx.setReversalAssetTx(adoptionReversalAssetTx); + adoptionReversalAssetTx.setRevertedAssetTx(adoptionAssetTx); + } } - final var adoptingMembership = determineAdoptingMembership(resource); - if (adoptingMembership != null) { - final var adoptingAssetTx = coopAssetsTransactionRepo.save(createAdoptingAssetTx(entity, adoptingMembership)); + if (resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER) { + final var adoptingMembership = determineAdoptingMembership(resource); + if ( entity.getMembership() == adoptingMembership) { + throw new ValidationException("transferring and adopting membership must be different, but both are " + + adoptingMembership.getTaggedMemberNumber()); + } + final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership); entity.setAdoptionAssetTx(adoptingAssetTx); } }; @@ -206,11 +267,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse final var adoptingMembershipUuid = resource.getAdoptingMembershipUuid(); final var adoptingMembershipMemberNumber = resource.getAdoptingMembershipMemberNumber(); if (adoptingMembershipUuid != null && adoptingMembershipMemberNumber != null) { - throw new IllegalArgumentException( - // @formatter:off - resource.getTransactionType() == TRANSFER - ? "[400] either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both" - : "[400] adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType=" + throw new ValidationException( + // @formatter:off + resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER + ? "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both" + : "adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType=" + resource.getTransactionType()); // @formatter:on } @@ -232,13 +293,9 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse + "' not found or not accessible"); } - if (resource.getTransactionType() == TRANSFER) { - throw new ValidationException( - "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=" - + TRANSFER); - } - - return null; + throw new ValidationException( + "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=" + + HsOfficeCoopAssetsTransactionTypeResource.TRANSFER); } private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java index 395c2895..b07bdebe 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java @@ -98,21 +98,21 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE private String comment; // Optionally, the UUID of the corresponding transaction for a reversal transaction. - @OneToOne(cascade = CascadeType.PERSIST) // TODO.impl: can probably be removed after office data migration + @OneToOne @JoinColumn(name = "revertedassettxuuid") private HsOfficeCoopAssetsTransactionEntity revertedAssetTx; // and the other way around - @OneToOne(mappedBy = "revertedAssetTx") + @OneToOne(mappedBy = "revertedAssetTx", cascade = CascadeType.PERSIST) private HsOfficeCoopAssetsTransactionEntity reversalAssetTx; // Optionally, the UUID of the corresponding transaction for a transfer transaction. - @OneToOne(cascade = CascadeType.PERSIST) // TODO.impl: can probably be removed after office data migration + @OneToOne(cascade = CascadeType.PERSIST) @JoinColumn(name = "assetadoptiontxuuid") private HsOfficeCoopAssetsTransactionEntity adoptionAssetTx; // and the other way around - @OneToOne(mappedBy = "adoptionAssetTx") + @OneToOne(mappedBy = "adoptionAssetTx", cascade = CascadeType.PERSIST) private HsOfficeCoopAssetsTransactionEntity transferAssetTx; @Override diff --git a/src/main/java/net/hostsharing/hsadminng/lambda/WithNonNull.java b/src/main/java/net/hostsharing/hsadminng/lambda/WithNonNull.java new file mode 100644 index 00000000..358f5319 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/lambda/WithNonNull.java @@ -0,0 +1,11 @@ +package net.hostsharing.hsadminng.lambda; + +import java.util.function.Consumer; + +public class WithNonNull { + public static void withNonNull(final T target, final Consumer code) { + if (target != null ) { + code.accept(target); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8d928aeb..9cfd265f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,15 @@ server: port : 8080 +management: + server: + port: 8081 + address: 127.0.0.1 + endpoints: + web: + exposure: + include: info, health, metrics + spring: datasource: diff --git a/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql b/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql index cf303db3..28575733 100644 --- a/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql +++ b/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql @@ -46,15 +46,15 @@ create or replace function rbac.hasGlobalAdminRole() stable -- leakproof language plpgsql as $$ declare - currentSubjectOrAssumedRolesUuids text; + assumedRoles text; begin begin - currentSubjectOrAssumedRolesUuids := current_setting('hsadminng.currentSubjectOrAssumedRolesUuids'); + assumedRoles := current_setting('hsadminng.assumedRoles'); exception when others then - currentSubjectOrAssumedRolesUuids := null; + assumedRoles := null; end; - return currentSubjectOrAssumedRolesUuids is null or length(currentSubjectOrAssumedRolesUuids) = 0; + return TRIM(COALESCE(assumedRoles, '')) = '' and rbac.isGlobalAdmin(); end; $$; --// diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql index 37c2affc..524e620c 100644 --- a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql +++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql @@ -35,21 +35,41 @@ create table if not exists hs_office.coopassettx --changeset michael.hoennig:hs-office-coopassets-BUSINESS-RULES endDelimiter:--// -- ---------------------------------------------------------------------------- -alter table hs_office.coopassettx - add constraint reversal_asset_tx_must_have_reverted_asset_tx - check (transactionType <> 'REVERSAL' or revertedAssetTxUuid is not null); +-- Not as CHECK constraints because those cannot be deferrable, +-- but we need these constraints deferrable because the rows are linked to each other. -alter table hs_office.coopassettx - add constraint non_reversal_asset_tx_must_not_have_reverted_asset_tx - check (transactionType = 'REVERSAL' or revertedAssetTxUuid is null or transactionType = 'REVERSAL'); +CREATE OR REPLACE FUNCTION validate_transaction_type() + RETURNS TRIGGER AS $$ +BEGIN + -- REVERSAL transactions must have revertedAssetTxUuid + IF NEW.transactionType = 'REVERSAL' AND NEW.revertedAssetTxUuid IS NULL THEN + RAISE EXCEPTION 'REVERSAL transactions must have revertedAssetTxUuid'; + END IF; -alter table hs_office.coopassettx - add constraint transfer_asset_tx_must_have_adopted_asset_tx - check (transactionType <> 'TRANSFER' or assetAdoptionTxUuid is not null); + -- Non-REVERSAL transactions must not have revertedAssetTxUuid + IF NEW.transactionType != 'REVERSAL' AND NEW.revertedAssetTxUuid IS NOT NULL THEN + RAISE EXCEPTION 'Non-REVERSAL transactions must not have revertedAssetTxUuid'; + END IF; + + -- TRANSFER transactions must have assetAdoptionTxUuid + IF NEW.transactionType = 'TRANSFER' AND NEW.assetAdoptionTxUuid IS NULL THEN + RAISE EXCEPTION 'TRANSFER transactions must have assetAdoptionTxUuid'; + END IF; + + -- Non-TRANSFER transactions must not have assetAdoptionTxUuid + IF NEW.transactionType != 'TRANSFER' AND NEW.assetAdoptionTxUuid IS NOT NULL THEN + RAISE EXCEPTION 'Non-TRANSFER transactions must not have assetAdoptionTxUuid'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Attach the trigger to the table +CREATE TRIGGER enforce_transaction_constraints + AFTER INSERT OR UPDATE ON hs_office.coopassettx + FOR EACH ROW EXECUTE FUNCTION validate_transaction_type(); -alter table hs_office.coopassettx - add constraint non_transfer_asset_tx_must_not_have_adopted_asset_tx - check (transactionType = 'TRANSFER' or assetAdoptionTxUuid is null); --// -- ============================================================================ diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index e53a7c75..9152d68f 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -68,6 +68,7 @@ public class ArchitectureTest { "..hs.hosting.asset", "..hs.hosting.asset.validators", "..hs.hosting.asset.factories", + "..hs.scenarios", "..errors", "..mapper", "..ping", @@ -160,9 +161,11 @@ public class ArchitectureTest { .that().resideInAPackage("..hs.office.(*)..") .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage( + "..hs.office.(*)..", "..hs.office.(*)..", "..hs.booking.(*)..", "..hs.hosting.(*)..", + "..hs.scenarios", "..hs.migration", "..rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest ); diff --git a/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java new file mode 100644 index 00000000..8b2bf3a0 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java @@ -0,0 +1,60 @@ +package net.hostsharing.hsadminng.config; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = {"management.port=0", "server.port=0"}) +// IMPORTANT: To test prod config, do not use test profile! +class WebSecurityConfigIntegrationTest { + + @Value("${local.server.port}") + private int serverPort; + + @Value("${local.management.port}") + private int managementPort; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void shouldSupportPingEndpoint() { + final var result = this.restTemplate.getForEntity( + "http://localhost:" + this.serverPort + "/api/ping", String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(result.getBody()).startsWith("pong"); + } + + @Test + public void shouldSupportActuatorEndpoint() { + final var result = this.restTemplate.getForEntity( + "http://localhost:" + this.managementPort + "/actuator", Map.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void shouldSupportHealthEndpoint() { + final var result = this.restTemplate.getForEntity( + "http://localhost:" + this.managementPort + "/actuator/health", Map.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(result.getBody().get("status")).isEqualTo("UP"); + } + + @Test + public void shouldSupportMetricsEndpoint() { + final var result = this.restTemplate.getForEntity( + "http://localhost:" + this.managementPort + "/actuator/metrics", Map.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 3598c0ce..ea433e71 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -12,6 +12,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealRepository; import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.ClassOrderer; @@ -24,6 +25,7 @@ import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -42,8 +44,9 @@ import static org.hamcrest.Matchers.matchesRegex; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional @TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java index 6b2bf5df..06e51a24 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java @@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository; import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -38,7 +39,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsBookingItemController.class) -@Import({StrictMapper.class, JsonObjectMapperConfiguration.class}) +@Import({StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class}) @RunWith(SpringRunner.class) class HsBookingItemControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java index 2e281882..d2860219 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java @@ -6,11 +6,13 @@ import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -23,8 +25,9 @@ import static org.hamcrest.Matchers.matchesRegex; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 2eff25de..5b732b68 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -14,6 +14,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.Nested; @@ -23,6 +24,7 @@ import org.junit.jupiter.api.TestClassOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; @@ -43,8 +45,9 @@ import static org.hamcrest.Matchers.matchesRegex; @Transactional @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java index c50e2dfd..45fd83c4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository; import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -52,7 +53,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsHostingAssetController.class) -@Import({ StandardMapper.class, JsonObjectMapperConfiguration.class}) +@Import({ StandardMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class }) @RunWith(SpringRunner.class) public class HsHostingAssetControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index 6b9188e6..b20a3c45 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -3,16 +3,19 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import io.restassured.RestAssured; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") class HsHostingAssetPropsControllerAcceptanceTest { @LocalServerPort diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java index e8e5090f..a6c64600 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java @@ -880,7 +880,6 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction); }); - coopAssets.entrySet().forEach(entry -> { final var legacyId = entry.getKey(); final var assetTransaction = entry.getValue(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java index ee52e315..4d6c9973 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java @@ -6,12 +6,14 @@ import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.apache.commons.lang3.RandomStringUtils; import org.json.JSONException; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -26,8 +28,9 @@ import static org.hamcrest.Matchers.startsWith; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java index f17a18a7..7a699a25 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java @@ -2,12 +2,15 @@ package net.hostsharing.hsadminng.hs.office.bankaccount; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -16,6 +19,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeBankAccountController.class) +@Import(DisableSecurityConfig.class) +@ActiveProfiles("test") class HsOfficeBankAccountControllerRestTest { @Autowired diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java index 19bb80a3..645dea0e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java @@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.apache.commons.lang3.RandomStringUtils; import org.json.JSONException; import org.junit.jupiter.api.AfterEach; @@ -15,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -33,8 +35,9 @@ import static org.hamcrest.Matchers.startsWith; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java index bc986db0..6e54acfa 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -14,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -31,8 +33,9 @@ import static org.hamcrest.Matchers.startsWith; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java index 1fdbe4f0..9409b856 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java @@ -8,8 +8,10 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.rbac.test.JsonBuilder; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import net.hostsharing.hsadminng.test.TestUuidGenerator; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.runner.RunWith; @@ -18,18 +20,27 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.function.Function; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.ADOPTION; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.DEPOSIT; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.DISBURSAL; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.REVERSAL; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.TRANSFER; import static net.hostsharing.hsadminng.rbac.test.JsonBuilder.jsonObject; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -38,11 +49,18 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeCoopAssetsTransactionController.class) -@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class }) +@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class }) +@ActiveProfiles("test") @RunWith(SpringRunner.class) class HsOfficeCoopAssetsTransactionControllerRestTest { - private static final UUID UNAVAILABLE_MEMBERSHIP_UUID = TestUuidGenerator.use(0); + // If you need to run just a single test-case in this data-driven test-method, set SINGLE_TEST_CASE_EXECUTION to true! + // There is a test which fails if single test-case execution active to avoid merging this to master. + private static final boolean SINGLE_TEST_CASE_EXECUTION = false; + + private static final int DYNAMIC_UUID_START_INDEX = 13; + + private static final UUID UNAVAILABLE_UUID = TestUuidGenerator.use(0); private static final String UNAVAILABLE_MEMBER_NUMBER = "M-1234699"; private static final UUID ORIGIN_MEMBERSHIP_UUID = TestUuidGenerator.use(1); @@ -65,9 +83,11 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .memberNumberSuffix(suffixOf(AVAILABLE_TARGET_MEMBER_NUMBER)) .build(); - // the following refs might change if impl changes - private static final UUID NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.ref(4); - private static final UUID NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.ref(5); + // The following refs depend on the implementation of the respective implementation and might change if it changes. + // The same TestUuidGenerator.ref(#) does NOT mean the UUIDs refer to the same entity, + // its rather coincidence because different test-cases have different execution paths in the production code. + private static final UUID NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.ref(DYNAMIC_UUID_START_INDEX); + private static final UUID NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.ref(DYNAMIC_UUID_START_INDEX); private static final UUID SOME_EXISTING_LOSS_ASSET_TX_UUID = TestUuidGenerator.use(3); public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_LOSS_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() @@ -80,6 +100,402 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .valueDate(LocalDate.parse("2024-10-15")) .build(); + private static final UUID SOME_EXISTING_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.use(4); + public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_EXISTING_TRANSFER_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(HsOfficeCoopAssetsTransactionType.TRANSFER) + .assetValue(BigDecimal.valueOf(-256)) + .reference("some transfer asset tx ref") + .comment("some transfer asset tx comment") + .valueDate(LocalDate.parse("2024-10-15")) + .build(); + + private static final UUID SOME_EXISTING_ADOPTION_ASSET_TX_UUID = TestUuidGenerator.use(5); + public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_ADOPTION_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_EXISTING_ADOPTION_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(HsOfficeCoopAssetsTransactionType.TRANSFER) + .assetValue(SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY.getAssetValue().negate()) + .reference("some adoption asset tx ref") + .comment("some adoption asset tx comment") + .valueDate(LocalDate.parse("2024-10-15")) + .transferAssetTx(SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY) + .build(); + { + SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY.setAdoptionAssetTx(SOME_EXISTING_ADOPTION_ASSET_TX_ENTITY); + } + + private final static UUID SOME_REVERTED_DISBURSAL_ASSET_TX_UUID = TestUuidGenerator.use(7); + private final static UUID SOME_DISBURSAL_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.use(8); + private final HsOfficeCoopAssetsTransactionEntity SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_REVERTED_DISBURSAL_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(DISBURSAL) + .assetValue(BigDecimal.valueOf(-128.00)) + .valueDate(LocalDate.parse("2024-10-15")) + .reference("some disbursal") + .comment("some disbursal to get reverted") + .reversalAssetTx( + HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_DISBURSAL_REVERSAL_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(REVERSAL) + .assetValue(BigDecimal.valueOf(128.00)) + .valueDate(LocalDate.parse("2024-10-20")) + .reference("some reversal") + .comment("some reversal of a disbursal asset tx") + .build() + ) + .build(); + { + SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY.getReversalAssetTx().setRevertedAssetTx(SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY); + } + + private final static UUID SOME_REVERTED_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.use(9); + private final static UUID SOME_TRANSFER_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.use(10); + private final static UUID SOME_REVERTED_ADOPTION_ASSET_TX_UUID = TestUuidGenerator.use(11); + private final static UUID SOME_ADOPTION_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.use(12); + final HsOfficeCoopAssetsTransactionEntity SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_REVERTED_TRANSFER_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(TRANSFER) + .assetValue(BigDecimal.valueOf(-1024)) + .valueDate(LocalDate.parse("2024-11-10")) + .reference("some transfer") + .comment("some transfer to get reverted") + .adoptionAssetTx( + HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_REVERTED_ADOPTION_ASSET_TX_UUID) + .membership(AVAILABLE_MEMBER_ENTITY) + .transactionType(ADOPTION) + .assetValue(BigDecimal.valueOf(1024)) + .valueDate(LocalDate.parse("2024-11-10")) + .reference("related adoption") + .comment("some reversal of a transfer asset tx") + .reversalAssetTx( + HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_ADOPTION_REVERSAL_ASSET_TX_UUID) + .membership(AVAILABLE_MEMBER_ENTITY) + .transactionType(REVERSAL) + .assetValue(BigDecimal.valueOf(1024)) + .valueDate(LocalDate.parse("2024-11-11")) + .reference("some reversal") + .comment("some adoption asset tx reversal") + .build() + ) + .build() + ) + .reversalAssetTx( + HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_TRANSFER_REVERSAL_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(REVERSAL) + .assetValue(BigDecimal.valueOf(1024)) + .valueDate(LocalDate.parse("2024-11-11")) + .reference("some transfer") + .comment("some transfer asset tx reversal") + .build() + ) + .build(); + { + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx() + .setTransferAssetTx(SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY); + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getReversalAssetTx() + .setRevertedAssetTx(SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY); + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx().getReversalAssetTx() + .setRevertedAssetTx(SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx()); + } + + private static final String EXPECTED_RESULT_FROM_GET_SINGLE = """ + { + "uuid": "99999999-9999-9999-9999-999999999999", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": -1024, + "valueDate": "2024-11-10", + "reference": "some transfer", + "comment": "some transfer to get reverted", + "adoptionAssetTx": { + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "ADOPTION", + "assetValue": 1024, + "valueDate": "2024-11-10", + "reference": "related adoption", + "comment": "some reversal of a transfer asset tx", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "99999999-9999-9999-9999-999999999999", + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc" + } + } + """; + + + private static final String EXPECTED_RESULT_FROM_GET_LIST = """ + [ + { + "uuid": "33333333-3333-3333-3333-333333333333", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "LOSS", + "assetValue": -64, + "valueDate": "2024-10-15", + "reference": "some loss asset tx ref", + "comment": "some loss asset tx comment", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": null + }, + { + "uuid": "44444444-4444-4444-4444-444444444444", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": -256, + "valueDate": "2024-10-15", + "reference": "some transfer asset tx ref", + "comment": "some transfer asset tx comment", + "adoptionAssetTx": { + "uuid": "55555555-5555-5555-5555-555555555555", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": 256, + "valueDate": "2024-10-15", + "reference": "some adoption asset tx ref", + "comment": "some adoption asset tx comment", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "44444444-4444-4444-4444-444444444444", + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": null + }, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": null + }, + { + "uuid": "55555555-5555-5555-5555-555555555555", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": 256, + "valueDate": "2024-10-15", + "reference": "some adoption asset tx ref", + "comment": "some adoption asset tx comment", + "adoptionAssetTx": null, + "transferAssetTx": { + "uuid": "44444444-4444-4444-4444-444444444444", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": -256, + "valueDate": "2024-10-15", + "reference": "some transfer asset tx ref", + "comment": "some transfer asset tx comment", + "adoptionAssetTx.uuid": "55555555-5555-5555-5555-555555555555", + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": null + }, + "revertedAssetTx": null, + "reversalAssetTx": null + }, + { + "uuid": "77777777-7777-7777-7777-777777777777", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "DISBURSAL", + "assetValue": -128.0, + "valueDate": "2024-10-15", + "reference": "some disbursal", + "comment": "some disbursal to get reverted", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": { + "uuid": "88888888-8888-8888-8888-888888888888", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "REVERSAL", + "assetValue": 128.0, + "valueDate": "2024-10-20", + "reference": "some reversal", + "comment": "some reversal of a disbursal asset tx", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": "77777777-7777-7777-7777-777777777777", + "reversalAssetTx.uuid": null + } + }, + { + "uuid": "88888888-8888-8888-8888-888888888888", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "REVERSAL", + "assetValue": 128.0, + "valueDate": "2024-10-20", + "reference": "some reversal", + "comment": "some reversal of a disbursal asset tx", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": { + "uuid": "77777777-7777-7777-7777-777777777777", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "DISBURSAL", + "assetValue": -128.0, + "valueDate": "2024-10-15", + "reference": "some disbursal", + "comment": "some disbursal to get reverted", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "88888888-8888-8888-8888-888888888888" + }, + "reversalAssetTx": null + }, + { + "uuid": "99999999-9999-9999-9999-999999999999", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": -1024, + "valueDate": "2024-11-10", + "reference": "some transfer", + "comment": "some transfer to get reverted", + "adoptionAssetTx": { + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "ADOPTION", + "assetValue": 1024, + "valueDate": "2024-11-10", + "reference": "related adoption", + "comment": "some reversal of a transfer asset tx", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "99999999-9999-9999-9999-999999999999", + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc" + }, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": { + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "REVERSAL", + "assetValue": 1024, + "valueDate": "2024-11-11", + "reference": "some transfer", + "comment": "some transfer asset tx reversal", + "adoptionAssetTx.uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": "99999999-9999-9999-9999-999999999999", + "reversalAssetTx.uuid": null + } + }, + { + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "ADOPTION", + "assetValue": 1024, + "valueDate": "2024-11-10", + "reference": "related adoption", + "comment": "some reversal of a transfer asset tx", + "adoptionAssetTx": null, + "transferAssetTx": { + "uuid": "77777777-7777-7777-7777-777777777777", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "DISBURSAL", + "assetValue": -128.0, + "valueDate": "2024-10-15", + "reference": "some disbursal", + "comment": "some disbursal to get reverted", + "adoptionAssetTx.uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "88888888-8888-8888-8888-888888888888" + }, + "revertedAssetTx": null, + "reversalAssetTx": { + "uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "REVERSAL", + "assetValue": 1024, + "valueDate": "2024-11-11", + "reference": "some reversal", + "comment": "some adoption asset tx reversal", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "77777777-7777-7777-7777-777777777777", + "revertedAssetTx.uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "reversalAssetTx.uuid": null + } + }, + { + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "REVERSAL", + "assetValue": 1024, + "valueDate": "2024-11-11", + "reference": "some transfer", + "comment": "some transfer asset tx reversal", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": { + "uuid": "77777777-7777-7777-7777-777777777777", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "DISBURSAL", + "assetValue": -128.0, + "valueDate": "2024-10-15", + "reference": "some disbursal", + "comment": "some disbursal to get reverted", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + "reversalAssetTx": null + }, + { + "uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "REVERSAL", + "assetValue": 1024, + "valueDate": "2024-11-11", + "reference": "some reversal", + "comment": "some adoption asset tx reversal", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": { + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "ADOPTION", + "assetValue": 1024, + "valueDate": "2024-11-10", + "reference": "related adoption", + "comment": "some reversal of a transfer asset tx", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "77777777-7777-7777-7777-777777777777", + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc" + }, + "reversalAssetTx": null + } + ] + """; + @Autowired MockMvc mockMvc; @@ -117,6 +533,44 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { requestBody -> requestBody.without("membership.uuid"), "[membershipUuid must not be null but is \"null\"]"), // TODO.impl: should be membership.uuid, Spring validation-problem? + MEMBERSHIP_UUID_NOT_FOUND_OR_NOT_ACCESSIBLE( + requestBody -> requestBody.with("membership.uuid", UNAVAILABLE_UUID), + "membership.uuid " + UNAVAILABLE_UUID + " not found"), + + MEMBERSHIP_UUID_AND_MEMBER_NUMBER_MUST_NOT_BE_GIVEN_BOTH( + requestBody -> requestBody + .with("transactionType", TRANSFER.name()) + .with("assetValue", "-128.00") + .with("adoptingMembership.uuid", UNAVAILABLE_UUID) + .with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER), + "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both"), + + MEMBERSHIP_UUID_OR_MEMBER_NUMBER_MUST_BE_GIVEN( + requestBody -> requestBody + .with("transactionType", TRANSFER) + .with("assetValue", "-128.00"), + "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=TRANSFER"), + + REVERSAL_ASSET_TRANSACTION_REQUIRES_REVERTED_ASSET_TX_UUID( + requestBody -> requestBody + .with("transactionType", REVERSAL) + .with("assetValue", "-128.00"), + "REVERSAL asset transaction requires revertedAssetTx.uuid"), + + REVERSAL_ASSET_TRANSACTION_REQUIRES_AVAILABLE_REVERTED_ASSET_TX_UUID( + requestBody -> requestBody + .with("transactionType", REVERSAL) + .with("assetValue", "-128.00") + .with("revertedAssetTx.uuid", UNAVAILABLE_UUID), + "revertedAssetTx.uuid " + UNAVAILABLE_UUID + " not found"), + + REVERSAL_ASSET_TRANSACTION_MUST_NEGATE_VALUE_OF_REVERTED_ASSET_TX( + requestBody -> requestBody + .with("transactionType", REVERSAL) + .with("assetValue", "128.00") + .with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID), + "given assetValue=128.00 but must be negative value from reverted asset tx: -64"), + TRANSACTION_TYPE_MISSING( requestBody -> requestBody.without("transactionType"), "[transactionType must not be null but is \"null\"]"), @@ -127,35 +581,40 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { ASSETS_VALUE_FOR_DEPOSIT_MUST_BE_POSITIVE( requestBody -> requestBody - .with("transactionType", "DEPOSIT") + .with("transactionType", DEPOSIT) .with("assetValue", -64.00), "[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"), ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE( requestBody -> requestBody - .with("transactionType", "DISBURSAL") + .with("transactionType", DISBURSAL) .with("assetValue", 64.00), "[for DISBURSAL, assetValue must be negative but is \"64.00\"]"), - //TODO: other transaction types + ADOPTING_MEMBERSHIP_MUST_NOT_BE_THE_SAME( + requestBody -> requestBody + .with("transactionType", TRANSFER) + .with("assetValue", -64.00) + .with("adoptingMembership.uuid", ORIGIN_MEMBERSHIP_UUID), + "transferring and adopting membership must be different, but both are M-1111100"), ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( requestBody -> requestBody - .with("transactionType", "TRANSFER") + .with("transactionType", TRANSFER) .with("assetValue", -64.00) .with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER), "adoptingMembership.memberNumber='M-1234699' not found or not accessible"), ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( requestBody -> requestBody - .with("transactionType", "TRANSFER") + .with("transactionType", TRANSFER) .with("assetValue", -64.00) - .with("adoptingMembership.uuid", UNAVAILABLE_MEMBERSHIP_UUID.toString()), - "adoptingMembership.uuid='" + UNAVAILABLE_MEMBERSHIP_UUID + "' not found or not accessible"), + .with("adoptingMembership.uuid", UNAVAILABLE_UUID), + "adoptingMembership.uuid='" + UNAVAILABLE_UUID + "' not found or not accessible"), ASSETS_VALUE_MUST_NOT_BE_NULL( requestBody -> requestBody - .with("transactionType", "REVERSAL") + .with("transactionType", REVERSAL) .with("assetValue", 0.00), "[assetValue must not be 0 but is \"0.00\"]"), @@ -190,8 +649,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { @EnumSource(BadRequestTestCases.class) void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception { // HOWTO: run just a single test-case in a data-driven test-method - // org.assertj.core.api.Assumptions.assumeThat( - // testCase == ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue(); + // - set SINGLE_TEST_CASE_EXECUTION to true - see above + // - select the test case enum value you want to run + assumeThat(!SINGLE_TEST_CASE_EXECUTION || + testCase == BadRequestTestCases.ADOPTING_MEMBERSHIP_MUST_NOT_BE_THE_SAME).isTrue(); // when mockMvc.perform(MockMvcRequestBuilders @@ -202,9 +663,9 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .accept(MediaType.APPLICATION_JSON)) // then - .andExpect(jsonPath("message", is("ERROR: [400] " + testCase.expectedErrorMessage))) .andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusPhrase", is("Bad Request"))) + .andExpect(jsonPath("message", is("ERROR: [400] " + testCase.expectedErrorMessage))) .andExpect(status().is4xxClientError()); } @@ -212,28 +673,38 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { REVERTING_SIMPLE_ASSET_TRANSACTION( requestBody -> requestBody - .with("transactionType", "REVERSAL") + .with("transactionType", REVERSAL) .with("assetValue", "64.00") .with("valueDate", "2024-10-15") - .with("reference", "reversal ref") - .with("comment", "reversal comment") - .with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString()), - Expected.REVERT_RESPONSE), + .with("reference", "reversal of loss ref") + .with("comment", "reversal of loss asset tx comment") + .with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID), + Expected.REVERT_LOSS_RESPONSE), TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_NUMBER( requestBody -> requestBody - .with("transactionType", "TRANSFER") + .with("transactionType", TRANSFER) .with("assetValue", -64.00) .with("adoptingMembership.memberNumber", AVAILABLE_TARGET_MEMBER_NUMBER), Expected.TRANSFER_RESPONSE), TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_UUID( requestBody -> requestBody - .with("transactionType", "TRANSFER") + .with("transactionType", TRANSFER) .with("assetValue", -64.00) - .with("membership.uuid", ORIGIN_MEMBERSHIP_UUID.toString()) - .with("adoptingMembership.uuid", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString()), - Expected.TRANSFER_RESPONSE); + .with("membership.uuid", ORIGIN_MEMBERSHIP_UUID) + .with("adoptingMembership.uuid", AVAILABLE_TARGET_MEMBERSHIP_UUID), + Expected.TRANSFER_RESPONSE), + + REVERTING_TRANSFER_ASSET_TRANSACTION_IMPLICITLY_REVERTS_ADOPTING_ASSET_TRANSACTION( + requestBody -> requestBody + .with("transactionType", REVERSAL) + .with("assetValue", "256.00") + .with("valueDate", "2024-10-15") + .with("reference", "reversal of transfer ref") + .with("comment", "reversal of transfer asset tx comment") + .with("revertedAssetTx.uuid", SOME_EXISTING_TRANSFER_ASSET_TX_UUID), + Expected.REVERT_TRANSFER_RESPONSE); private final Function givenBodyTransformation; private final String expectedResponseBody; @@ -251,7 +722,7 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { private static class Expected { - public static final String REVERT_RESPONSE = """ + public static final String REVERT_LOSS_RESPONSE = """ { "uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", @@ -259,9 +730,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { "transactionType": "REVERSAL", "assetValue": 64.00, "valueDate": "2024-10-15", - "reference": "reversal ref", - "comment": "reversal comment", + "reference": "reversal of loss ref", + "comment": "reversal of loss asset tx comment", "adoptionAssetTx": null, + "reversalAssetTx": null, "transferAssetTx": null, "revertedAssetTx": { "uuid": "%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", @@ -279,7 +751,9 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { } } """ - .replace("%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID.toString()) + .replace( + "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", + NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID.toString()) .replace("%{ORIGIN_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString()) .replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER) .replace("%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString()); @@ -303,20 +777,57 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { "reversalAssetTx": null } """ - .replace("%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}", NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID.toString()) + .replace( + "%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}", + NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID.toString()) .replace("%{ORIGIN_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString()) .replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER) .replace("%{AVAILABLE_MEMBERSHIP_UUID}", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString()) .replace("%{AVAILABLE_TARGET_MEMBER_NUMBER}", AVAILABLE_TARGET_MEMBER_NUMBER); + + public static final String REVERT_TRANSFER_RESPONSE = """ + { + "uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", + "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", + "membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}", + "transactionType": "REVERSAL", + "assetValue": 256.00, + "valueDate": "2024-10-15", + "reference": "reversal of transfer ref", + "comment": "reversal of transfer asset tx comment", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": { + "uuid": "%{SOME_EXISTING_TRANSFER_ASSET_TX_UUID}", + "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", + "membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}", + "transactionType": "TRANSFER", + "assetValue": -256.00, + "valueDate": "2024-10-15", + "reference": "some transfer asset tx ref", + "comment": "some transfer asset tx comment", + "adoptionAssetTx.uuid": "%{SOME_EXISTING_ADOPTION_ASSET_TX_UUID}", + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}" + } + } + """ + .replace( + "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", + NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID.toString()) + .replace("%{ORIGIN_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString()) + .replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER) + .replace("%{SOME_EXISTING_TRANSFER_ASSET_TX_UUID}", SOME_EXISTING_TRANSFER_ASSET_TX_UUID.toString()) + .replace("%{SOME_EXISTING_ADOPTION_ASSET_TX_UUID}", SOME_EXISTING_ADOPTION_ASSET_TX_UUID.toString()); } } @ParameterizedTest @EnumSource(SuccessfullyCreatedTestCases.class) void respondWithSuccessfullyCreated(final SuccessfullyCreatedTestCases testCase) throws Exception { - // uncomment, if you need to run just a single test-case in this data-driven test-method - // org.assertj.core.api.Assumptions.assumeThat( - // testCase == ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue(); + assumeThat(!SINGLE_TEST_CASE_EXECUTION || + testCase == SuccessfullyCreatedTestCases.REVERTING_TRANSFER_ASSET_TRANSACTION_IMPLICITLY_REVERTS_ADOPTING_ASSET_TRANSACTION).isTrue(); // when mockMvc.perform(MockMvcRequestBuilders @@ -331,12 +842,77 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponseBody))); } + @Test + void getSingleGeneratesProperJsonForAvailableUuid() throws Exception { + // given + when(coopAssetsTransactionRepo.findByUuid(SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getUuid())) + .thenReturn(Optional.of(SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/office/coopassetstransactions/" + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getUuid()) + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$", lenientlyEquals(EXPECTED_RESULT_FROM_GET_SINGLE))); + } + + @Test + void getSingleGeneratesNotFoundForUnavailableUuid() throws Exception { + // given + when(coopAssetsTransactionRepo.findByUuid(UNAVAILABLE_UUID)).thenReturn(Optional.empty()); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/office/coopassetstransactions/" + UNAVAILABLE_UUID) + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().isNotFound()); + } + + @Test + void getListGeneratesProperJson() throws Exception { + // given + when(coopAssetsTransactionRepo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange(null, null, null)) + .thenReturn(List.of( + SOME_EXISTING_LOSS_ASSET_TX_ENTITY, + SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY, + SOME_EXISTING_ADOPTION_ASSET_TX_ENTITY, + SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY, + SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY.getReversalAssetTx(), + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY, + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx(), + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getReversalAssetTx(), + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx().getReversalAssetTx() + )); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/office/coopassetstransactions") + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$", lenientlyEquals(EXPECTED_RESULT_FROM_GET_LIST))); + } + + @Test + void singleTestCaseExecutionMustBeDisabled() { + assertThat(SINGLE_TEST_CASE_EXECUTION).isFalse(); + } + @BeforeEach void initMocks() { - TestUuidGenerator.start(4); + TestUuidGenerator.start(DYNAMIC_UUID_START_INDEX); when(emw.find(eq(HsOfficeMembershipEntity.class), eq(ORIGIN_MEMBERSHIP_UUID))).thenReturn(ORIGIN_TARGET_MEMBER_ENTITY); - when(emw.find(eq(HsOfficeMembershipEntity.class), eq(AVAILABLE_TARGET_MEMBERSHIP_UUID))).thenReturn(AVAILABLE_MEMBER_ENTITY); + when(emw.find(eq(HsOfficeMembershipEntity.class), eq(AVAILABLE_TARGET_MEMBERSHIP_UUID))).thenReturn( + AVAILABLE_MEMBER_ENTITY); final var availableMemberNumber = Integer.valueOf(AVAILABLE_TARGET_MEMBER_NUMBER.substring("M-".length())); when(membershipRepo.findMembershipByMemberNumber(eq(availableMemberNumber))).thenReturn(AVAILABLE_MEMBER_ENTITY); @@ -346,6 +922,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_LOSS_ASSET_TX_UUID)) .thenReturn(Optional.of(SOME_EXISTING_LOSS_ASSET_TX_ENTITY)); + when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_TRANSFER_ASSET_TX_UUID)) + .thenReturn(Optional.of(SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY)); + when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_ADOPTION_ASSET_TX_UUID)) + .thenReturn(Optional.of(SOME_EXISTING_ADOPTION_ASSET_TX_ENTITY)); when(coopAssetsTransactionRepo.save(any(HsOfficeCoopAssetsTransactionEntity.class))) .thenAnswer(invocation -> { final var entity = (HsOfficeCoopAssetsTransactionEntity) invocation.getArgument(0); @@ -358,10 +938,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { } private int partnerNumberOf(final String memberNumber) { - return Integer.parseInt(memberNumber.substring("M-".length(), memberNumber.length()-2)); + return Integer.parseInt(memberNumber.substring("M-".length(), memberNumber.length() - 2)); } private String suffixOf(final String memberNumber) { - return memberNumber.substring("M-".length()+5); + return memberNumber.substring("M-".length() + 5); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java index c2e6a11f..e7b1e52f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -14,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -28,7 +30,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.startsWith; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {HsadminNgApplication.class, JpaAttempt.class}) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = {HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class}) +@ActiveProfiles("test") @Transactional class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java index 6da7ddd1..2b0129a3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java @@ -3,12 +3,15 @@ package net.hostsharing.hsadminng.hs.office.coopshares; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.rbac.test.JsonBuilder; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -21,6 +24,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeCoopSharesTransactionController.class) +@Import(DisableSecurityConfig.class) +@ActiveProfiles("test") class HsOfficeCoopSharesTransactionControllerRestTest { @Autowired diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java index 53f32e80..65cfa7f5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java @@ -12,6 +12,7 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -19,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -33,8 +35,9 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java index 2a8beba2..c2dd44c1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java @@ -8,6 +8,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.json.JSONException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; @@ -15,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -31,8 +33,9 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java index cc4b7d0d..432a38e6 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransact import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -14,6 +15,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -27,7 +29,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficeMembershipController.class) -@Import(StandardMapper.class) +@Import({StandardMapper.class, DisableSecurityConfig.class}) +@ActiveProfiles("test") public class HsOfficeMembershipControllerRestTest { @Autowired diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java index 9f2f3318..82472faa 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java @@ -13,10 +13,12 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealReposito import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.UUID; @@ -29,8 +31,9 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanup { private static final UUID GIVEN_NON_EXISTING_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java index 74270f69..caf1fe6f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -16,6 +17,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -36,7 +38,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(HsOfficePartnerController.class) -@Import(StandardMapper.class) +@Import({StandardMapper.class, DisableSecurityConfig.class}) +@ActiveProfiles("test") class HsOfficePartnerControllerRestTest { static final UUID GIVEN_MANDANTE_UUID = UUID.randomUUID(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java index 9339ff56..7ab55de0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java @@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; @@ -13,6 +14,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -26,8 +28,9 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup { @LocalServerPort diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java index 9bc4eafa..b65ae95e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java @@ -9,11 +9,13 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationTypeResource; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.UUID; @@ -26,8 +28,9 @@ import static org.hamcrest.Matchers.startsWith; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java index dec419f0..969019e1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java @@ -16,7 +16,8 @@ import net.hostsharing.hsadminng.hs.office.scenarios.membership.CancelMembership import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDepositTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDisbursalTransaction; -import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertTransaction; +import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertSimpleTransaction; +import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertTransferTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsTransferTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesCancellationTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction; @@ -29,23 +30,29 @@ import net.hostsharing.hsadminng.hs.office.scenarios.person.ShouldUpdatePersonDa 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.hs.scenarios.Produces; +import net.hostsharing.hsadminng.hs.scenarios.Requires; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; -import net.hostsharing.hsadminng.test.IgnoreOnFailure; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension; +import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; @Tag("scenarioTest") @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class }, + classes = { HsadminNgApplication.class, DisableSecurityConfig.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}", @@ -53,430 +60,487 @@ import org.springframework.test.annotation.DirtiesContext; "hsadminng.superuser=${HSADMINNG_SUPERUSER:superuser-alex@hostsharing.net}" } ) -@DirtiesContext -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@ActiveProfiles("test") +@TestClassOrder(ClassOrderer.OrderAnnotation.class) @ExtendWith(IgnoreOnFailureExtension.class) class HsOfficeScenarioTests extends ScenarioTest { - @Test - @Order(1010) - @Produces(explicitly = "Partner: P-31010 - Test AG", implicitly = { "Person: Test AG", "Contact: Test AG - Hamburg" }) - void shouldCreateLegalPersonAsPartner() { - new CreatePartner(this) - .given("partnerNumber", "P-31010") - .given("personType", "LEGAL_PERSON") - .given("tradeName", "Test AG") - .given("contactCaption", "Test AG - Hamburg") - .given("postalAddress", """ - "firm": "Test AG", - "street": "Shanghai-Allee 1", - "zipcode": "20123", - "city": "Hamburg", - "country": "Germany" - """) - .given("officePhoneNumber", "+49 40 654321-0") - .given("emailAddress", "hamburg@test-ag.example.org") - .doRun() - .keep(); + @Nested + @Order(10) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class PartnerScenarios { + + @Test + @Order(1010) + @Produces(explicitly = "Partner: P-31010 - Test AG", implicitly = { "Person: Test AG", "Contact: Test AG - Hamburg" }) + void shouldCreateLegalPersonAsPartner() { + new CreatePartner(scenarioTest) + .given("partnerNumber", "P-31010") + .given("personType", "LEGAL_PERSON") + .given("tradeName", "Test AG") + .given("contactCaption", "Test AG - Hamburg") + .given( + "postalAddress", """ + "firm": "Test AG", + "street": "Shanghai-Allee 1", + "zipcode": "20123", + "city": "Hamburg", + "country": "Germany" + """) + .given("officePhoneNumber", "+49 40 654321-0") + .given("emailAddress", "hamburg@test-ag.example.org") + .doRun() + .keep(); + } + + @Test + @Order(1011) + @Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", + implicitly = { "Person: Michelle Matthieu", "Contact: Michelle Matthieu" }) + void shouldCreateNaturalPersonAsPartner() { + new CreatePartner(scenarioTest) + .given("partnerNumber", "P-31011") + .given("personType", "NATURAL_PERSON") + .given("givenName", "Michelle") + .given("familyName", "Matthieu") + .given("contactCaption", "Michelle Matthieu") + .given( + "postalAddress", """ + "name": "Michelle Matthieu", + "street": "An der Wandse 34", + "zipcode": "22123", + "city": "Hamburg", + "country": "Germany" + """) + .given("officePhoneNumber", "+49 40 123456") + .given("emailAddress", "michelle.matthieu@example.org") + .doRun() + .keep(); + } + + @Test + @Order(1020) + @Requires("Person: Test AG") + @Produces("Representative: Tracy Trust for Test AG") + void shouldAddRepresentativeToPartner() { + new AddRepresentativeToPartner(scenarioTest) + .given("partnerPersonTradeName", "Test AG") + .given("representativeFamilyName", "Trust") + .given("representativeGivenName", "Tracy") + .given( + "representativePostalAddress", """ + "name": "Michelle Matthieu", + "street": "An der Alster 100", + "zipcode": "20000", + "city": "Hamburg", + "country": "Germany" + """) + .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(scenarioTest) + .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(scenarioTest) + .given("operationsContactPerson", "Dennis Krause") + .doRun(); + } + + @Test + @Order(1090) + void shouldDeletePartner() { + new DeletePartner(scenarioTest) + .given("partnerNumber", "P-31020") + .doRun(); + } } - @Test - @Order(1011) - @Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", - implicitly = { "Person: Michelle Matthieu", "Contact: Michelle Matthieu" }) - void shouldCreateNaturalPersonAsPartner() { - new CreatePartner(this) - .given("partnerNumber", "P-31011") - .given("personType", "NATURAL_PERSON") - .given("givenName", "Michelle") - .given("familyName", "Matthieu") - .given("contactCaption", "Michelle Matthieu") - .given("postalAddress", """ - "name": "Michelle Matthieu", - "street": "An der Wandse 34", - "zipcode": "22123", - "city": "Hamburg", - "country": "Germany" - """) - .given("officePhoneNumber", "+49 40 123456") - .given("emailAddress", "michelle.matthieu@example.org") - .doRun() - .keep(); + @Nested + @Order(11) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class PartnerContactScenarios { + + @Test + @Order(1100) + @Requires("Partner: P-31011 - Michelle Matthieu") + void shouldAmendContactData() { + new AmendContactData(scenarioTest) + .given("partnerName", "Matthieu") + .given("newEmailAddress", "michelle@matthieu.example.org") + .doRun(); + } + + @Test + @Order(1101) + @Requires("Partner: P-31011 - Michelle Matthieu") + void shouldAddPhoneNumberToContactData() { + new AddPhoneNumberToContactData(scenarioTest) + .given("partnerName", "Matthieu") + .given("phoneNumberKeyToAdd", "mobile") + .given("phoneNumberToAdd", "+49 152 1234567") + .doRun(); + } + + @Test + @Order(1102) + @Requires("Partner: P-31011 - Michelle Matthieu") + void shouldRemovePhoneNumberFromContactData() { + new RemovePhoneNumberFromContactData(scenarioTest) + .given("partnerName", "Matthieu") + .given("phoneNumberKeyToRemove", "office") + .doRun(); + } + + @Test + @Order(1103) + @Requires("Partner: P-31010 - Test AG") + void shouldReplaceContactData() { + new ReplaceContactData(scenarioTest) + .given("partnerName", "Test AG") + .given("newContactCaption", "Test AG - China") + .given("newPostalAddress", """ + "firm": "Test AG", + "name": "Fi Zhong-Kha", + "building": "Thi Chi Koh Building", + "street": "No.2 Commercial Second Street", + "district": "Niushan Wei Wu", + "city": "Dongguan City", + "province": "Guangdong Province", + "country": "China" + """) + .given("newOfficePhoneNumber", "++15 999 654321") + .given("newEmailAddress", "norden@test-ag.example.org") + .doRun(); + } } - @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", """ - "name": "Michelle Matthieu", - "street": "An der Alster 100", - "zipcode": "20000", - "city": "Hamburg", - "country": "Germany" - """) - .given("representativePhoneNumber", "+49 40 123456") - .given("representativeEMailAddress", "tracy.trust@example.org") - .doRun() - .keep(); + @Nested + @Order(12) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class PartnerPersonScenarios { + + @Test + @Order(1201) + @Requires("Partner: P-31011 - Michelle Matthieu") + void shouldUpdatePersonData() { + new ShouldUpdatePersonData(scenarioTest) + .given("oldFamilyName", "Matthieu") + .given("newFamilyName", "Matthieu-Zhang") + .doRun(); + } } - @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(); + @Nested + @Order(20) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class DebitorScenarios { + @Test + @Order(2010) + @Requires("Partner: P-31010 - Test AG") + @Produces("Debitor: D-3101000 - Test AG - main debitor") + void shouldCreateSelfDebitorForPartner() { + new CreateSelfDebitorForPartner(scenarioTest) + .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: D-3101001 - Test AG - main debitor") + void shouldCreateExternalDebitorForPartner() { + new CreateExternalDebitorForPartner(scenarioTest) + .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") + @Produces(explicitly = "Debitor: D-3101000 - Test AG - delete debitor", permanent = false) + void shouldDeleteDebitor() { + new DeleteDebitor(scenarioTest) + .given("partnerNumber", "P-31020") + .given("debitorSuffix", "02") + .doRun(); + } + + @Test + @Order(2020) + @Requires("Debitor: D-3101000 - Test AG - main debitor") + @Disabled("see TODO.spec in DontDeleteDefaultDebitor") + void shouldNotDeleteDefaultDebitor() { + new DontDeleteDefaultDebitor(scenarioTest) + .given("partnerNumber", "P-31010") + .given("debitorSuffix", "00") + .doRun(); + } } - @Test - @Order(1039) - @Requires("Operations-Contact: Dennis Krause for Test AG") - void shouldRemoveOperationsContactFromPartner() { - new RemoveOperationsContactFromPartner(this) - .given("operationsContactPerson", "Dennis Krause") - .doRun(); + @Nested + @Order(31) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class SepaMandateScenarios { + + @Test + @Order(3100) + @Requires("Debitor: D-3101000 - Test AG - main debitor") + @Produces("SEPA-Mandate: Test AG") + void shouldCreateSepaMandateForDebitor() { + new CreateSepaMandateForDebitor(scenarioTest) + // existing debitor + .given("debitorNumber", "D-3101000") + + // new sepa-mandate + .given("mandateReference", "Test AG - main debitor") + .given("mandateAgreement", "2022-10-12") + .given("mandateValidFrom", "2024-10-15") + + // new bank-account + .given("bankAccountHolder", "Test AG - debit bank account") + .given("bankAccountIBAN", "DE02701500000000594937") + .given("bankAccountBIC", "SSKMDEMM") + .doRun() + .keep(); + } + + @Test + @Order(3108) + @Requires("SEPA-Mandate: Test AG") + void shouldInvalidateSepaMandateForDebitor() { + new InvalidateSepaMandateForDebitor(scenarioTest) + .given("bankAccountIBAN", "DE02701500000000594937") + .given("mandateValidUntil", "2025-09-30") + .doRun(); + } + + @Test + @Order(3109) + @Requires("SEPA-Mandate: Test AG") + void shouldFinallyDeleteSepaMandateForDebitor() { + new FinallyDeleteSepaMandateForDebitor(scenarioTest) + .given("bankAccountIBAN", "DE02701500000000594937") + .doRun(); + } } - @Test - @Order(1090) - void shouldDeletePartner() { - new DeletePartner(this) - .given("partnerNumber", "P-31020") - .doRun(); + @Nested + @Order(40) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class MembershipScenarios { + + @Test + @Order(4000) + @Requires("Partner: P-31010 - Test AG") + @Produces("Membership: M-3101000 - Test AG") + void shouldCreateMembershipForPartner() { + new CreateMembership(scenarioTest) + .given("partnerName", "Test AG") + .given("validFrom", "2024-10-15") + .given("newStatus", "ACTIVE") + .given("membershipFeeBillable", "true") + .doRun() + .keep(); + } + + @Test + @Order(4090) + @Requires("Membership: M-3101000 - Test AG") + void shouldCancelMembershipOfPartner() { + new CancelMembership(scenarioTest) + .given("memberNumber", "M-3101000") + .given("validTo", "2025-12-30") + .given("newStatus", "CANCELLED") + .doRun(); + } } - @Test - @Order(1100) - @Requires("Partner: P-31011 - Michelle Matthieu") - void shouldAmendContactData() { - new AmendContactData(this) - .given("partnerName", "Matthieu") - .given("newEmailAddress", "michelle@matthieu.example.org") - .doRun(); + @Nested + @Order(42) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class CoopSharesScenarios { + + @Test + @Order(4201) + @Requires("Membership: M-3101000 - Test AG") + @Produces("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") + void shouldSubscribeCoopShares() { + new CreateCoopSharesSubscriptionTransaction(scenarioTest) + .given("memberNumber", "M-3101000") + .given("reference", "sign 2024-01-15") + .given("shareCount", 100) + .given("comment", "Signing the Membership") + .given("transactionDate", "2024-01-15") + .doRun(); + } + + @Test + @Order(4202) + @Requires("Membership: M-3101000 - Test AG") + void shouldRevertCoopSharesSubscription() { + new CreateCoopSharesRevertTransaction(scenarioTest) + .given("memberNumber", "M-3101000") + .given("comment", "reverting some incorrect transaction") + .given("dateOfIncorrectTransaction", "2024-02-15") + .doRun(); + } + + @Test + @Order(4202) + @Requires("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") + @Produces("Coop-Shares M-3101000 - Test AG - CANCELLATION Transaction") + void shouldCancelCoopSharesSubscription() { + new CreateCoopSharesCancellationTransaction(scenarioTest) + .given("memberNumber", "M-3101000") + .given("reference", "cancel 2024-01-15") + .given("sharesToCancel", 8) + .given("comment", "Cancelling 8 Shares") + .given("transactionDate", "2024-02-15") + .doRun(); + } } - @Test - @Order(1101) - @Requires("Partner: P-31011 - Michelle Matthieu") - void shouldAddPhoneNumberToContactData() { - new AddPhoneNumberToContactData(this) - .given("partnerName", "Matthieu") - .given("phoneNumberKeyToAdd", "mobile") - .given("phoneNumberToAdd", "+49 152 1234567") - .doRun(); + @Nested + @Order(43) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class CoopAssetsScenarios { + + @Test + @Order(4301) + @Requires("Membership: M-3101000 - Test AG") + @Produces("Coop-Assets: M-3101000 - Test AG - DEPOSIT Transaction") + void shouldSubscribeCoopAssets() { + new CreateCoopAssetsDepositTransaction(scenarioTest) + .given("memberNumber", "M-3101000") + .given("reference", "sign 2024-01-15") + .given("assetValue", 100 * 64) + .given("comment", "disposal for initial shares") + .given("transactionDate", "2024-01-15") + .doRun(); + } + + @Test + @Order(4302) + @Requires("Membership: M-3101000 - Test AG") + void shouldRevertCoopAssetsSubscription() { + new CreateCoopAssetsRevertSimpleTransaction(scenarioTest) + .given("memberNumber", "M-3101000") + .given("comment", "reverting some incorrect transaction") + .given("dateOfIncorrectTransaction", "2024-02-15") + .doRun(); + } + + @Test + @Order(4303) + @Requires("Coop-Assets: M-3101000 - Test AG - DEPOSIT Transaction") + @Produces("Coop-Assets: M-3101000 - Test AG - DISBURSAL Transaction") + void shouldDisburseCoopAssets() { + new CreateCoopAssetsDisbursalTransaction(scenarioTest) + .given("memberNumber", "M-3101000") + .given("reference", "cancel 2024-01-15") + .given("valueToDisburse", 8 * 64) + .given("comment", "disbursal according to shares cancellation") + .given("transactionDate", "2024-02-15") + .doRun(); + } + + @Test + @Order(4304) + @Requires("Coop-Assets: M-3101000 - Test AG - DEPOSIT Transaction") + @Produces(explicitly = "Coop-Assets: M-3101000 - Test AG - TRANSFER Transaction", implicitly = "Membership: M-4303000 - New AG") + void shouldTransferCoopAssets() { + new CreateCoopAssetsTransferTransaction(scenarioTest) + .given("transferringMemberNumber", "M-3101000") + .given("adoptingMemberNumber", "M-4303000") + .given("reference", "transfer 2024-12-31") + .given("valueToTransfer", 2 * 64) + .given("comment", "transfer assets from M-3101000 to M-4303000") + .given("transactionDate", "2024-12-31") + .doRun(); + } + + @Test + @Order(4305) + @Requires("Coop-Assets: M-3101000 - Test AG - TRANSFER Transaction") + void shouldRevertCoopAssetsTransferIncludingRelatedAssetAdoption() { + new CreateCoopAssetsRevertTransferTransaction(scenarioTest) + .given("transferringMemberNumber", "M-3101000") + .given("adoptingMemberNumber", "M-4303000") + .given("transferredValue", 2 * 64) + .given("comment", "reverting some incorrect transfer transaction") + .given("dateOfIncorrectTransaction", "2024-02-15") + .doRun(); + } } - @Test - @Order(1102) - @Requires("Partner: P-31011 - Michelle Matthieu") - void shouldRemovePhoneNumberFromContactData() { - new RemovePhoneNumberFromContactData(this) - .given("partnerName", "Matthieu") - .given("phoneNumberKeyToRemove", "office") - .doRun(); - } + @Nested + @Order(50) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class SubscriptionScenarios { - @Test - @Order(1103) - @Requires("Partner: P-31010 - Test AG") - void shouldReplaceContactData() { - new ReplaceContactData(this) - .given("partnerName", "Test AG") - .given("newContactCaption", "Test AG - China") - .given("newPostalAddress", """ - "firm": "Test AG", - "name": "Fi Zhong-Kha", - "building": "Thi Chi Koh Building", - "street": "No.2 Commercial Second Street", - "district": "Niushan Wei Wu", - "city": "Dongguan City", - "province": "Guangdong Province", - "country": "China" - """) - .given("newOfficePhoneNumber", "++15 999 654321") - .given("newEmailAddress", "norden@test-ag.example.org") - .doRun(); - } + @Test + @Order(5000) + @Requires("Person: Test AG") + @Produces("Subscription: Michael Miller to operations-announce") + void shouldSubscribeNewPersonAndContactToMailinglist() { + new SubscribeToMailinglist(scenarioTest) + // 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(1201) - @Requires("Partner: P-31011 - Michelle Matthieu") - void shouldUpdatePersonData() { - new ShouldUpdatePersonData(this) - .given("oldFamilyName", "Matthieu") - .given("newFamilyName", "Matthieu-Zhang") - .doRun(); - } - - @Test - @Order(2010) - @Requires("Partner: P-31010 - Test AG") - @Produces("Debitor: D-3101000 - Test AG - main debitor") - void shouldCreateSelfDebitorForPartner() { - new CreateSelfDebitorForPartner(this) - .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: D-3101001 - Test AG - main debitor") - 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") - @Produces(explicitly = "Debitor: D-3101000 - Test AG - delete debitor", permanent = false) - void shouldDeleteDebitor() { - new DeleteDebitor(this) - .given("partnerNumber", "P-31020") - .given("debitorSuffix", "02") - .doRun(); - } - - @Test - @Order(2020) - @Requires("Debitor: D-3101000 - Test AG - main debitor") - @Disabled("see TODO.spec in DontDeleteDefaultDebitor") - void shouldNotDeleteDefaultDebitor() { - new DontDeleteDefaultDebitor(this) - .given("partnerNumber", "P-31010") - .given("debitorSuffix", "00") - .doRun(); - } - - @Test - @Order(3100) - @Requires("Debitor: D-3101000 - Test AG - main debitor") - @Produces("SEPA-Mandate: Test AG") - void shouldCreateSepaMandateForDebitor() { - new CreateSepaMandateForDebitor(this) - // existing debitor - .given("debitorNumber", "D-3101000") - - // new sepa-mandate - .given("mandateReference", "Test AG - main debitor") - .given("mandateAgreement", "2022-10-12") - .given("mandateValidFrom", "2024-10-15") - - // new bank-account - .given("bankAccountHolder", "Test AG - debit bank account") - .given("bankAccountIBAN", "DE02701500000000594937") - .given("bankAccountBIC", "SSKMDEMM") - .doRun() - .keep(); - } - - @Test - @Order(3108) - @Requires("SEPA-Mandate: Test AG") - void shouldInvalidateSepaMandateForDebitor() { - new InvalidateSepaMandateForDebitor(this) - .given("bankAccountIBAN", "DE02701500000000594937") - .given("mandateValidUntil", "2025-09-30") - .doRun(); - } - - @Test - @Order(3109) - @Requires("SEPA-Mandate: Test AG") - void shouldFinallyDeleteSepaMandateForDebitor() { - new FinallyDeleteSepaMandateForDebitor(this) - .given("bankAccountIBAN", "DE02701500000000594937") - .doRun(); - } - - @Test - @Order(4000) - @Requires("Partner: P-31010 - Test AG") - @Produces("Membership: M-3101000 - Test AG") - void shouldCreateMembershipForPartner() { - new CreateMembership(this) - .given("partnerName", "Test AG") - .given("validFrom", "2024-10-15") - .given("newStatus", "ACTIVE") - .given("membershipFeeBillable", "true") - .doRun() - .keep(); - } - - @Test - @Order(4201) - @Requires("Membership: M-3101000 - Test AG") - @Produces("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") - void shouldSubscribeCoopShares() { - new CreateCoopSharesSubscriptionTransaction(this) - .given("memberNumber", "M-3101000") - .given("reference", "sign 2024-01-15") - .given("shareCount", 100) - .given("comment", "Signing the Membership") - .given("transactionDate", "2024-01-15") - .doRun(); - } - - @Test - @Order(4202) - @Requires("Membership: M-3101000 - Test AG") - void shouldRevertCoopSharesSubscription() { - new CreateCoopSharesRevertTransaction(this) - .given("memberNumber", "M-3101000") - .given("comment", "reverting some incorrect transaction") - .given("dateOfIncorrectTransaction", "2024-02-15") - .doRun(); - } - - @Test - @Order(4202) - @Requires("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") - @Produces("Coop-Shares M-3101000 - Test AG - CANCELLATION Transaction") - void shouldCancelCoopSharesSubscription() { - new CreateCoopSharesCancellationTransaction(this) - .given("memberNumber", "M-3101000") - .given("reference", "cancel 2024-01-15") - .given("sharesToCancel", 8) - .given("comment", "Cancelling 8 Shares") - .given("transactionDate", "2024-02-15") - .doRun(); - } - - @Test - @Order(4301) - @Requires("Membership: M-3101000 - Test AG") - @Produces("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") - void shouldSubscribeCoopAssets() { - new CreateCoopAssetsDepositTransaction(this) - .given("memberNumber", "M-3101000") - .given("reference", "sign 2024-01-15") - .given("assetValue", 100 * 64) - .given("comment", "disposal for initial shares") - .given("transactionDate", "2024-01-15") - .doRun(); - } - - @Test - @Order(4302) - @Requires("Membership: M-3101000 - Test AG") - void shouldRevertCoopAssetsSubscription() { - new CreateCoopAssetsRevertTransaction(this) - .given("memberNumber", "M-3101000") - .given("comment", "reverting some incorrect transaction") - .given("dateOfIncorrectTransaction", "2024-02-15") - .doRun(); - } - - @Test - @Order(4303) - @Requires("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") - @Produces("Coop-Assets M-3101000 - Test AG - DISBURSAL Transaction") - void shouldDisburseCoopAssets() { - new CreateCoopAssetsDisbursalTransaction(this) - .given("memberNumber", "M-3101000") - .given("reference", "cancel 2024-01-15") - .given("valueToDisburse", 8 * 64) - .given("comment", "disbursal according to shares cancellation") - .given("transactionDate", "2024-02-15") - .doRun(); - } - - @Test - @Order(4304) - @Requires("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") - @Produces("Coop-Assets M-3101000 - Test AG - TRANSFER Transaction") - void shouldTransferCoopAssets() { - new CreateCoopAssetsTransferTransaction(this) - .given("transferringMemberNumber", "M-3101000") - .given("adoptingMemberNumber", "M-4303000") - .given("reference", "transfer 2024-12-31") - .given("valueToDisburse", 2 * 64) - .given("comment", "transfer assets from M-3101000 to M-4303000") - .given("transactionDate", "2024-12-31") - .doRun(); - } - - @Test - @Order(4305) - @Requires("Coop-Assets M-3101000 - Test AG - TRANSFER Transaction") - @IgnoreOnFailure("TODO.impl: reverting transfers is not implemented yet") - void shouldRevertCoopAssetsTransfer() { - new CreateCoopAssetsRevertTransaction(this) - .given("memberNumber", "M-3101000") - .given("comment", "reverting some incorrect transfer transaction") - .given("dateOfIncorrectTransaction", "2024-02-15") - .doRun(); - } - - @Test - @Order(4900) - @Requires("Membership: M-3101000 - Test AG") - void shouldCancelMembershipOfPartner() { - new CancelMembership(this) - .given("memberNumber", "M-3101000") - .given("validTo", "2025-12-30") - .given("newStatus", "CANCELLED") - .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(); + @Test + @Order(5001) + @Requires("Subscription: Michael Miller to operations-announce") + void shouldUnsubscribeNewPersonAndContactToMailinglist() { + new UnsubscribeFromMailinglist(scenarioTest) + .given("mailingList", "operations-announce") + .given("subscriberEMailAddress", "michael.miller@example.org") + .doRun(); + } } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/Produces.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/Produces.java deleted file mode 100644 index 5d24fd2c..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/Produces.java +++ /dev/null @@ -1,16 +0,0 @@ -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 {}; - boolean permanent() default true; // false means that the object gets deleted again in the process -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java deleted file mode 100644 index 3e0dab34..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java +++ /dev/null @@ -1,185 +0,0 @@ -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.hs.office.scenarios.TemplateResolver.Resolver; -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 org.testcontainers.shaded.org.apache.commons.lang3.ObjectUtils; - -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 net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; -import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; -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>(Class useCase, UUID uuid) { - - @Override - public String toString() { - return ObjectUtils.toString(uuid); - } - - } - private final static Map> aliases = new HashMap<>(); - - private final static Map 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(); - 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() - ) { - assertThat(producesAnnot.permanent()).as("cannot depend on non-permanent producer: " + potentialProducerMethod); - - // 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 allOf(final String value, final String explicitly, final String[] implicitly) { - final var all = new HashSet(); - 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 resolvedName = resolve(nameWithPlaceholders, DROP_COMMENTS); - final UUID alias = ofNullable(knowVariables().get(resolvedName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null); - assertThat(alias).as("alias '" + resolvedName + "' 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 knowVariables() { - final var map = new LinkedHashMap(); - ScenarioTest.aliases.forEach((key, value) -> map.put(key, value.uuid())); - map.putAll(ScenarioTest.properties); - return map; - } - - public static String resolve(final String text, final Resolver resolver) { - final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve(resolver); - return resolved; - } - - public static Object resolveTyped(final String text) { - final var resolved = resolve(text, DROP_COMMENTS); - try { - return UUID.fromString(resolved); - } catch (final IllegalArgumentException e) { - // ignore and just use the String value - } - return resolved; - } - -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java index 5f8d266d..501a2a6d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AddPhoneNumberToContactData.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios.contact; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java index 0c0e3021..5f05b6a8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/AmendContactData.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios.contact; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java index 715e1832..03a8e07a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/RemovePhoneNumberFromContactData.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios.contact; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java index 2638a4e5..d7a0569a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/contact/ReplaceContactData.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios.contact; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.CREATED; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java index cf525c30..f05a9e47 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateExternalDebitorForPartner.java @@ -1,7 +1,7 @@ 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.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.office.scenarios.person.CreatePerson; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java index 41b40ee6..daa00811 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartner.java @@ -1,7 +1,7 @@ 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.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.CREATED; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java index 549848f1..b50cc7c9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSepaMandateForDebitor.java @@ -1,7 +1,7 @@ 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.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.CREATED; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteDebitor.java index 84e9cd30..2ff0484d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DeleteDebitor.java @@ -1,7 +1,7 @@ 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.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import org.springframework.http.HttpStatus; public class DeleteDebitor extends UseCase { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java index e5459c88..00d7fa79 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/DontDeleteDefaultDebitor.java @@ -1,7 +1,7 @@ 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 net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; public class DontDeleteDefaultDebitor extends UseCase { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java index 224a98f3..7dff8d6a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/FinallyDeleteSepaMandateForDebitor.java @@ -1,7 +1,7 @@ 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.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java index 3af160c7..501e9f06 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/InvalidateSepaMandateForDebitor.java @@ -1,7 +1,7 @@ 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.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.OK; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CancelMembership.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CancelMembership.java index 23f718b5..d5d00900 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CancelMembership.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CancelMembership.java @@ -1,8 +1,8 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership; import io.restassured.http.ContentType; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateMembership.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateMembership.java index 890dfedf..63541e09 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateMembership.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateMembership.java @@ -1,8 +1,8 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership; import io.restassured.http.ContentType; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDepositTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDepositTransaction.java index af9b01a1..7c7a05ad 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDepositTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDepositTransaction.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; public class CreateCoopAssetsDepositTransaction extends CreateCoopAssetsTransaction { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDisbursalTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDisbursalTransaction.java index 542f75d8..e605782f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDisbursalTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDisbursalTransaction.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; public class CreateCoopAssetsDisbursalTransaction extends CreateCoopAssetsTransaction { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertSimpleTransaction.java similarity index 66% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java rename to src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertSimpleTransaction.java index 60d85fbe..45cba19d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertSimpleTransaction.java @@ -1,16 +1,16 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; -public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransaction { +public class CreateCoopAssetsRevertSimpleTransaction extends CreateCoopAssetsTransaction { - public CreateCoopAssetsRevertTransaction(final ScenarioTest testSuite) { + public CreateCoopAssetsRevertSimpleTransaction(final ScenarioTest testSuite) { super(testSuite); requires("CoopAssets-Transaction with incorrect assetValue", alias -> new CreateCoopAssetsDepositTransaction(testSuite) .given("memberNumber", "%{memberNumber}") - .given("reference", "sign %{dateOfIncorrectTransaction}") // same as relatedAssetTx + .given("reference", "sign %{dateOfIncorrectTransaction}") // same text as relatedAssetTx .given("assetValue", 10) .given("comment", "coop-assets deposit transaction with wrong asset value") .given("transactionDate", "%{dateOfIncorrectTransaction}") @@ -21,7 +21,9 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti protected HttpResponse run() { given("transactionType", "REVERSAL"); given("assetValue", -10); + given("reference", "sign %{dateOfIncorrectTransaction}"); // same text as relatedAssetTx given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue")); + given("transactionDate", "%{dateOfIncorrectTransaction}"); return super.run(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransferTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransferTransaction.java new file mode 100644 index 00000000..a1680682 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransferTransaction.java @@ -0,0 +1,59 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; + +import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; +import org.springframework.http.HttpStatus; + +import java.math.BigDecimal; + +import static net.hostsharing.hsadminng.hs.scenarios.ScenarioTest.resolveTyped; + +public class CreateCoopAssetsRevertTransferTransaction extends CreateCoopAssetsTransaction { + + public CreateCoopAssetsRevertTransferTransaction(final ScenarioTest testSuite) { + super(testSuite); + + requires("Accidental CoopAssets-TRANSFER-Transaction", alias -> + new CreateCoopAssetsTransferTransaction(testSuite) + .given("reference", "transfer %{dateOfIncorrectTransaction}") + .given("valueToTransfer", "%{transferredValue}") + .given("comment", "accidental transfer of assets from %{transferringMemberNumber} to %{adoptingMemberNumber}") + .given("transactionDate", "%{dateOfIncorrectTransaction}") + ); + } + + @Override + protected HttpResponse run() { + given("transactionType", "REVERSAL"); + given("assetValue", "%{transferredValue}"); + given("reference", "sign %{dateOfIncorrectTransaction}"); // same text as relatedAssetTx + given("revertedAssetTx", uuid("Accidental CoopAssets-TRANSFER-Transaction")); + given("transactionDate", "%{dateOfIncorrectTransaction}"); + return super.run(); + } + + @Override + protected void verify(final UseCase.HttpResponse response) { + super.verify(response); + + final var revertedAssetTxUuid = response.getFromBody("revertedAssetTx.uuid"); + given("negativeAssetValue", resolveTyped("%{transferredValue}", BigDecimal.class).negate()); + + verify("Verify Reverted Coop-Assets TRANSFER-Transaction", + () -> httpGet("/api/hs/office/coopassetstransactions/" + revertedAssetTxUuid) + .expecting(HttpStatus.OK).expecting(ContentType.JSON), + path("assetValue").contains("%{negativeAssetValue}"), + path("comment").contains("%{comment}"), + path("valueDate").contains("%{transactionDate}") + ); + + final var adoptionAssetTxUuid = response.getFromBody("revertedAssetTx.['adoptionAssetTx.uuid']"); + + verify("Verify Related Coop-Assets ADOPTION-Transaction Also Got Reverted", + () -> httpGet("/api/hs/office/coopassetstransactions/" + adoptionAssetTxUuid) + .expecting(HttpStatus.OK).expecting(ContentType.JSON), + path("reversalAssetTx.['transferAssetTx.uuid']").contains(revertedAssetTxUuid.toString()) + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java index fbe4b8d5..beb78b52 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java @@ -1,8 +1,8 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; import io.restassured.http.ContentType; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransferTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransferTransaction.java index bceecfd8..bf43bd85 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransferTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransferTransaction.java @@ -1,10 +1,10 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership; import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner; -import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransaction { @@ -19,8 +19,8 @@ public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransac .given("emailAddress", "board-of-directors@new-ag.example.org") ); - requires("Membership: New AG", alias -> new CreateMembership(testSuite) - .given("partnerNumber", toPartnerNumber("%{adoptingMemberNumber}")) + requires("Membership: %{adoptingMemberNumber} - New AG", alias -> new CreateMembership(testSuite) + .given("memberNumber", toPartnerNumber("%{adoptingMemberNumber}")) .given("partnerName", "New AG") .given("validFrom", "2024-11-15") .given("newStatus", "ACTIVE") @@ -34,8 +34,7 @@ public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransac given("memberNumber", "%{transferringMemberNumber}"); given("transactionType", "TRANSFER"); - given("assetValue", "-%{valueToDisburse}"); - given("assetValue", "-%{valueToDisburse}"); + given("assetValue", "-%{valueToTransfer}"); return super.run(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesCancellationTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesCancellationTransaction.java index 16549b36..19f02820 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesCancellationTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesCancellationTransaction.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; public class CreateCoopSharesCancellationTransaction extends CreateCoopSharesTransaction { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesRevertTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesRevertTransaction.java index 8464393a..4f2a8555 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesRevertTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesRevertTransaction.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; public class CreateCoopSharesRevertTransaction extends CreateCoopSharesTransaction { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesSubscriptionTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesSubscriptionTransaction.java index 84db07dd..79f79285 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesSubscriptionTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesSubscriptionTransaction.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; public class CreateCoopSharesSubscriptionTransaction extends CreateCoopSharesTransaction { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesTransaction.java index cd8d9c14..a70cbdf5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesTransaction.java @@ -1,8 +1,8 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; import io.restassured.http.ContentType; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java index f862c782..4ea68cba 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddOperationsContactToPartner.java @@ -1,8 +1,8 @@ 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 net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java index 0e4a28c5..41732d3a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/AddRepresentativeToPartner.java @@ -1,8 +1,8 @@ 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 net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java index 3896bbf6..87ca7c87 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/CreatePartner.java @@ -1,8 +1,8 @@ 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 net.hostsharing.hsadminng.hs.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java index 4453d959..df0a0c57 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/DeletePartner.java @@ -1,7 +1,7 @@ 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 net.hostsharing.hsadminng.hs.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import org.springframework.http.HttpStatus; public class DeletePartner extends UseCase { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java index 4a43503c..39e39068 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/CreatePerson.java @@ -1,8 +1,8 @@ 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 net.hostsharing.hsadminng.hs.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import org.springframework.http.HttpStatus; public class CreatePerson extends UseCase { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/ShouldUpdatePersonData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/ShouldUpdatePersonData.java index 04a61c5f..1d092936 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/ShouldUpdatePersonData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/person/ShouldUpdatePersonData.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios.person; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java index 2190e6d9..905c3a72 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/RemoveOperationsContactFromPartner.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios.subscription; -import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.NOT_FOUND; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java index 7cca9ba4..c524fb18 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeToMailinglist.java @@ -1,8 +1,8 @@ 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 net.hostsharing.hsadminng.hs.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java index dfb17ea1..4a6b34cb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/UnsubscribeFromMailinglist.java @@ -1,7 +1,7 @@ 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 net.hostsharing.hsadminng.hs.scenarios.UseCase; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.NO_CONTENT; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java index 7812a9fb..4eccac8e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java @@ -8,6 +8,7 @@ import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountReposi import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -15,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -30,8 +32,9 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCleanup { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/JsonOptional.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/JsonOptional.java new file mode 100644 index 00000000..f5c829c6 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/JsonOptional.java @@ -0,0 +1,44 @@ +package net.hostsharing.hsadminng.hs.scenarios; + + +public final class JsonOptional { + + private final boolean jsonValueGiven; + private final V jsonValue; + + private JsonOptional() { + this.jsonValueGiven = false; + this.jsonValue = null; + } + + private JsonOptional(final V jsonValue) { + this.jsonValueGiven = true; + this.jsonValue = jsonValue; + } + + public static JsonOptional ofValue(final T value) { + return new JsonOptional<>(value); + } + + public static JsonOptional notGiven() { + return new JsonOptional<>(); + } + + public V given() { + if (!jsonValueGiven) { + throw new IllegalStateException("JSON value was not given"); + } + return jsonValue; + } + + public String givenAsString() { + if (jsonValue instanceof Double doubleValue) { + if (doubleValue % 1 == 0) { + return String.valueOf(doubleValue.intValue()); // avoid trailing ".0" + } else { + return doubleValue.toString(); + } + } + return jsonValue == null ? null : jsonValue.toString(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java similarity index 61% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java rename to src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java index 6d3dc2a2..4a51932e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java @@ -1,10 +1,10 @@ -package net.hostsharing.hsadminng.hs.office.scenarios; +package net.hostsharing.hsadminng.hs.scenarios; -import net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse; +import net.hostsharing.hsadminng.hs.scenarios.UseCase.HttpResponse; import java.util.function.Consumer; -import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; import static org.junit.jupiter.api.Assertions.fail; public class PathAssertion { @@ -19,7 +19,7 @@ public class PathAssertion { public Consumer contains(final String resolvableValue) { return response -> { try { - response.path(path).map(this::asString).contains(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)); + response.path(path).isEqualTo(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)); } catch (final AssertionError e) { // without this, the error message is often lacking important context fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" ); @@ -37,15 +37,4 @@ public class PathAssertion { } }; } - - private String asString(final Object value) { - if (value instanceof Double doubleValue) { - if (doubleValue % 1 == 0) { - return String.valueOf(doubleValue.intValue()); // avoid trailing ".0" - } else { - return doubleValue.toString(); - } - } - return value.toString(); - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/Produces.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/Produces.java new file mode 100644 index 00000000..181d6648 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/Produces.java @@ -0,0 +1,46 @@ +package net.hostsharing.hsadminng.hs.scenarios; + +import lombok.experimental.UtilityClass; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.HashSet; +import java.util.Set; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.Arrays.asList; + +@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 {}; + boolean permanent() default true; // false means that the object gets deleted again in the process + + @UtilityClass + final class Aggregator { + + public static Set producedAliases(final Produces producesAnnot) { + return allOf( + producesAnnot.value(), + producesAnnot.explicitly(), + producesAnnot.implicitly()); + } + + + + private Set allOf(final String value, final String explicitly, final String[] implicitly) { + final var all = new HashSet(); + if (!value.isEmpty()) { + all.add(value); + } + if (!explicitly.isEmpty()) { + all.add(explicitly); + } + all.addAll(asList(implicitly)); + return all; + } + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/README.md similarity index 100% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md rename to src/test/java/net/hostsharing/hsadminng/hs/scenarios/README.md diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/Requires.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/Requires.java similarity index 83% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/Requires.java rename to src/test/java/net/hostsharing/hsadminng/hs/scenarios/Requires.java index 59ea21ec..d0a3ed5a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/Requires.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/Requires.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios; +package net.hostsharing.hsadminng.hs.scenarios; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/ScenarioTest.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/ScenarioTest.java new file mode 100644 index 00000000..d1da4bb1 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/ScenarioTest.java @@ -0,0 +1,257 @@ +package net.hostsharing.hsadminng.hs.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 net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver; +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.NotNull; +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.math.BigDecimal; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Stack; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Arrays.stream; +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.scenarios.Produces.Aggregator.producedAliases; +import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +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 + + private final Stack currentTestMethodProduces = new Stack<>(); + + protected ScenarioTest scenarioTest = this; + + Optional takeProducedAlias() { + if (currentTestMethodProduces.isEmpty()) { + return Optional.empty(); + } + return Optional.of(currentTestMethodProduces.pop()); + } + + record Alias>(Class useCase, UUID uuid) { + + @Override + public String toString() { + return Objects.toString(uuid); + } + + } + + private final static Map> aliases = new HashMap<>(); + + private final static Map properties = new HashMap<>(); + public final TestReport testReport = new TestReport(aliases); + + @LocalServerPort + Integer port; + + @Autowired + HsOfficePersonRepository personRepo; + + @Autowired + JpaAttempt jpaAttempt; + + @SneakyThrows + @BeforeEach + void beforeScenario(final TestInfo testInfo) { + createHostsharingPerson(); + try { + testInfo.getTestMethod().ifPresent(currentTestMethod -> { + callRequiredProducers(currentTestMethod); + keepProducesAlias(currentTestMethod); + }); + testReport.createTestLogMarkdownFile(testInfo); + } catch (Exception exc) { + throw exc; + } + } + + @AfterEach + void afterScenario(final TestInfo testInfo) { // final TestInfo testInfo + testInfo.getTestMethod() .ifPresent(currentTestMethod -> { + // FIXME: extract to method + final var producesAnnot = currentTestMethod.getAnnotation(Produces.class); + if (producesAnnot != null && producesAnnot.permanent()) { + final var testMethodProduces = producedAliases(producesAnnot); + testMethodProduces.forEach(declaredAlias -> + assertThat(knowVariables().containsKey(declaredAlias)) + .as("@Producer method " + currentTestMethod.getName() + + " did declare but not produce \"" + declaredAlias + "\"") + .isTrue() ); + } + }); + + properties.clear(); + 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 testMethodRequires = Optional.of(currentTestMethod) + .map(m -> m.getAnnotation(Requires.class)) + .map(Requires::value) + .orElse(null); + if (testMethodRequires != null) { + for (Method potentialProducerMethod : getPotentialProducerMethods()) { + final var producesAnnot = potentialProducerMethod.getAnnotation(Produces.class); + final var testMethodProduces = producedAliases(producesAnnot); + // @formatter:off + if ( // that method can produce something required + testMethodProduces.contains(testMethodRequires) && + + // and it does not produce anything we already have (would cause errors) + SetUtils.intersection(testMethodProduces, knowVariables().keySet()).isEmpty() + ) { + assertThat(producesAnnot.permanent()).as("cannot depend on non-permanent producer: " + potentialProducerMethod); + + // then we recursively produce the pre-requisites of the producer method + callRequiredProducers(potentialProducerMethod); + keepProducesAlias(currentTestMethod); + + // and finally we call the producer method + invokeProducerMethod(this, potentialProducerMethod); + } + // @formatter:on + } + + assertThat(knowVariables().containsKey(testMethodRequires)) + .as("no @Producer for @Required(\"" + testMethodRequires + "\") found") + .isTrue(); + } + } + + private void keepProducesAlias(final Method currentTestMethod) { + final var producesAnnot = currentTestMethod.getAnnotation(Produces.class); + if (producesAnnot != null) { + final var producesAlias = isNotBlank(producesAnnot.value()) ? producesAnnot.value() : producesAnnot.explicitly(); + assertThat(producesAlias) + .as(currentTestMethod.getName() + " must define either value or explicit for @Produces") + .isNotNull(); + this.currentTestMethodProduces.push(producesAlias); + } + } + + private Method @NotNull [] getPotentialProducerMethods() { + final var methodsDeclaredInOuterTestClass = stream(getClass().getDeclaredMethods()) + .filter(m -> m.getAnnotation(Produces.class) != null) + .toArray(Method[]::new); + final var methodsDeclaredInInnerTestClasses = stream(getClass().getDeclaredClasses()) + .map(Class::getDeclaredMethods).flatMap(Stream::of) + .filter(m -> m.getAnnotation(Produces.class) != null) + .toArray(Method[]::new); + return ArrayUtils.addAll(methodsDeclaredInOuterTestClass, methodsDeclaredInInnerTestClasses); + } + + @SneakyThrows + private void invokeProducerMethod(final ScenarioTest scenarioTest, final Method producerMethod) { + producerMethod.setAccessible(true); + if (producerMethod.getDeclaringClass() == scenarioTest.getClass()) { + producerMethod.invoke(this); + } else { + final var innerClassConstructor = producerMethod.getDeclaringClass() + .getDeclaredConstructor(scenarioTest.getClass()); + innerClassConstructor.setAccessible(true); + final var inner = innerClassConstructor.newInstance(this); + producerMethod.invoke(inner); + } + } + + static boolean containsAlias(final String alias) { + return aliases.containsKey(alias); + } + + static UUID uuid(final String nameWithPlaceholders) { + final var resolvedName = resolve(nameWithPlaceholders, DROP_COMMENTS); + final UUID alias = ofNullable(knowVariables().get(resolvedName)).filter(v -> v instanceof UUID) + .map(UUID.class::cast) + .orElse(null); + assertThat(alias).as("alias '" + resolvedName + "' 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 void removeProperty(final String propName) { + properties.remove(propName); + } + + static Map knowVariables() { + final var map = new LinkedHashMap(); + ScenarioTest.aliases.forEach((key, value) -> map.put(key, value.uuid())); + map.putAll(ScenarioTest.properties); + return map; + } + + public static String resolve(final String text, final Resolver resolver) { + final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve(resolver); + return resolved; + } + + public static Object resolveTyped(final String resolvableText) { + final var resolved = resolve(resolvableText, DROP_COMMENTS); + try { + return UUID.fromString(resolved); + } catch (final IllegalArgumentException e) { + // ignore and just use the String value + } + return resolved; + } + + public static T resolveTyped(final String resolvableText, final Class valueType) { + final var resolvedValue = resolve(resolvableText, DROP_COMMENTS); + if (valueType == BigDecimal.class) { + //noinspection unchecked + return (T) new BigDecimal(resolvedValue); + } + //noinspection unchecked + return (T) resolvedValue; + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java similarity index 98% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java rename to src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java index 002d0512..d28adb26 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolver.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios; +package net.hostsharing.hsadminng.hs.scenarios; import org.apache.commons.lang3.StringUtils; @@ -10,7 +10,7 @@ import java.util.Objects; import java.util.regex.Pattern; import java.util.stream.Collectors; -import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; public class TemplateResolver { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolverUnitTest.java similarity index 92% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolverUnitTest.java index 435d44d3..34e2783f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TemplateResolverUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolverUnitTest.java @@ -1,10 +1,10 @@ -package net.hostsharing.hsadminng.hs.office.scenarios; +package net.hostsharing.hsadminng.hs.scenarios; import org.junit.jupiter.api.Test; import java.util.Map; -import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; import static org.assertj.core.api.Assertions.assertThat; class TemplateResolverUnitTest { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TestReport.java similarity index 85% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java rename to src/test/java/net/hostsharing/hsadminng/hs/scenarios/TestReport.java index ec6e0fd6..b700d556 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TestReport.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios; +package net.hostsharing.hsadminng.hs.scenarios; import lombok.SneakyThrows; import net.hostsharing.hsadminng.system.SystemProcess; @@ -12,9 +12,12 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.Map; +import java.util.regex.Pattern; +import static java.lang.String.join; import static org.assertj.core.api.Assertions.assertThat; public class TestReport { @@ -41,9 +44,12 @@ public class TestReport { } public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException { - final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow(); + final var testMethodName = testInfo.getTestMethod().map(Method::getName) + .map(TestReport::chopShouldPrefix) + .map(TestReport::splitMixedCaseIntoSeparateWords) + .orElseThrow(); final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow(); - markdownReportFile = new File(BUILD_DOC_SCENARIOS, testMethodOrder + "-" + testMethodName + ".md"); + markdownReportFile = new File(BUILD_DOC_SCENARIOS, testMethodOrder + ": " + testMethodName + ".md"); markdownReport = new PrintWriter(new FileWriter(markdownReportFile)); print("## Scenario #" + determineScenarioTitle(testInfo)); } @@ -119,6 +125,20 @@ public class TestReport { return result.toString(); } + private static String chopShouldPrefix(final String text) { + return text.replaceAll("^should", ""); + } + + private static String splitMixedCaseIntoSeparateWords(final String text) { + final var WORD_FINDER = Pattern.compile("(([A-Z]?[a-z]+)|([A-Z]))"); + final var matcher = WORD_FINDER.matcher(text); + final var words = new ArrayList(); + while (matcher.find()) { + words.add(matcher.group(0)); + } + return join(" ", words); + } + @SneakyThrows private String currentGitBranch() { try { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCase.java similarity index 87% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java rename to src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCase.java index 27e38e13..7b438134 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCase.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios; +package net.hostsharing.hsadminng.hs.scenarios; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -9,13 +9,14 @@ import lombok.Getter; import lombok.SneakyThrows; import net.hostsharing.hsadminng.reflection.AnnotationFinder; import org.apache.commons.collections4.map.LinkedMap; -import org.assertj.core.api.OptionalAssert; +import org.assertj.core.api.AbstractStringAssert; 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 jakarta.validation.constraints.NotNull; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -27,15 +28,15 @@ import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import static java.net.URLEncoder.encode; -import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; -import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS; +import static java.util.stream.Collectors.joining; +import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS; import static net.hostsharing.hsadminng.test.DebuggerDetection.isDebuggerAttached; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; @@ -70,9 +71,7 @@ public abstract class UseCase> { } public final void requires(final String alias, final Function> useCaseFactory) { - if (!ScenarioTest.containsAlias(alias)) { - requirements.put(alias, useCaseFactory); - } + requirements.put(alias, useCaseFactory); } public final HttpResponse doRun() { @@ -88,13 +87,18 @@ public abstract class UseCase> { testReport.printLine(""); testReport.silent(() -> requirements.forEach((alias, factory) -> { - if (!ScenarioTest.containsAlias(alias)) { - factory.apply(alias).run().keepAs(alias); + final var resolvedAlias = ScenarioTest.resolve(alias, DROP_COMMENTS); + if (!ScenarioTest.containsAlias(resolvedAlias)) { + factory.apply(resolvedAlias).run().keepAs(resolvedAlias); } }) ); final var response = run(); verify(response); + keepInProduceAlias(response); + + resetProperties(); + return response; } @@ -109,7 +113,7 @@ public abstract class UseCase> { } public final UseCase given(final String propName, final Object propValue) { - givenProperties.put(propName, propValue); + givenProperties.put(propName, ScenarioTest.resolve(propValue == null ? null : propValue.toString(), TemplateResolver.Resolver.KEEP_COMMENTS)); ScenarioTest.putProperty(propName, propValue); return this; } @@ -206,7 +210,8 @@ public abstract class UseCase> { return new PathAssertion(path); } - protected void verify( + @SafeVarargs + protected final void verify( final String title, final Supplier http, final Consumer... assertions) { @@ -236,12 +241,24 @@ public abstract class UseCase> { String resolvePlaceholders() { return ScenarioTest.resolve(template, DROP_COMMENTS); } + + } + + private void keepInProduceAlias(final HttpResponse response) { + final var producedAlias = testSuite.takeProducedAlias(); + if (response != null) { + producedAlias.ifPresent(response::keepAs); + } } private static Duration seconds(final int secondsIfNoDebuggerAttached) { return isDebuggerAttached() ? Duration.ofHours(1) : Duration.ofSeconds(secondsIfNoDebuggerAttached); } + private void resetProperties() { + givenProperties.forEach((propName, val) -> ScenarioTest.removeProperty(propName)); + } + public final class HttpResponse { @Getter @@ -319,22 +336,25 @@ public abstract class UseCase> { } @SneakyThrows - public String getFromBody(final String path) { - return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS)); + public V getFromBody(final String path) { + final var body = response.body(); + final var resolvedPath = ScenarioTest.resolve(path, DROP_COMMENTS); + return JsonPath.parse(body).read(resolvedPath); } + @NotNull @SneakyThrows - public Optional getFromBodyAsOptional(final String path) { + public JsonOptional getFromBodyAsOptional(final String path) { try { - return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS))); + return JsonOptional.ofValue(getFromBody(path)); } catch (final PathNotFoundException e) { - return null; // means the property did not exist at all, not that it was there with value null + return JsonOptional.notGiven(); } } @SneakyThrows - public OptionalAssert path(final String path) { - return assertThat(getFromBodyAsOptional(path)); + public AbstractStringAssert path(final String path) { + return assertThat(getFromBodyAsOptional(path).givenAsString()); } @SneakyThrows @@ -396,4 +416,12 @@ public abstract class UseCase> { private String title(String resultAlias) { return getClass().getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1 $2") + " => " + resultAlias; } + + @Override + public String toString() { + final var properties = givenProperties.entrySet().stream() + .map(e -> "\t" + e.getKey() + "=" + e.getValue()) + .collect(joining("\n")); + return getClass().getSimpleName() + "(\n\t" + properties + "\n)"; + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCaseNotImplementedYet.java similarity index 87% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java rename to src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCaseNotImplementedYet.java index d21d6413..98073e65 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCaseNotImplementedYet.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCaseNotImplementedYet.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios; +package net.hostsharing.hsadminng.hs.scenarios; import static org.assertj.core.api.Assumptions.assumeThat; diff --git a/src/test/java/net/hostsharing/hsadminng/lambda/WithNonNullUnitTest.java b/src/test/java/net/hostsharing/hsadminng/lambda/WithNonNullUnitTest.java new file mode 100644 index 00000000..cb947144 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/lambda/WithNonNullUnitTest.java @@ -0,0 +1,30 @@ +package net.hostsharing.hsadminng.lambda; + +import org.junit.jupiter.api.Test; + +import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +class WithNonNullUnitTest { + + Boolean didRun = null; + + @Test + void withNonNullRunsBodyIfNotNull() { + didRun = false; + withNonNull("test", nonNullValue -> { + assertThat(nonNullValue).isEqualTo("test"); + didRun = true; + } ); + assertThat(didRun).isTrue(); + } + + @Test + void withNonNullDoesNotRunBodyIfNull() { + didRun = false; + withNonNull(null, nonNullValue -> { + didRun = true; + } ); + assertThat(didRun).isFalse(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java index ef1c482c..25d04156 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java @@ -1,8 +1,8 @@ package net.hostsharing.hsadminng.rbac.context; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.Array; +import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Test; @@ -13,6 +13,8 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import jakarta.servlet.http.HttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -32,6 +34,9 @@ class ContextIntegrationTests { @Autowired private JpaAttempt jpaAttempt; + @PersistenceContext + private EntityManager em; + @Test void defineWithoutHttpServletRequestUsesCallStack() { @@ -43,7 +48,7 @@ class ContextIntegrationTests { @Test @Transactional - void defineWithcurrentSubjectButWithoutAssumedRoles() { + void defineWithCurrentSubjectButWithoutAssumedRoles() { // when context.define("superuser-alex@hostsharing.net"); @@ -60,7 +65,7 @@ class ContextIntegrationTests { } @Test - void defineWithoutcurrentSubjectButWithAssumedRoles() { + void defineWithoutCurrentSubjectButWithAssumedRoles() { // when final var result = jpaAttempt.transacted(() -> context.define(null, "rbactest.package#yyy00:ADMIN") @@ -73,7 +78,7 @@ class ContextIntegrationTests { } @Test - void defineWithUnknowncurrentSubject() { + void defineWithUnknownCurrentSubject() { // when final var result = jpaAttempt.transacted(() -> context.define("unknown@example.org") @@ -87,7 +92,7 @@ class ContextIntegrationTests { @Test @Transactional - void defineWithcurrentSubjectAndAssumedRoles() { + void defineWithCurrentSubjectAndAssumedRoles() { // given context.define("superuser-alex@hostsharing.net", "rbactest.customer#xxx:OWNER;rbactest.customer#yyy:OWNER"); @@ -102,7 +107,7 @@ class ContextIntegrationTests { } @Test - public void defineContextWithcurrentSubjectAndAssumeInaccessibleRole() { + public void defineContextWithCurrentSubjectAndAssumeInaccessibleRole() { // when final var result = jpaAttempt.transacted(() -> context.define("customer-admin@xxx.example.com", "rbactest.package#yyy00:ADMIN") @@ -113,4 +118,52 @@ class ContextIntegrationTests { jakarta.persistence.PersistenceException.class, "ERROR: [403] subject customer-admin@xxx.example.com has no permission to assume role rbactest.package#yyy00:ADMIN"); } + + @Test + public void hasGlobalAdminRoleIsTrueForGlobalAdminWithoutAssumedRole() { + + final var hsGlobalAdminRole = jpaAttempt.transacted(() -> { + // given + context.define("superuser-alex@hostsharing.net"); + + // when + return (boolean) em.createNativeQuery("select rbac.hasGlobalAdminRole()").getSingleResult(); + } + ); + + // then + assertThat(hsGlobalAdminRole.returnedValue()).isTrue(); + } + + @Test + public void hasGlobalAdminRoleIsTrueForGlobalAdminWithAssumedRole() { + final var hsGlobalAdminRole = jpaAttempt.transacted(() -> { + // given + context.define("superuser-alex@hostsharing.net", "rbactest.package#yyy00:ADMIN"); + + // when + return (boolean) em.createNativeQuery("select rbac.hasGlobalAdminRole()").getSingleResult(); + }); + + // when + + // then + assertThat(hsGlobalAdminRole.returnedValue()).isFalse(); + } + + @Test + public void hasGlobalAdminRoleIsFalseForNonGlobalAdminWithoutAssumedRole() { + + final var hsGlobalAdminRole = jpaAttempt.transacted(() -> { + // given + context.define("customer-admin@xxx.example.com"); + + // when + return (boolean) em.createNativeQuery("select rbac.hasGlobalAdminRole()").getSingleResult(); + } + ); + + // then + assertThat(hsGlobalAdminRole.returnedValue()).isFalse(); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantControllerAcceptanceTest.java index 0dff3e75..db13757e 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantControllerAcceptanceTest.java @@ -10,12 +10,14 @@ import net.hostsharing.hsadminng.rbac.role.RbacRoleRepository; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -31,8 +33,9 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional(readOnly = true, propagation = Propagation.NEVER) class RbacGrantControllerAcceptanceTest extends ContextBasedTest { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerAcceptanceTest.java index 43ff21ab..ac925f2e 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerAcceptanceTest.java @@ -4,17 +4,20 @@ import io.restassured.RestAssured; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = HsadminNgApplication.class + classes = {HsadminNgApplication.class, DisableSecurityConfig.class} ) +@ActiveProfiles("test") class RbacRoleControllerAcceptanceTest { @LocalServerPort diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java index a9f09345..f7507be1 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/role/RbacRoleControllerRestTest.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.role; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; @@ -11,6 +12,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -29,7 +31,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(RbacRoleController.class) -@Import(StandardMapper.class) +@Import({StandardMapper.class, DisableSecurityConfig.class}) +@ActiveProfiles("test") @RunWith(SpringRunner.class) class RbacRoleControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerAcceptanceTest.java index aa1bac97..9392cc6c 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerAcceptanceTest.java @@ -5,11 +5,13 @@ import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.UUID; @@ -19,8 +21,9 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class RbacSubjectControllerAcceptanceTest { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java index f1067753..1b96029b 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectControllerRestTest.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.subject; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -10,6 +11,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -24,7 +26,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(RbacSubjectController.class) -@Import(StandardMapper.class) +@Import({StandardMapper.class, DisableSecurityConfig.class}) +@ActiveProfiles("test") @RunWith(SpringRunner.class) class RbacSubjectControllerRestTest { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonBuilder.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonBuilder.java index 35a29d90..482e20aa 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonBuilder.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonBuilder.java @@ -27,7 +27,7 @@ public class JsonBuilder { * @param value JSON value * @return this JsonBuilder */ - public JsonBuilder with(final String key, final String value) { + public JsonBuilder with(final String key, final Object value) { try { jsonObject.put(key, value); } catch (JSONException e) { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerControllerAcceptanceTest.java index cf61003f..a022003b 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerControllerAcceptanceTest.java @@ -5,6 +5,7 @@ import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -12,6 +13,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; @@ -24,8 +26,9 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = { HsadminNgApplication.class, JpaAttempt.class } + classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class } ) +@ActiveProfiles("test") @Transactional class TestCustomerControllerAcceptanceTest { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageControllerAcceptanceTest.java index b76cc849..2ba2f086 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageControllerAcceptanceTest.java @@ -4,6 +4,7 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.test.DisableSecurityConfig; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -11,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.UUID; @@ -22,8 +24,9 @@ import static org.hamcrest.Matchers.is; @SpringBootTest( webEnvironment = WebEnvironment.RANDOM_PORT, - classes = HsadminNgApplication.class + classes = { HsadminNgApplication.class, DisableSecurityConfig.class } ) +@ActiveProfiles("test") @Transactional class TestPackageControllerAcceptanceTest { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageRepositoryIntegrationTest.java index aea12049..5b45f1b5 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageRepositoryIntegrationTest.java @@ -38,27 +38,29 @@ class TestPackageRepositoryIntegrationTest extends ContextBasedTest { class FindAllByOptionalNameLike { @Test - public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { + public void globalAdmin_withoutAssumedRole_canViewAllPackagesDueToBypassoOfRecursiveCteRbacQuery() { // given - // alex is not just rbac.global-admin but lso the creating user, thus we use fran + // alex is not just rbac.global-admin but also the creating user, thus we use fran context.define("superuser-fran@hostsharing.net"); // when final var result = testPackageRepository.findAllByOptionalNameLike(null); // then - noPackagesAreReturned(result); + + exactlyThesePackagesAreReturned(result, + "xxx00", "xxx01", "xxx02", "yyy00", "yyy01", "yyy02", "zzz00", "zzz01", "zzz02"); } @Test - public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { - given: + public void globalAdmin_withAssumedGlobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { + // given context.define("superuser-alex@hostsharing.net", "rbac.global#global:ADMIN"); // when final var result = testPackageRepository.findAllByOptionalNameLike(null); - then: + // then noPackagesAreReturned(result); } diff --git a/src/test/java/net/hostsharing/hsadminng/test/DisableSecurityConfig.java b/src/test/java/net/hostsharing/hsadminng/test/DisableSecurityConfig.java new file mode 100644 index 00000000..b0def144 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/test/DisableSecurityConfig.java @@ -0,0 +1,19 @@ +package net.hostsharing.hsadminng.test; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@TestConfiguration +public class DisableSecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .csrf(AbstractHttpConfigurer::disable); + return http.build(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/test/TestUuidGenerator.java b/src/test/java/net/hostsharing/hsadminng/test/TestUuidGenerator.java index b75424c1..e0737bb9 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/TestUuidGenerator.java +++ b/src/test/java/net/hostsharing/hsadminng/test/TestUuidGenerator.java @@ -63,6 +63,9 @@ public class TestUuidGenerator { * @return a constant UUID related to the given index */ public static UUID use(final int index) { + if (staticallyUsedIndexes.contains(index)) { + throw new IllegalArgumentException("index " + index + " already used statically"); + } staticallyUsedIndexes.add(index); return GIVEN_UUIDS.get(index); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 54141c3e..a365daf3 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,3 +1,13 @@ + +management: + server: + port: 8081 + address: 127.0.0.1 + endpoints: + web: + exposure: + include: info, health, metrics + spring: sql: init: