Merge branch 'master' into TP-20241126-znuny-view-customer_company

This commit is contained in:
Dev und Test fuer hsadminng 2024-12-05 10:22:09 +01:00
commit 164209f78e
95 changed files with 2152 additions and 867 deletions

5
Jenkinsfile vendored
View File

@ -76,7 +76,10 @@ pipeline {
sh ''' sh '''
./gradlew convertMarkdownToHtml ./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 // cleanup workspace
cleanWs() cleanWs()

View File

@ -60,36 +60,40 @@ If you have at least Docker and the Java JDK installed in appropriate versions a
cd your-hsadmin-ng-directory cd your-hsadmin-ng-directory
source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew' source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew'
gw # initially downloads the configured Gradle version into the project 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: # if the container has not been built yet, run this:
pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432 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 pg-sql-start
gw bootRun # compiles and runs the application on localhost:8080 gw bootRun # compiles and runs the application on localhost:8080
# the following command should reply with "pong": # 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: # the following command should return a JSON array with just all customers:
curl \ curl -f\
-H 'current-subject: superuser-alex@hostsharing.net' \ -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: # 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' \ -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 # add a new customer
curl \ curl -f\
-H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \ -H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \
-d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \ -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: 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. Mike and Sven are just example global admin accounts as part of the example data which is automatically inserted in Testcontainers and Development environments.

View File

@ -58,6 +58,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation' 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 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2'
implementation 'org.springdoc:springdoc-openapi:2.6.0' implementation 'org.springdoc:springdoc-openapi:2.6.0'
implementation 'org.postgresql:postgresql:42.7.4' implementation 'org.postgresql:postgresql:42.7.4'

View File

@ -43,6 +43,11 @@
{ "moduleLicense": "WTFPL" }, { "moduleLicense": "WTFPL" },
{
"moduleLicense": "Public Domain, per Creative Commons CC0",
"moduleVersion": "2.0.3"
},
{ {
"moduleLicense": null, "moduleLicense": null,
"#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE", "#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE",

View File

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

View File

@ -27,11 +27,13 @@ import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import static java.util.Optional.ofNullable; 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.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.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.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.LOSS;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.TRANSFER; import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull;
@RestController @RestController
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
@ -66,7 +68,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
fromValueDate, fromValueDate,
toValueDate); 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); return ResponseEntity.ok(resources);
} }
@ -106,7 +111,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
if (result.isEmpty()) { if (result.isEmpty()) {
return ResponseEntity.notFound().build(); 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( private static void validateCreditTransaction(
final HsOfficeCoopAssetsTransactionInsertResource requestBody, final HsOfficeCoopAssetsTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> 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) { && requestBody.getAssetValue().signum() > 0) {
violations.add("for %s, assetValue must be negative but is \"%.2f\"".formatted( violations.add("for %s, assetValue must be negative but is \"%.2f\"".formatted(
requestBody.getTransactionType(), requestBody.getAssetValue())); 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<HsOfficeCoopAssetsTransactionEntity, HsOfficeCoopAssetsTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { final BiConsumer<HsOfficeCoopAssetsTransactionEntity, HsOfficeCoopAssetsTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setMembershipUuid(entity.getMembership().getUuid()); resource.setMembershipUuid(entity.getMembership().getUuid());
resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber()); resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber());
if (entity.getReversalAssetTx() != null) { withNonNull(
resource.getReversalAssetTx().setRevertedAssetTxUuid(entity.getUuid()); resource.getReversalAssetTx(), reversalAssetTxResource -> {
resource.getReversalAssetTx().setMembershipUuid(entity.getMembership().getUuid()); reversalAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
resource.getReversalAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber()); 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) { withNonNull(
resource.getRevertedAssetTx().setReversalAssetTxUuid(entity.getUuid()); resource.getRevertedAssetTx(), revertAssetTxResource -> {
resource.getRevertedAssetTx().setMembershipUuid(entity.getMembership().getUuid()); revertAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
resource.getRevertedAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber()); 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) { withNonNull(
resource.getAdoptionAssetTx().setTransferAssetTxUuid(entity.getUuid()); resource.getAdoptionAssetTx(), adoptionAssetTxResource -> {
resource.getAdoptionAssetTx().setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid()); adoptionAssetTxResource.setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid());
resource.getAdoptionAssetTx().setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber()); adoptionAssetTxResource.setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber());
} adoptionAssetTxResource.setTransferAssetTxUuid(entity.getUuid());
withNonNull(
entity.getAdoptionAssetTx().getReversalAssetTx(), reversalAssetTx ->
adoptionAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
});
if (entity.getTransferAssetTx() != null) { withNonNull(
resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid()); resource.getTransferAssetTx(), transferAssetTxResource -> {
resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid()); resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid());
resource.getTransferAssetTx().setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber()); 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<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if (resource.getMembershipUuid() != null) { if (resource.getMembershipUuid() != null) {
final HsOfficeMembershipEntity membership = ofNullable(emw.find(HsOfficeMembershipEntity.class, resource.getMembershipUuid())) final HsOfficeMembershipEntity membership = ofNullable(emw.find(
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] membership.uuid %s not found".formatted( HsOfficeMembershipEntity.class,
resource.getMembershipUuid()))
.orElseThrow(() -> new EntityNotFoundException("membership.uuid %s not found".formatted(
resource.getMembershipUuid()))); resource.getMembershipUuid())));
entity.setMembership(membership); 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()) 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()))); resource.getRevertedAssetTxUuid())));
revertedAssetTx.setReversalAssetTx(entity);
entity.setRevertedAssetTx(revertedAssetTx); entity.setRevertedAssetTx(revertedAssetTx);
if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) { if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) {
throw new ValidationException("given assetValue=" + resource.getAssetValue() + throw new ValidationException("given assetValue=" + resource.getAssetValue() +
" but must be negative value from reverted asset tx: " + revertedAssetTx.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 (resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER) {
if (adoptingMembership != null) { final var adoptingMembership = determineAdoptingMembership(resource);
final var adoptingAssetTx = coopAssetsTransactionRepo.save(createAdoptingAssetTx(entity, adoptingMembership)); 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); entity.setAdoptionAssetTx(adoptingAssetTx);
} }
}; };
@ -206,11 +267,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
final var adoptingMembershipUuid = resource.getAdoptingMembershipUuid(); final var adoptingMembershipUuid = resource.getAdoptingMembershipUuid();
final var adoptingMembershipMemberNumber = resource.getAdoptingMembershipMemberNumber(); final var adoptingMembershipMemberNumber = resource.getAdoptingMembershipMemberNumber();
if (adoptingMembershipUuid != null && adoptingMembershipMemberNumber != null) { if (adoptingMembershipUuid != null && adoptingMembershipMemberNumber != null) {
throw new IllegalArgumentException( throw new ValidationException(
// @formatter:off // @formatter:off
resource.getTransactionType() == TRANSFER resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER
? "[400] either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both" ? "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both"
: "[400] adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType=" : "adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType="
+ resource.getTransactionType()); + resource.getTransactionType());
// @formatter:on // @formatter:on
} }
@ -232,13 +293,9 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
+ "' not found or not accessible"); + "' not found or not accessible");
} }
if (resource.getTransactionType() == TRANSFER) { throw new ValidationException(
throw new ValidationException( "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType="
"either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=" + HsOfficeCoopAssetsTransactionTypeResource.TRANSFER);
+ TRANSFER);
}
return null;
} }
private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx( private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx(

View File

@ -98,21 +98,21 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
private String comment; private String comment;
// Optionally, the UUID of the corresponding transaction for a reversal transaction. // 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") @JoinColumn(name = "revertedassettxuuid")
private HsOfficeCoopAssetsTransactionEntity revertedAssetTx; private HsOfficeCoopAssetsTransactionEntity revertedAssetTx;
// and the other way around // and the other way around
@OneToOne(mappedBy = "revertedAssetTx") @OneToOne(mappedBy = "revertedAssetTx", cascade = CascadeType.PERSIST)
private HsOfficeCoopAssetsTransactionEntity reversalAssetTx; private HsOfficeCoopAssetsTransactionEntity reversalAssetTx;
// Optionally, the UUID of the corresponding transaction for a transfer transaction. // 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") @JoinColumn(name = "assetadoptiontxuuid")
private HsOfficeCoopAssetsTransactionEntity adoptionAssetTx; private HsOfficeCoopAssetsTransactionEntity adoptionAssetTx;
// and the other way around // and the other way around
@OneToOne(mappedBy = "adoptionAssetTx") @OneToOne(mappedBy = "adoptionAssetTx", cascade = CascadeType.PERSIST)
private HsOfficeCoopAssetsTransactionEntity transferAssetTx; private HsOfficeCoopAssetsTransactionEntity transferAssetTx;
@Override @Override

View File

@ -0,0 +1,11 @@
package net.hostsharing.hsadminng.lambda;
import java.util.function.Consumer;
public class WithNonNull {
public static <T> void withNonNull(final T target, final Consumer<T> code) {
if (target != null ) {
code.accept(target);
}
}
}

View File

@ -1,6 +1,15 @@
server: server:
port : 8080 port : 8080
management:
server:
port: 8081
address: 127.0.0.1
endpoints:
web:
exposure:
include: info, health, metrics
spring: spring:
datasource: datasource:

View File

@ -46,15 +46,15 @@ create or replace function rbac.hasGlobalAdminRole()
stable -- leakproof stable -- leakproof
language plpgsql as $$ language plpgsql as $$
declare declare
currentSubjectOrAssumedRolesUuids text; assumedRoles text;
begin begin
begin begin
currentSubjectOrAssumedRolesUuids := current_setting('hsadminng.currentSubjectOrAssumedRolesUuids'); assumedRoles := current_setting('hsadminng.assumedRoles');
exception exception
when others then when others then
currentSubjectOrAssumedRolesUuids := null; assumedRoles := null;
end; end;
return currentSubjectOrAssumedRolesUuids is null or length(currentSubjectOrAssumedRolesUuids) = 0; return TRIM(COALESCE(assumedRoles, '')) = '' and rbac.isGlobalAdmin();
end; $$; end; $$;
--// --//

View File

@ -35,21 +35,41 @@ create table if not exists hs_office.coopassettx
--changeset michael.hoennig:hs-office-coopassets-BUSINESS-RULES endDelimiter:--// --changeset michael.hoennig:hs-office-coopassets-BUSINESS-RULES endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
alter table hs_office.coopassettx -- Not as CHECK constraints because those cannot be deferrable,
add constraint reversal_asset_tx_must_have_reverted_asset_tx -- but we need these constraints deferrable because the rows are linked to each other.
check (transactionType <> 'REVERSAL' or revertedAssetTxUuid is not null);
alter table hs_office.coopassettx CREATE OR REPLACE FUNCTION validate_transaction_type()
add constraint non_reversal_asset_tx_must_not_have_reverted_asset_tx RETURNS TRIGGER AS $$
check (transactionType = 'REVERSAL' or revertedAssetTxUuid is null or transactionType = 'REVERSAL'); 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 -- Non-REVERSAL transactions must not have revertedAssetTxUuid
add constraint transfer_asset_tx_must_have_adopted_asset_tx IF NEW.transactionType != 'REVERSAL' AND NEW.revertedAssetTxUuid IS NOT NULL THEN
check (transactionType <> 'TRANSFER' or assetAdoptionTxUuid is not null); 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);
--// --//
-- ============================================================================ -- ============================================================================

View File

@ -68,6 +68,7 @@ public class ArchitectureTest {
"..hs.hosting.asset", "..hs.hosting.asset",
"..hs.hosting.asset.validators", "..hs.hosting.asset.validators",
"..hs.hosting.asset.factories", "..hs.hosting.asset.factories",
"..hs.scenarios",
"..errors", "..errors",
"..mapper", "..mapper",
"..ping", "..ping",
@ -160,9 +161,11 @@ public class ArchitectureTest {
.that().resideInAPackage("..hs.office.(*)..") .that().resideInAPackage("..hs.office.(*)..")
.should().onlyBeAccessed().byClassesThat() .should().onlyBeAccessed().byClassesThat()
.resideInAnyPackage( .resideInAnyPackage(
"..hs.office.(*)..",
"..hs.office.(*)..", "..hs.office.(*)..",
"..hs.booking.(*)..", "..hs.booking.(*)..",
"..hs.hosting.(*)..", "..hs.hosting.(*)..",
"..hs.scenarios",
"..hs.migration", "..hs.migration",
"..rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest "..rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest
); );

View File

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

View File

@ -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.hs.hosting.asset.validators.Dns;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.ClassOrderer; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate; import java.time.LocalDate;
@ -42,8 +44,9 @@ import static org.hamcrest.Matchers.matchesRegex;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
@TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems @TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems
class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsBookingItemController.class) @WebMvcTest(HsBookingItemController.class)
@Import({StrictMapper.class, JsonObjectMapperConfiguration.class}) @Import({StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class})
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
class HsBookingItemControllerRestTest { class HsBookingItemControllerRestTest {

View File

@ -6,11 +6,13 @@ import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository; import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -23,8 +25,9 @@ import static org.hamcrest.Matchers.matchesRegex;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -14,6 +14,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap; import java.util.HashMap;
@ -43,8 +45,9 @@ import static org.hamcrest.Matchers.matchesRegex;
@Transactional @Transactional
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 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 @TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems
class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository;
import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.mapper.Array;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsHostingAssetController.class) @WebMvcTest(HsHostingAssetController.class)
@Import({ StandardMapper.class, JsonObjectMapperConfiguration.class}) @Import({ StandardMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class })
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
public class HsHostingAssetControllerRestTest { public class HsHostingAssetControllerRestTest {

View File

@ -3,16 +3,19 @@ package net.hostsharing.hsadminng.hs.hosting.asset;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
class HsHostingAssetPropsControllerAcceptanceTest { class HsHostingAssetPropsControllerAcceptanceTest {
@LocalServerPort @LocalServerPort

View File

@ -880,7 +880,6 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction); coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction);
}); });
coopAssets.entrySet().forEach(entry -> { coopAssets.entrySet().forEach(entry -> {
final var legacyId = entry.getKey(); final var legacyId = entry.getKey();
final var assetTransaction = entry.getValue(); final var assetTransaction = entry.getValue();

View File

@ -6,12 +6,14 @@ import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.json.JSONException; import org.json.JSONException;
import org.junit.jupiter.api.*; import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -26,8 +28,9 @@ import static org.hamcrest.Matchers.startsWith;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -2,12 +2,15 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficeBankAccountController.class) @WebMvcTest(HsOfficeBankAccountController.class)
@Import(DisableSecurityConfig.class)
@ActiveProfiles("test")
class HsOfficeBankAccountControllerRestTest { class HsOfficeBankAccountControllerRestTest {
@Autowired @Autowired

View File

@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.json.JSONException; import org.json.JSONException;
import org.junit.jupiter.api.AfterEach; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -33,8 +35,9 @@ import static org.hamcrest.Matchers.startsWith;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
@ -14,6 +15,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -31,8 +33,9 @@ import static org.hamcrest.Matchers.startsWith;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -8,8 +8,10 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.rbac.test.JsonBuilder; import net.hostsharing.hsadminng.rbac.test.JsonBuilder;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import net.hostsharing.hsadminng.test.TestUuidGenerator; import net.hostsharing.hsadminng.test.TestUuidGenerator;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource;
import org.junit.runner.RunWith; 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.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function; 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.JsonBuilder.jsonObject;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; 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.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficeCoopAssetsTransactionController.class) @WebMvcTest(HsOfficeCoopAssetsTransactionController.class)
@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class }) @Import({ StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class })
@ActiveProfiles("test")
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
class HsOfficeCoopAssetsTransactionControllerRestTest { 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 String UNAVAILABLE_MEMBER_NUMBER = "M-1234699";
private static final UUID ORIGIN_MEMBERSHIP_UUID = TestUuidGenerator.use(1); private static final UUID ORIGIN_MEMBERSHIP_UUID = TestUuidGenerator.use(1);
@ -65,9 +83,11 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
.memberNumberSuffix(suffixOf(AVAILABLE_TARGET_MEMBER_NUMBER)) .memberNumberSuffix(suffixOf(AVAILABLE_TARGET_MEMBER_NUMBER))
.build(); .build();
// the following refs might change if impl changes // The following refs depend on the implementation of the respective implementation and might change if it changes.
private static final UUID NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.ref(4); // The same TestUuidGenerator.ref(#) does NOT mean the UUIDs refer to the same entity,
private static final UUID NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.ref(5); // 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); private static final UUID SOME_EXISTING_LOSS_ASSET_TX_UUID = TestUuidGenerator.use(3);
public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_LOSS_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_LOSS_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder()
@ -80,6 +100,402 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
.valueDate(LocalDate.parse("2024-10-15")) .valueDate(LocalDate.parse("2024-10-15"))
.build(); .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 @Autowired
MockMvc mockMvc; MockMvc mockMvc;
@ -117,6 +533,44 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
requestBody -> requestBody.without("membership.uuid"), requestBody -> requestBody.without("membership.uuid"),
"[membershipUuid must not be null but is \"null\"]"), // TODO.impl: should be membership.uuid, Spring validation-problem? "[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( TRANSACTION_TYPE_MISSING(
requestBody -> requestBody.without("transactionType"), requestBody -> requestBody.without("transactionType"),
"[transactionType must not be null but is \"null\"]"), "[transactionType must not be null but is \"null\"]"),
@ -127,35 +581,40 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
ASSETS_VALUE_FOR_DEPOSIT_MUST_BE_POSITIVE( ASSETS_VALUE_FOR_DEPOSIT_MUST_BE_POSITIVE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "DEPOSIT") .with("transactionType", DEPOSIT)
.with("assetValue", -64.00), .with("assetValue", -64.00),
"[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"), "[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"),
ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE( ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "DISBURSAL") .with("transactionType", DISBURSAL)
.with("assetValue", 64.00), .with("assetValue", 64.00),
"[for DISBURSAL, assetValue must be negative but is \"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( ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "TRANSFER") .with("transactionType", TRANSFER)
.with("assetValue", -64.00) .with("assetValue", -64.00)
.with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER), .with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER),
"adoptingMembership.memberNumber='M-1234699' not found or not accessible"), "adoptingMembership.memberNumber='M-1234699' not found or not accessible"),
ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "TRANSFER") .with("transactionType", TRANSFER)
.with("assetValue", -64.00) .with("assetValue", -64.00)
.with("adoptingMembership.uuid", UNAVAILABLE_MEMBERSHIP_UUID.toString()), .with("adoptingMembership.uuid", UNAVAILABLE_UUID),
"adoptingMembership.uuid='" + UNAVAILABLE_MEMBERSHIP_UUID + "' not found or not accessible"), "adoptingMembership.uuid='" + UNAVAILABLE_UUID + "' not found or not accessible"),
ASSETS_VALUE_MUST_NOT_BE_NULL( ASSETS_VALUE_MUST_NOT_BE_NULL(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "REVERSAL") .with("transactionType", REVERSAL)
.with("assetValue", 0.00), .with("assetValue", 0.00),
"[assetValue must not be 0 but is \"0.00\"]"), "[assetValue must not be 0 but is \"0.00\"]"),
@ -190,8 +649,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
@EnumSource(BadRequestTestCases.class) @EnumSource(BadRequestTestCases.class)
void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception { void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception {
// HOWTO: run just a single test-case in a data-driven test-method // HOWTO: run just a single test-case in a data-driven test-method
// org.assertj.core.api.Assumptions.assumeThat( // - set SINGLE_TEST_CASE_EXECUTION to true - see above
// testCase == ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue(); // - 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 // when
mockMvc.perform(MockMvcRequestBuilders mockMvc.perform(MockMvcRequestBuilders
@ -202,9 +663,9 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
.accept(MediaType.APPLICATION_JSON)) .accept(MediaType.APPLICATION_JSON))
// then // then
.andExpect(jsonPath("message", is("ERROR: [400] " + testCase.expectedErrorMessage)))
.andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusCode", is(400)))
.andExpect(jsonPath("statusPhrase", is("Bad Request"))) .andExpect(jsonPath("statusPhrase", is("Bad Request")))
.andExpect(jsonPath("message", is("ERROR: [400] " + testCase.expectedErrorMessage)))
.andExpect(status().is4xxClientError()); .andExpect(status().is4xxClientError());
} }
@ -212,28 +673,38 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
REVERTING_SIMPLE_ASSET_TRANSACTION( REVERTING_SIMPLE_ASSET_TRANSACTION(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "REVERSAL") .with("transactionType", REVERSAL)
.with("assetValue", "64.00") .with("assetValue", "64.00")
.with("valueDate", "2024-10-15") .with("valueDate", "2024-10-15")
.with("reference", "reversal ref") .with("reference", "reversal of loss ref")
.with("comment", "reversal comment") .with("comment", "reversal of loss asset tx comment")
.with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString()), .with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID),
Expected.REVERT_RESPONSE), Expected.REVERT_LOSS_RESPONSE),
TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_NUMBER( TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_NUMBER(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "TRANSFER") .with("transactionType", TRANSFER)
.with("assetValue", -64.00) .with("assetValue", -64.00)
.with("adoptingMembership.memberNumber", AVAILABLE_TARGET_MEMBER_NUMBER), .with("adoptingMembership.memberNumber", AVAILABLE_TARGET_MEMBER_NUMBER),
Expected.TRANSFER_RESPONSE), Expected.TRANSFER_RESPONSE),
TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_UUID( TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_UUID(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "TRANSFER") .with("transactionType", TRANSFER)
.with("assetValue", -64.00) .with("assetValue", -64.00)
.with("membership.uuid", ORIGIN_MEMBERSHIP_UUID.toString()) .with("membership.uuid", ORIGIN_MEMBERSHIP_UUID)
.with("adoptingMembership.uuid", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString()), .with("adoptingMembership.uuid", AVAILABLE_TARGET_MEMBERSHIP_UUID),
Expected.TRANSFER_RESPONSE); 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<JsonBuilder, JsonBuilder> givenBodyTransformation; private final Function<JsonBuilder, JsonBuilder> givenBodyTransformation;
private final String expectedResponseBody; private final String expectedResponseBody;
@ -251,7 +722,7 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
private static class Expected { 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}", "uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}",
"membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}",
@ -259,9 +730,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
"transactionType": "REVERSAL", "transactionType": "REVERSAL",
"assetValue": 64.00, "assetValue": 64.00,
"valueDate": "2024-10-15", "valueDate": "2024-10-15",
"reference": "reversal ref", "reference": "reversal of loss ref",
"comment": "reversal comment", "comment": "reversal of loss asset tx comment",
"adoptionAssetTx": null, "adoptionAssetTx": null,
"reversalAssetTx": null,
"transferAssetTx": null, "transferAssetTx": null,
"revertedAssetTx": { "revertedAssetTx": {
"uuid": "%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", "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_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString())
.replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER) .replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER)
.replace("%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString()); .replace("%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString());
@ -303,20 +777,57 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
"reversalAssetTx": null "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_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString())
.replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER) .replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER)
.replace("%{AVAILABLE_MEMBERSHIP_UUID}", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString()) .replace("%{AVAILABLE_MEMBERSHIP_UUID}", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString())
.replace("%{AVAILABLE_TARGET_MEMBER_NUMBER}", AVAILABLE_TARGET_MEMBER_NUMBER); .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 @ParameterizedTest
@EnumSource(SuccessfullyCreatedTestCases.class) @EnumSource(SuccessfullyCreatedTestCases.class)
void respondWithSuccessfullyCreated(final SuccessfullyCreatedTestCases testCase) throws Exception { void respondWithSuccessfullyCreated(final SuccessfullyCreatedTestCases testCase) throws Exception {
// uncomment, if you need to run just a single test-case in this data-driven test-method assumeThat(!SINGLE_TEST_CASE_EXECUTION ||
// org.assertj.core.api.Assumptions.assumeThat( testCase == SuccessfullyCreatedTestCases.REVERTING_TRANSFER_ASSET_TRANSACTION_IMPLICITLY_REVERTS_ADOPTING_ASSET_TRANSACTION).isTrue();
// testCase == ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue();
// when // when
mockMvc.perform(MockMvcRequestBuilders mockMvc.perform(MockMvcRequestBuilders
@ -331,12 +842,77 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
.andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponseBody))); .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 @BeforeEach
void initMocks() { 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(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())); final var availableMemberNumber = Integer.valueOf(AVAILABLE_TARGET_MEMBER_NUMBER.substring("M-".length()));
when(membershipRepo.findMembershipByMemberNumber(eq(availableMemberNumber))).thenReturn(AVAILABLE_MEMBER_ENTITY); when(membershipRepo.findMembershipByMemberNumber(eq(availableMemberNumber))).thenReturn(AVAILABLE_MEMBER_ENTITY);
@ -346,6 +922,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_LOSS_ASSET_TX_UUID)) when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_LOSS_ASSET_TX_UUID))
.thenReturn(Optional.of(SOME_EXISTING_LOSS_ASSET_TX_ENTITY)); .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))) when(coopAssetsTransactionRepo.save(any(HsOfficeCoopAssetsTransactionEntity.class)))
.thenAnswer(invocation -> { .thenAnswer(invocation -> {
final var entity = (HsOfficeCoopAssetsTransactionEntity) invocation.getArgument(0); final var entity = (HsOfficeCoopAssetsTransactionEntity) invocation.getArgument(0);
@ -358,10 +938,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
} }
private int partnerNumberOf(final String memberNumber) { 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) { private String suffixOf(final String memberNumber) {
return memberNumber.substring("M-".length()+5); return memberNumber.substring("M-".length() + 5);
} }
} }

View File

@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
@ -14,6 +15,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; 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.hasSize;
import static org.hamcrest.Matchers.startsWith; 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 @Transactional
class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -3,12 +3,15 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.rbac.test.JsonBuilder; import net.hostsharing.hsadminng.rbac.test.JsonBuilder;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficeCoopSharesTransactionController.class) @WebMvcTest(HsOfficeCoopSharesTransactionController.class)
@Import(DisableSecurityConfig.class)
@ActiveProfiles("test")
class HsOfficeCoopSharesTransactionControllerRestTest { class HsOfficeCoopSharesTransactionControllerRestTest {
@Autowired @Autowired

View File

@ -12,6 +12,7 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
@ -19,6 +20,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -33,8 +35,9 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -8,6 +8,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.json.JSONException; import org.json.JSONException;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Nested; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -31,8 +33,9 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransact
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; 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.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficeMembershipController.class) @WebMvcTest(HsOfficeMembershipController.class)
@Import(StandardMapper.class) @Import({StandardMapper.class, DisableSecurityConfig.class})
@ActiveProfiles("test")
public class HsOfficeMembershipControllerRestTest { public class HsOfficeMembershipControllerRestTest {
@Autowired @Autowired

View File

@ -13,10 +13,12 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealReposito
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.*; import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.UUID; import java.util.UUID;
@ -29,8 +31,9 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanup {
private static final UUID GIVEN_NON_EXISTING_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); private static final UUID GIVEN_NON_EXISTING_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");

View File

@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; 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.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficePartnerController.class) @WebMvcTest(HsOfficePartnerController.class)
@Import(StandardMapper.class) @Import({StandardMapper.class, DisableSecurityConfig.class})
@ActiveProfiles("test")
class HsOfficePartnerControllerRestTest { class HsOfficePartnerControllerRestTest {
static final UUID GIVEN_MANDANTE_UUID = UUID.randomUUID(); static final UUID GIVEN_MANDANTE_UUID = UUID.randomUUID();

View File

@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Nested; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -26,8 +28,9 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup {
@LocalServerPort @LocalServerPort

View File

@ -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.generated.api.v1.model.HsOfficeRelationTypeResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.UUID; import java.util.UUID;
@ -26,8 +28,9 @@ import static org.hamcrest.Matchers.startsWith;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -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.CreateMembership;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDepositTransaction; 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.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.coopassets.CreateCoopAssetsTransferTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesCancellationTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesCancellationTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction; 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.RemoveOperationsContactFromPartner;
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeToMailinglist; import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeToMailinglist;
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist; 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.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.IgnoreOnFailure; import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension; import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles;
@Tag("scenarioTest") @Tag("scenarioTest")
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class }, classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class },
properties = { properties = {
"spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///scenariosTC}", "spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///scenariosTC}",
"spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}", "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}" "hsadminng.superuser=${HSADMINNG_SUPERUSER:superuser-alex@hostsharing.net}"
} }
) )
@DirtiesContext @ActiveProfiles("test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestClassOrder(ClassOrderer.OrderAnnotation.class)
@ExtendWith(IgnoreOnFailureExtension.class) @ExtendWith(IgnoreOnFailureExtension.class)
class HsOfficeScenarioTests extends ScenarioTest { class HsOfficeScenarioTests extends ScenarioTest {
@Test @Nested
@Order(1010) @Order(10)
@Produces(explicitly = "Partner: P-31010 - Test AG", implicitly = { "Person: Test AG", "Contact: Test AG - Hamburg" }) @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
void shouldCreateLegalPersonAsPartner() { class PartnerScenarios {
new CreatePartner(this)
.given("partnerNumber", "P-31010") @Test
.given("personType", "LEGAL_PERSON") @Order(1010)
.given("tradeName", "Test AG") @Produces(explicitly = "Partner: P-31010 - Test AG", implicitly = { "Person: Test AG", "Contact: Test AG - Hamburg" })
.given("contactCaption", "Test AG - Hamburg") void shouldCreateLegalPersonAsPartner() {
.given("postalAddress", """ new CreatePartner(scenarioTest)
"firm": "Test AG", .given("partnerNumber", "P-31010")
"street": "Shanghai-Allee 1", .given("personType", "LEGAL_PERSON")
"zipcode": "20123", .given("tradeName", "Test AG")
"city": "Hamburg", .given("contactCaption", "Test AG - Hamburg")
"country": "Germany" .given(
""") "postalAddress", """
.given("officePhoneNumber", "+49 40 654321-0") "firm": "Test AG",
.given("emailAddress", "hamburg@test-ag.example.org") "street": "Shanghai-Allee 1",
.doRun() "zipcode": "20123",
.keep(); "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 @Nested
@Order(1011) @Order(11)
@Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
implicitly = { "Person: Michelle Matthieu", "Contact: Michelle Matthieu" }) class PartnerContactScenarios {
void shouldCreateNaturalPersonAsPartner() {
new CreatePartner(this) @Test
.given("partnerNumber", "P-31011") @Order(1100)
.given("personType", "NATURAL_PERSON") @Requires("Partner: P-31011 - Michelle Matthieu")
.given("givenName", "Michelle") void shouldAmendContactData() {
.given("familyName", "Matthieu") new AmendContactData(scenarioTest)
.given("contactCaption", "Michelle Matthieu") .given("partnerName", "Matthieu")
.given("postalAddress", """ .given("newEmailAddress", "michelle@matthieu.example.org")
"name": "Michelle Matthieu", .doRun();
"street": "An der Wandse 34", }
"zipcode": "22123",
"city": "Hamburg", @Test
"country": "Germany" @Order(1101)
""") @Requires("Partner: P-31011 - Michelle Matthieu")
.given("officePhoneNumber", "+49 40 123456") void shouldAddPhoneNumberToContactData() {
.given("emailAddress", "michelle.matthieu@example.org") new AddPhoneNumberToContactData(scenarioTest)
.doRun() .given("partnerName", "Matthieu")
.keep(); .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 @Nested
@Order(1020) @Order(12)
@Requires("Person: Test AG") @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Produces("Representative: Tracy Trust for Test AG") class PartnerPersonScenarios {
void shouldAddRepresentativeToPartner() {
new AddRepresentativeToPartner(this) @Test
.given("partnerPersonTradeName", "Test AG") @Order(1201)
.given("representativeFamilyName", "Trust") @Requires("Partner: P-31011 - Michelle Matthieu")
.given("representativeGivenName", "Tracy") void shouldUpdatePersonData() {
.given("representativePostalAddress", """ new ShouldUpdatePersonData(scenarioTest)
"name": "Michelle Matthieu", .given("oldFamilyName", "Matthieu")
"street": "An der Alster 100", .given("newFamilyName", "Matthieu-Zhang")
"zipcode": "20000", .doRun();
"city": "Hamburg", }
"country": "Germany"
""")
.given("representativePhoneNumber", "+49 40 123456")
.given("representativeEMailAddress", "tracy.trust@example.org")
.doRun()
.keep();
} }
@Test @Nested
@Order(1030) @Order(20)
@Requires("Person: Test AG") @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Produces("Operations-Contact: Dennis Krause for Test AG") class DebitorScenarios {
void shouldAddOperationsContactToPartner() { @Test
new AddOperationsContactToPartner(this) @Order(2010)
.given("partnerPersonTradeName", "Test AG") @Requires("Partner: P-31010 - Test AG")
.given("operationsContactFamilyName", "Krause") @Produces("Debitor: D-3101000 - Test AG - main debitor")
.given("operationsContactGivenName", "Dennis") void shouldCreateSelfDebitorForPartner() {
.given("operationsContactPhoneNumber", "+49 9932 587741") new CreateSelfDebitorForPartner(scenarioTest)
.given("operationsContactEMailAddress", "dennis.krause@example.org") .given("partnerPersonTradeName", "Test AG")
.doRun() .given("billingContactCaption", "Test AG - billing department")
.keep(); .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 @Nested
@Order(1039) @Order(31)
@Requires("Operations-Contact: Dennis Krause for Test AG") @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
void shouldRemoveOperationsContactFromPartner() { class SepaMandateScenarios {
new RemoveOperationsContactFromPartner(this)
.given("operationsContactPerson", "Dennis Krause") @Test
.doRun(); @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 @Nested
@Order(1090) @Order(40)
void shouldDeletePartner() { @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
new DeletePartner(this) class MembershipScenarios {
.given("partnerNumber", "P-31020")
.doRun(); @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 @Nested
@Order(1100) @Order(42)
@Requires("Partner: P-31011 - Michelle Matthieu") @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
void shouldAmendContactData() { class CoopSharesScenarios {
new AmendContactData(this)
.given("partnerName", "Matthieu") @Test
.given("newEmailAddress", "michelle@matthieu.example.org") @Order(4201)
.doRun(); @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 @Nested
@Order(1101) @Order(43)
@Requires("Partner: P-31011 - Michelle Matthieu") @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
void shouldAddPhoneNumberToContactData() { class CoopAssetsScenarios {
new AddPhoneNumberToContactData(this)
.given("partnerName", "Matthieu") @Test
.given("phoneNumberKeyToAdd", "mobile") @Order(4301)
.given("phoneNumberToAdd", "+49 152 1234567") @Requires("Membership: M-3101000 - Test AG")
.doRun(); @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 @Nested
@Order(1102) @Order(50)
@Requires("Partner: P-31011 - Michelle Matthieu") @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
void shouldRemovePhoneNumberFromContactData() { class SubscriptionScenarios {
new RemovePhoneNumberFromContactData(this)
.given("partnerName", "Matthieu")
.given("phoneNumberKeyToRemove", "office")
.doRun();
}
@Test @Test
@Order(1103) @Order(5000)
@Requires("Partner: P-31010 - Test AG") @Requires("Person: Test AG")
void shouldReplaceContactData() { @Produces("Subscription: Michael Miller to operations-announce")
new ReplaceContactData(this) void shouldSubscribeNewPersonAndContactToMailinglist() {
.given("partnerName", "Test AG") new SubscribeToMailinglist(scenarioTest)
.given("newContactCaption", "Test AG - China") // TODO.spec: do we need the personType? or is an operational contact always a natural person? what about distribution lists?
.given("newPostalAddress", """ .given("partnerPersonTradeName", "Test AG")
"firm": "Test AG", .given("subscriberFamilyName", "Miller")
"name": "Fi Zhong-Kha", .given("subscriberGivenName", "Michael")
"building": "Thi Chi Koh Building", .given("subscriberEMailAddress", "michael.miller@example.org")
"street": "No.2 Commercial Second Street", .given("mailingList", "operations-announce")
"district": "Niushan Wei Wu", .doRun()
"city": "Dongguan City", .keep();
"province": "Guangdong Province", }
"country": "China"
""")
.given("newOfficePhoneNumber", "++15 999 654321")
.given("newEmailAddress", "norden@test-ag.example.org")
.doRun();
}
@Test @Test
@Order(1201) @Order(5001)
@Requires("Partner: P-31011 - Michelle Matthieu") @Requires("Subscription: Michael Miller to operations-announce")
void shouldUpdatePersonData() { void shouldUnsubscribeNewPersonAndContactToMailinglist() {
new ShouldUpdatePersonData(this) new UnsubscribeFromMailinglist(scenarioTest)
.given("oldFamilyName", "Matthieu") .given("mailingList", "operations-announce")
.given("newFamilyName", "Matthieu-Zhang") .given("subscriberEMailAddress", "michael.miller@example.org")
.doRun(); .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();
} }
} }

View File

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

View File

@ -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<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {
@Override
public String toString() {
return ObjectUtils.toString(uuid);
}
}
private final static Map<String, Alias<?>> aliases = new HashMap<>();
private final static Map<String, Object> properties = new HashMap<>();
public final TestReport testReport = new TestReport(aliases);
@LocalServerPort
Integer port;
@Autowired
HsOfficePersonRepository personRepo;
@Autowired
JpaAttempt jpaAttempt;
@SneakyThrows
@BeforeEach
void init(final TestInfo testInfo) {
createHostsharingPerson();
try {
testInfo.getTestMethod().ifPresent(this::callRequiredProducers);
testReport.createTestLogMarkdownFile(testInfo);
} catch (Exception exc) {
throw exc;
}
}
@AfterEach
void cleanup() { // final TestInfo testInfo
properties.clear();
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<String> allOf(final String value, final String explicitly, final String[] implicitly) {
final var all = new HashSet<String>();
if (!value.isEmpty()) {
all.add(value);
}
if (!explicitly.isEmpty()) {
all.add(explicitly);
}
all.addAll(asList(implicitly));
return all;
}
static boolean containsAlias(final String alias) {
return aliases.containsKey(alias);
}
static UUID uuid(final String nameWithPlaceholders) {
final var 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<String, Object> knowVariables() {
final var map = new LinkedHashMap<String, Object>();
ScenarioTest.aliases.forEach((key, value) -> map.put(key, value.uuid()));
map.putAll(ScenarioTest.properties);
return map;
}
public static String resolve(final String text, final 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;
}
}

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.contact; package net.hostsharing.hsadminng.hs.office.scenarios.contact;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.contact; package net.hostsharing.hsadminng.hs.office.scenarios.contact;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.contact; package net.hostsharing.hsadminng.hs.office.scenarios.contact;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.contact; package net.hostsharing.hsadminng.hs.office.scenarios.contact;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.CREATED;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor; package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.person.CreatePerson; import net.hostsharing.hsadminng.hs.office.scenarios.person.CreatePerson;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor; package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.CREATED;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor; package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.CREATED;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor; package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
public class DeleteDebitor extends UseCase<DeleteDebitor> { public class DeleteDebitor extends UseCase<DeleteDebitor> {

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor; package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
public class DontDeleteDefaultDebitor extends UseCase<DontDeleteDefaultDebitor> { public class DontDeleteDefaultDebitor extends UseCase<DontDeleteDefaultDebitor> {

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor; package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor; package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.HttpStatus.OK;

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership; package net.hostsharing.hsadminng.hs.office.scenarios.membership;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership; package net.hostsharing.hsadminng.hs.office.scenarios.membership;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; 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 { public class CreateCoopAssetsDepositTransaction extends CreateCoopAssetsTransaction {

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; 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 { public class CreateCoopAssetsDisbursalTransaction extends CreateCoopAssetsTransaction {

View File

@ -1,16 +1,16 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; 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); super(testSuite);
requires("CoopAssets-Transaction with incorrect assetValue", alias -> requires("CoopAssets-Transaction with incorrect assetValue", alias ->
new CreateCoopAssetsDepositTransaction(testSuite) new CreateCoopAssetsDepositTransaction(testSuite)
.given("memberNumber", "%{memberNumber}") .given("memberNumber", "%{memberNumber}")
.given("reference", "sign %{dateOfIncorrectTransaction}") // same as relatedAssetTx .given("reference", "sign %{dateOfIncorrectTransaction}") // same text as relatedAssetTx
.given("assetValue", 10) .given("assetValue", 10)
.given("comment", "coop-assets deposit transaction with wrong asset value") .given("comment", "coop-assets deposit transaction with wrong asset value")
.given("transactionDate", "%{dateOfIncorrectTransaction}") .given("transactionDate", "%{dateOfIncorrectTransaction}")
@ -21,7 +21,9 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti
protected HttpResponse run() { protected HttpResponse run() {
given("transactionType", "REVERSAL"); given("transactionType", "REVERSAL");
given("assetValue", -10); given("assetValue", -10);
given("reference", "sign %{dateOfIncorrectTransaction}"); // same text as relatedAssetTx
given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue")); given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue"));
given("transactionDate", "%{dateOfIncorrectTransaction}");
return super.run(); return super.run();
} }
} }

View File

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

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,10 +1,10 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; 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.membership.CreateMembership;
import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner; 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 { public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransaction {
@ -19,8 +19,8 @@ public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransac
.given("emailAddress", "board-of-directors@new-ag.example.org") .given("emailAddress", "board-of-directors@new-ag.example.org")
); );
requires("Membership: New AG", alias -> new CreateMembership(testSuite) requires("Membership: %{adoptingMemberNumber} - New AG", alias -> new CreateMembership(testSuite)
.given("partnerNumber", toPartnerNumber("%{adoptingMemberNumber}")) .given("memberNumber", toPartnerNumber("%{adoptingMemberNumber}"))
.given("partnerName", "New AG") .given("partnerName", "New AG")
.given("validFrom", "2024-11-15") .given("validFrom", "2024-11-15")
.given("newStatus", "ACTIVE") .given("newStatus", "ACTIVE")
@ -34,8 +34,7 @@ public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransac
given("memberNumber", "%{transferringMemberNumber}"); given("memberNumber", "%{transferringMemberNumber}");
given("transactionType", "TRANSFER"); given("transactionType", "TRANSFER");
given("assetValue", "-%{valueToDisburse}"); given("assetValue", "-%{valueToTransfer}");
given("assetValue", "-%{valueToDisburse}");
return super.run(); return super.run();
} }

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; 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 { public class CreateCoopSharesCancellationTransaction extends CreateCoopSharesTransaction {

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; 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 { public class CreateCoopSharesRevertTransaction extends CreateCoopSharesTransaction {

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; 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 { public class CreateCoopSharesSubscriptionTransaction extends CreateCoopSharesTransaction {

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner; package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner; package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner; package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.partner; package net.hostsharing.hsadminng.hs.office.scenarios.partner;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
public class DeletePartner extends UseCase<DeletePartner> { public class DeletePartner extends UseCase<DeletePartner> {

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.person; package net.hostsharing.hsadminng.hs.office.scenarios.person;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
public class CreatePerson extends UseCase<CreatePerson> { public class CreatePerson extends UseCase<CreatePerson> {

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.person; package net.hostsharing.hsadminng.hs.office.scenarios.person;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.subscription; package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.NOT_FOUND;

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.subscription; package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios.subscription; package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.http.HttpStatus.NO_CONTENT;

View File

@ -8,6 +8,7 @@ import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountReposi
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
@ -15,6 +16,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -30,8 +32,9 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCleanup { class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCleanup {

View File

@ -0,0 +1,44 @@
package net.hostsharing.hsadminng.hs.scenarios;
public final class JsonOptional<V> {
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 <T> JsonOptional<T> ofValue(final T value) {
return new JsonOptional<>(value);
}
public static <T> JsonOptional<T> 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();
}
}

View File

@ -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 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; import static org.junit.jupiter.api.Assertions.fail;
public class PathAssertion { public class PathAssertion {
@ -19,7 +19,7 @@ public class PathAssertion {
public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) { public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) {
return response -> { return response -> {
try { 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) { } catch (final AssertionError e) {
// without this, the error message is often lacking important context // without this, the error message is often lacking important context
fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" ); 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();
}
} }

View File

@ -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<String> producedAliases(final Produces producesAnnot) {
return allOf(
producesAnnot.value(),
producesAnnot.explicitly(),
producesAnnot.implicitly());
}
private Set<String> allOf(final String value, final String explicitly, final String[] implicitly) {
final var all = new HashSet<String>();
if (!value.isEmpty()) {
all.add(value);
}
if (!explicitly.isEmpty()) {
all.add(explicitly);
}
all.addAll(asList(implicitly));
return all;
}
}
}

View File

@ -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.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;

View File

@ -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<String> currentTestMethodProduces = new Stack<>();
protected ScenarioTest scenarioTest = this;
Optional<String> takeProducedAlias() {
if (currentTestMethodProduces.isEmpty()) {
return Optional.empty();
}
return Optional.of(currentTestMethodProduces.pop());
}
record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {
@Override
public String toString() {
return Objects.toString(uuid);
}
}
private final static Map<String, Alias<?>> aliases = new HashMap<>();
private final static Map<String, Object> properties = new HashMap<>();
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<String, Object> knowVariables() {
final var map = new LinkedHashMap<String, Object>();
ScenarioTest.aliases.forEach((key, value) -> map.put(key, value.uuid()));
map.putAll(ScenarioTest.properties);
return map;
}
public static String resolve(final String text, final 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> T resolveTyped(final String resolvableText, final Class<T> valueType) {
final var resolvedValue = resolve(resolvableText, DROP_COMMENTS);
if (valueType == BigDecimal.class) {
//noinspection unchecked
return (T) new BigDecimal(resolvedValue);
}
//noinspection unchecked
return (T) resolvedValue;
}
}

View File

@ -1,4 +1,4 @@
package net.hostsharing.hsadminng.hs.office.scenarios; package net.hostsharing.hsadminng.hs.scenarios;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -10,7 +10,7 @@ import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; 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 { public class TemplateResolver {

View File

@ -1,10 +1,10 @@
package net.hostsharing.hsadminng.hs.office.scenarios; package net.hostsharing.hsadminng.hs.scenarios;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Map; 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; import static org.assertj.core.api.Assertions.assertThat;
class TemplateResolverUnitTest { class TemplateResolverUnitTest {

View File

@ -1,4 +1,4 @@
package net.hostsharing.hsadminng.hs.office.scenarios; package net.hostsharing.hsadminng.hs.scenarios;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.system.SystemProcess; import net.hostsharing.hsadminng.system.SystemProcess;
@ -12,9 +12,12 @@ import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern;
import static java.lang.String.join;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
public class TestReport { public class TestReport {
@ -41,9 +44,12 @@ public class TestReport {
} }
public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException { 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(); 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)); markdownReport = new PrintWriter(new FileWriter(markdownReportFile));
print("## Scenario #" + determineScenarioTitle(testInfo)); print("## Scenario #" + determineScenarioTitle(testInfo));
} }
@ -119,6 +125,20 @@ public class TestReport {
return result.toString(); 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<String>();
while (matcher.find()) {
words.add(matcher.group(0));
}
return join(" ", words);
}
@SneakyThrows @SneakyThrows
private String currentGitBranch() { private String currentGitBranch() {
try { try {

View File

@ -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.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -9,13 +9,14 @@ import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.reflection.AnnotationFinder; import net.hostsharing.hsadminng.reflection.AnnotationFinder;
import org.apache.commons.collections4.map.LinkedMap; 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.hibernate.AssertionFailure;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import jakarta.validation.constraints.NotNull;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
@ -27,15 +28,15 @@ import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import static java.net.URLEncoder.encode; import static java.net.URLEncoder.encode;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS; 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 net.hostsharing.hsadminng.test.DebuggerDetection.isDebuggerAttached;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ -70,9 +71,7 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public final void requires(final String alias, final Function<String, UseCase<?>> useCaseFactory) { public final void requires(final String alias, final Function<String, UseCase<?>> useCaseFactory) {
if (!ScenarioTest.containsAlias(alias)) { requirements.put(alias, useCaseFactory);
requirements.put(alias, useCaseFactory);
}
} }
public final HttpResponse doRun() { public final HttpResponse doRun() {
@ -88,13 +87,18 @@ public abstract class UseCase<T extends UseCase<?>> {
testReport.printLine(""); testReport.printLine("");
testReport.silent(() -> testReport.silent(() ->
requirements.forEach((alias, factory) -> { requirements.forEach((alias, factory) -> {
if (!ScenarioTest.containsAlias(alias)) { final var resolvedAlias = ScenarioTest.resolve(alias, DROP_COMMENTS);
factory.apply(alias).run().keepAs(alias); if (!ScenarioTest.containsAlias(resolvedAlias)) {
factory.apply(resolvedAlias).run().keepAs(resolvedAlias);
} }
}) })
); );
final var response = run(); final var response = run();
verify(response); verify(response);
keepInProduceAlias(response);
resetProperties();
return response; return response;
} }
@ -109,7 +113,7 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public final UseCase<T> given(final String propName, final Object propValue) { public final UseCase<T> 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); ScenarioTest.putProperty(propName, propValue);
return this; return this;
} }
@ -206,7 +210,8 @@ public abstract class UseCase<T extends UseCase<?>> {
return new PathAssertion(path); return new PathAssertion(path);
} }
protected void verify( @SafeVarargs
protected final void verify(
final String title, final String title,
final Supplier<UseCase.HttpResponse> http, final Supplier<UseCase.HttpResponse> http,
final Consumer<UseCase.HttpResponse>... assertions) { final Consumer<UseCase.HttpResponse>... assertions) {
@ -236,12 +241,24 @@ public abstract class UseCase<T extends UseCase<?>> {
String resolvePlaceholders() { String resolvePlaceholders() {
return ScenarioTest.resolve(template, DROP_COMMENTS); 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) { private static Duration seconds(final int secondsIfNoDebuggerAttached) {
return isDebuggerAttached() ? Duration.ofHours(1) : Duration.ofSeconds(secondsIfNoDebuggerAttached); return isDebuggerAttached() ? Duration.ofHours(1) : Duration.ofSeconds(secondsIfNoDebuggerAttached);
} }
private void resetProperties() {
givenProperties.forEach((propName, val) -> ScenarioTest.removeProperty(propName));
}
public final class HttpResponse { public final class HttpResponse {
@Getter @Getter
@ -319,22 +336,25 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
@SneakyThrows @SneakyThrows
public String getFromBody(final String path) { public <V> V getFromBody(final String path) {
return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS)); final var body = response.body();
final var resolvedPath = ScenarioTest.resolve(path, DROP_COMMENTS);
return JsonPath.parse(body).read(resolvedPath);
} }
@NotNull
@SneakyThrows @SneakyThrows
public <T> Optional<T> getFromBodyAsOptional(final String path) { public <V> JsonOptional<V> getFromBodyAsOptional(final String path) {
try { try {
return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS))); return JsonOptional.ofValue(getFromBody(path));
} catch (final PathNotFoundException e) { } 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 @SneakyThrows
public <T> OptionalAssert<T> path(final String path) { public AbstractStringAssert<?> path(final String path) {
return assertThat(getFromBodyAsOptional(path)); return assertThat(getFromBodyAsOptional(path).givenAsString());
} }
@SneakyThrows @SneakyThrows
@ -396,4 +416,12 @@ public abstract class UseCase<T extends UseCase<?>> {
private String title(String resultAlias) { private String title(String resultAlias) {
return getClass().getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1 $2") + " => " + 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)";
}
} }

View File

@ -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; import static org.assertj.core.api.Assumptions.assumeThat;

View File

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

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.rbac.context; package net.hostsharing.hsadminng.rbac.context;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.mapper.Array;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.Test; 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.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -32,6 +34,9 @@ class ContextIntegrationTests {
@Autowired @Autowired
private JpaAttempt jpaAttempt; private JpaAttempt jpaAttempt;
@PersistenceContext
private EntityManager em;
@Test @Test
void defineWithoutHttpServletRequestUsesCallStack() { void defineWithoutHttpServletRequestUsesCallStack() {
@ -43,7 +48,7 @@ class ContextIntegrationTests {
@Test @Test
@Transactional @Transactional
void defineWithcurrentSubjectButWithoutAssumedRoles() { void defineWithCurrentSubjectButWithoutAssumedRoles() {
// when // when
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
@ -60,7 +65,7 @@ class ContextIntegrationTests {
} }
@Test @Test
void defineWithoutcurrentSubjectButWithAssumedRoles() { void defineWithoutCurrentSubjectButWithAssumedRoles() {
// when // when
final var result = jpaAttempt.transacted(() -> final var result = jpaAttempt.transacted(() ->
context.define(null, "rbactest.package#yyy00:ADMIN") context.define(null, "rbactest.package#yyy00:ADMIN")
@ -73,7 +78,7 @@ class ContextIntegrationTests {
} }
@Test @Test
void defineWithUnknowncurrentSubject() { void defineWithUnknownCurrentSubject() {
// when // when
final var result = jpaAttempt.transacted(() -> final var result = jpaAttempt.transacted(() ->
context.define("unknown@example.org") context.define("unknown@example.org")
@ -87,7 +92,7 @@ class ContextIntegrationTests {
@Test @Test
@Transactional @Transactional
void defineWithcurrentSubjectAndAssumedRoles() { void defineWithCurrentSubjectAndAssumedRoles() {
// given // given
context.define("superuser-alex@hostsharing.net", "rbactest.customer#xxx:OWNER;rbactest.customer#yyy:OWNER"); context.define("superuser-alex@hostsharing.net", "rbactest.customer#xxx:OWNER;rbactest.customer#yyy:OWNER");
@ -102,7 +107,7 @@ class ContextIntegrationTests {
} }
@Test @Test
public void defineContextWithcurrentSubjectAndAssumeInaccessibleRole() { public void defineContextWithCurrentSubjectAndAssumeInaccessibleRole() {
// when // when
final var result = jpaAttempt.transacted(() -> final var result = jpaAttempt.transacted(() ->
context.define("customer-admin@xxx.example.com", "rbactest.package#yyy00:ADMIN") context.define("customer-admin@xxx.example.com", "rbactest.package#yyy00:ADMIN")
@ -113,4 +118,52 @@ class ContextIntegrationTests {
jakarta.persistence.PersistenceException.class, jakarta.persistence.PersistenceException.class,
"ERROR: [403] subject customer-admin@xxx.example.com has no permission to assume role rbactest.package#yyy00:ADMIN"); "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();
}
} }

View File

@ -10,12 +10,14 @@ import net.hostsharing.hsadminng.rbac.role.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; 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.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -31,8 +33,9 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 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) @Transactional(readOnly = true, propagation = Propagation.NEVER)
class RbacGrantControllerAcceptanceTest extends ContextBasedTest { class RbacGrantControllerAcceptanceTest extends ContextBasedTest {

View File

@ -4,17 +4,20 @@ import io.restassured.RestAssured;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = HsadminNgApplication.class classes = {HsadminNgApplication.class, DisableSecurityConfig.class}
) )
@ActiveProfiles("test")
class RbacRoleControllerAcceptanceTest { class RbacRoleControllerAcceptanceTest {
@LocalServerPort @LocalServerPort

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.role;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith; 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.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(RbacRoleController.class) @WebMvcTest(RbacRoleController.class)
@Import(StandardMapper.class) @Import({StandardMapper.class, DisableSecurityConfig.class})
@ActiveProfiles("test")
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
class RbacRoleControllerRestTest { class RbacRoleControllerRestTest {

View File

@ -5,11 +5,13 @@ import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.UUID; import java.util.UUID;
@ -19,8 +21,9 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class RbacSubjectControllerAcceptanceTest { class RbacSubjectControllerAcceptanceTest {

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.subject;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(RbacSubjectController.class) @WebMvcTest(RbacSubjectController.class)
@Import(StandardMapper.class) @Import({StandardMapper.class, DisableSecurityConfig.class})
@ActiveProfiles("test")
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
class RbacSubjectControllerRestTest { class RbacSubjectControllerRestTest {

View File

@ -27,7 +27,7 @@ public class JsonBuilder {
* @param value JSON value * @param value JSON value
* @return this JsonBuilder * @return this JsonBuilder
*/ */
public JsonBuilder with(final String key, final String value) { public JsonBuilder with(final String key, final Object value) {
try { try {
jsonObject.put(key, value); jsonObject.put(key, value);
} catch (JSONException e) { } catch (JSONException e) {

View File

@ -5,6 +5,7 @@ import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
@ -12,6 +13,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -24,8 +26,9 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class } classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class TestCustomerControllerAcceptanceTest { class TestCustomerControllerAcceptanceTest {

View File

@ -4,6 +4,7 @@ import io.restassured.RestAssured;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.test.DisableSecurityConfig;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; 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;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.UUID; import java.util.UUID;
@ -22,8 +24,9 @@ import static org.hamcrest.Matchers.is;
@SpringBootTest( @SpringBootTest(
webEnvironment = WebEnvironment.RANDOM_PORT, webEnvironment = WebEnvironment.RANDOM_PORT,
classes = HsadminNgApplication.class classes = { HsadminNgApplication.class, DisableSecurityConfig.class }
) )
@ActiveProfiles("test")
@Transactional @Transactional
class TestPackageControllerAcceptanceTest { class TestPackageControllerAcceptanceTest {

View File

@ -38,27 +38,29 @@ class TestPackageRepositoryIntegrationTest extends ContextBasedTest {
class FindAllByOptionalNameLike { class FindAllByOptionalNameLike {
@Test @Test
public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { public void globalAdmin_withoutAssumedRole_canViewAllPackagesDueToBypassoOfRecursiveCteRbacQuery() {
// given // 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"); context.define("superuser-fran@hostsharing.net");
// when // when
final var result = testPackageRepository.findAllByOptionalNameLike(null); final var result = testPackageRepository.findAllByOptionalNameLike(null);
// then // then
noPackagesAreReturned(result);
exactlyThesePackagesAreReturned(result,
"xxx00", "xxx01", "xxx02", "yyy00", "yyy01", "yyy02", "zzz00", "zzz01", "zzz02");
} }
@Test @Test
public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { public void globalAdmin_withAssumedGlobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() {
given: // given
context.define("superuser-alex@hostsharing.net", "rbac.global#global:ADMIN"); context.define("superuser-alex@hostsharing.net", "rbac.global#global:ADMIN");
// when // when
final var result = testPackageRepository.findAllByOptionalNameLike(null); final var result = testPackageRepository.findAllByOptionalNameLike(null);
then: // then
noPackagesAreReturned(result); noPackagesAreReturned(result);
} }

View File

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

View File

@ -63,6 +63,9 @@ public class TestUuidGenerator {
* @return a constant UUID related to the given index * @return a constant UUID related to the given index
*/ */
public static UUID use(final int index) { public static UUID use(final int index) {
if (staticallyUsedIndexes.contains(index)) {
throw new IllegalArgumentException("index " + index + " already used statically");
}
staticallyUsedIndexes.add(index); staticallyUsedIndexes.add(index);
return GIVEN_UUIDS.get(index); return GIVEN_UUIDS.get(index);
} }

View File

@ -1,3 +1,13 @@
management:
server:
port: 8081
address: 127.0.0.1
endpoints:
web:
exposure:
include: info, health, metrics
spring: spring:
sql: sql:
init: init: