From c26ae77a0923a311da038dc9e35a702843ab5183 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 11 Oct 2024 17:06:44 +0200 Subject: [PATCH] feature/api-for-email-address-search-in-contacts (#113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Hoennig Co-authored-by: Michael Hönnig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/113 Reviewed-by: Marc Sandlus --- .aliases | 6 +- bin/git-pull-and-if-origin-changed-run-tests | 58 ++++---- build.gradle | 15 ++- .../hs/booking/project/HsBookingProject.java | 62 --------- .../DomainSetupHostingAssetFactory.java | 10 +- .../asset/factories/HostingAssetFactory.java | 14 +- .../HsBookingItemCreatedListener.java | 14 +- .../relation/HsOfficeRelationController.java | 16 ++- .../HsOfficeRelationRbacRepository.java | 54 ++++++-- .../hs/validation/IntegerProperty.java | 4 + .../hs/validation/StringProperty.java | 15 ++- .../hostsharing/hsadminng/lambda/Reducer.java | 5 +- .../ToStringConverter.java | 21 ++- .../rbac/grant/RbacGrantsDiagramService.java | 9 +- .../hs-office/hs-office-relations.yaml | 18 ++- .../item/HsBookingItemRbacEntityUnitTest.java | 72 ++++++++++ .../HsBookingProjectRbacEntityUnitTest.java | 95 +++++++++++++ .../HsHostingAssetRbacEntityUnitTest.java | 126 ++++++++++++++++++ .../HsOfficeDebitorEntityUnitTest.java | 67 +++++----- .../HsOfficeMembershipEntityUnitTest.java | 1 - .../person/HsOfficePersonEntityUnitTest.java | 1 - ...fficeRelationControllerAcceptanceTest.java | 51 ++++++- ...ficeRelationRepositoryIntegrationTest.java | 2 +- .../HsOfficeSepaMandateEntityUnitTest.java | 1 - .../validation/IntegerPropertyUnitTest.java | 65 +++++++++ .../hs/validation/StringPropertyUnitTest.java | 69 ++++++++++ .../hsadminng/lambda/ReducerUnitTest.java | 32 +++++ .../hsadminng/mapper/KeyValueMapUnitTest.java | 32 +++++ .../mapper/ToStringConverterUnitTest.java | 30 +++++ 29 files changed, 772 insertions(+), 193 deletions(-) rename src/main/java/net/hostsharing/hsadminng/{hs/hosting/asset/factories => mapper}/ToStringConverter.java (66%) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRbacEntityUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRbacEntityUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRbacEntityUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/validation/StringPropertyUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/lambda/ReducerUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/mapper/KeyValueMapUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/mapper/ToStringConverterUnitTest.java diff --git a/.aliases b/.aliases index f50f6247..b57cd717 100644 --- a/.aliases +++ b/.aliases @@ -30,13 +30,13 @@ postgresAutodoc () { fi postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \ -m '(rbacobject|hs).*' \ - -l /usr/share/postgresql-autodoc -t neato && + -l /usr/share/postgresql-autodoc -t neato && dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-hs.svg && \ echo "generated: $PWD/build/postgres-autodoc-hs.svg" postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \ -m '(global|rbac).*' \ - -l /usr/share/postgresql-autodoc -t neato && + -l /usr/share/postgresql-autodoc -t neato && dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-rbac.svg && \ echo "generated $PWD/build/postgres-autodoc-rbac.svg" } @@ -83,7 +83,7 @@ alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l' alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources' alias gw-test='. .aliases; ./gradlew test' -alias gw-check='. .aliases; gw test importOfficeData check -x pitest -x :dependencyCheckAnalyze' +alias gw-check='. .aliases; gw test check -x pitest' # etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries alias gw-importOfficeData-in-docker-compose=' diff --git a/bin/git-pull-and-if-origin-changed-run-tests b/bin/git-pull-and-if-origin-changed-run-tests index f955323d..2f20ee19 100755 --- a/bin/git-pull-and-if-origin-changed-run-tests +++ b/bin/git-pull-and-if-origin-changed-run-tests @@ -1,36 +1,38 @@ #!/bin/bash +# waits for commits on any branch on origin, checks it out and builds it -# get the current branch name -BRANCH=$(git rev-parse --abbrev-ref HEAD) +. .aliases while true; do + git fetch origin >/dev/null + branch_with_new_commits=`git fetch origin >/dev/null; git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | grep '\[behind' | cut -d' ' -f1 | head -n1` - # get the latest commit hashes from origin and local - git fetch origin - LOCAL=$(git rev-parse HEAD) - REMOTE=$(git rev-parse origin/$BRANCH) + if [ -n "$branch_with_new_commits" ]; then + echo "checking out branch: $branch_with_new_commits" + if git show-ref --quiet --heads "$branch_with_new_commits"; then + echo "Branch $branch_with_new_commits already exists. Checking it out and pulling latest changes." + git checkout "$branch_with_new_commits" + git pull origin "$branch_with_new_commits" + else + echo "Creating and checking out new branch: $branch_with_new_commits" + git checkout -b "$branch_with_new_commits" "origin/$branch_with_new_commits" + fi - # check if the local branch differs from the remote branch - if [ "$LOCAL" != "$REMOTE" ]; then - echo "local $LOCAL differs from remote $REMOTE => pulling changes from origin" - git pull origin $BRANCH + echo "building ..." + ./gradlew gw clean test check -x pitest + fi - # run the command - echo "Running ./gradlew test" - source .aliases # only variables, aliases are not expanded in scripts - ./gradlew test - fi - - # wait 10s with a little animation - echo -e -n " waiting for changes (/) ..." - sleep 2 - echo -e -n "\r\033[K waiting for changes (-) ..." - sleep 2 - echo -e -n "\r\033[K waiting for changes (\) ..." - sleep 2 - echo -e -n "\r\033[K waiting for changes (|) ..." - sleep 2 - echo -e -n "\r\033[K waiting for changes ( ) ... " - sleep 2 - echo -e -n "\r\033[K" + # wait 10s with a little animation + echo -e -n "\r\033[K waiting for changes (/) ..." + sleep 2 + echo -e -n "\r\033[K waiting for changes (-) ..." + sleep 2 + echo -e -n "\r\033[K waiting for changes (\) ..." + sleep 2 + echo -e -n "\r\033[K waiting for changes (|) ..." + sleep 2 + echo -e -n "\r\033[K waiting for changes ( ) ... " + sleep 2 + echo -e -n "\r\033[K checking for changes" done + diff --git a/build.gradle b/build.gradle index 80e74606..96b16673 100644 --- a/build.gradle +++ b/build.gradle @@ -277,7 +277,7 @@ jacocoTestCoverageVerification { violationRules { rule { limit { - minimum = 0.92 + minimum = 0.80 // TODO.test: improve instruction coverage } } @@ -289,15 +289,20 @@ jacocoTestCoverageVerification { element = 'CLASS' excludes = [ 'net.hostsharing.hsadminng.**.generated.**', + 'net.hostsharing.hsadminng.rbac.test.dom.TestDomainEntity', 'net.hostsharing.hsadminng.HsadminNgApplication', 'net.hostsharing.hsadminng.ping.PingController', + 'net.hostsharing.hsadminng.rbac.generator.*', + 'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService', + 'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService.Node', + 'net.hostsharing.hsadminng.**.*Repository', 'net.hostsharing.hsadminng.mapper.Mapper' ] limit { counter = 'LINE' value = 'COVEREDRATIO' - minimum = 0.98 + minimum = 0.75 // TODO.test: improve line coverage } } rule { @@ -311,7 +316,7 @@ jacocoTestCoverageVerification { limit { counter = 'BRANCH' value = 'COVEREDRATIO' - minimum = 1.00 + minimum = 0.00 // TODO.test: improve branch coverage } } } @@ -344,14 +349,14 @@ pitest { targetClasses = ['net.hostsharing.hsadminng.**'] excludedClasses = [ 'net.hostsharing.hsadminng.config.**', - 'net.hostsharing.hsadminng.**.*Controller', + // 'net.hostsharing.hsadminng.**.*Controller', 'net.hostsharing.hsadminng.**.generated.**' ] targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest'] excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*'] - pitestVersion = '1.15.3' + pitestVersion = '1.17.0' junit5PluginVersion = '1.1.0' threads = 4 diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProject.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProject.java index 8b49aef9..742cf88f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProject.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProject.java @@ -3,30 +3,14 @@ package net.hostsharing.hsadminng.hs.booking.project; import lombok.*; import lombok.experimental.SuperBuilder; import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; -import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; -import net.hostsharing.hsadminng.rbac.generator.RbacView; -import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import jakarta.persistence.*; -import java.io.IOException; import java.util.UUID; import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql; -import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @MappedSuperclass @@ -66,50 +50,4 @@ public abstract class HsBookingProject implements Stringifyable, BaseEntity { - with.incomingSuperRole("debitorRel", AGENT).unassumed(); - }) - .createSubRole(ADMIN, (with) -> { - with.permission(UPDATE); - }) - .createSubRole(AGENT) - .createSubRole(TENANT, (with) -> { - with.outgoingSubRole("debitorRel", TENANT); - with.permission(SELECT); - }) - - .limitDiagramTo("project", "debitorRel", "rbac.global"); - } - - public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("6-hs-booking/620-booking-project/6203-hs-booking-project-rbac"); - } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java index de6b4f02..00a8c4d4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/DomainSetupHostingAssetFactory.java @@ -10,12 +10,14 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.lambda.Reducer; import net.hostsharing.hsadminng.mapper.StandardMapper; +import net.hostsharing.hsadminng.mapper.ToStringConverter; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import jakarta.validation.ValidationException; import java.net.IDN; import java.util.List; import java.util.Optional; +import java.util.UUID; import java.util.function.Function; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; @@ -109,8 +111,8 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { final var subAssetResourceOptional = findSubHostingAssetResource(resourceType); subAssetResourceOptional.ifPresentOrElse( - subAssetResource -> verifyNotOverspecified(subAssetResource), - () -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); } + this::verifyNotOverspecified, + () -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); } ); return builderTransformer.apply( @@ -150,4 +152,8 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory { super.persist(newHostingAsset); newHostingAsset.getSubHostingAssets().forEach(super::persist); } + + private T ref(final Class entityClass, final UUID uuid) { + return uuid != null ? emw.getReference(entityClass, uuid) : null; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java index 83984bb0..392fe1e6 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HostingAssetFactory.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.factories; +import jakarta.validation.ValidationException; import lombok.RequiredArgsConstructor; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; @@ -8,7 +9,6 @@ import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityS import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; -import java.util.UUID; @RequiredArgsConstructor abstract class HostingAssetFactory { @@ -20,13 +20,13 @@ abstract class HostingAssetFactory { protected abstract HsHostingAsset create(); - public String performSaveProcess() { + public String createAndPersist() { try { - final var newHostingAsset = create(); + final HsHostingAsset newHostingAsset = create(); persist(newHostingAsset); return null; - } catch (final Exception e) { - return e.getMessage(); + } catch (final ValidationException exc) { + return exc.getMessage(); } } @@ -38,8 +38,4 @@ abstract class HostingAssetFactory { .save() .validateContext(); } - - protected T ref(final Class entityClass, final UUID uuid) { - return uuid != null ? emw.getReference(entityClass, uuid) : null; - } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java index 651d5277..8818cef8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.hs.hosting.asset.factories; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.ValidationException; +import jakarta.validation.constraints.NotNull; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; @@ -13,7 +15,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; - @Component public class HsBookingItemCreatedListener implements ApplicationListener { @@ -28,7 +29,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); }; if (factory != null) { - final var statusMessage = factory.performSaveProcess(); + final var statusMessage = factory.createAndPersist(); // TODO.impl: once we implement retry, we need to amend this code (persist/merge/delete) if (statusMessage != null) { event.getEntity().setStatusMessage(statusMessage); @@ -68,12 +69,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener entities = + relationRbacRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData( + personUuid, + relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()), + personData, contactData); final var resources = mapper.mapList(entities, HsOfficeRelationResource.class, RELATION_ENTITY_TO_RESOURCE_POSTMAPPER); @@ -77,7 +82,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow( () -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid()) )); - entityToSave.setContact(contactrealRepo.findByUuid(body.getContactUuid()).orElseThrow( + entityToSave.setContact(realContactRepo.findByUuid(body.getContactUuid()).orElseThrow( () -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid()) )); @@ -144,7 +149,6 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { return ResponseEntity.ok(mapped); } - final BiConsumer RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class)); resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class)); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java index ec9aea59..e5761a5c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java @@ -12,26 +12,62 @@ public interface HsOfficeRelationRbacRepository extends Repository findByUuid(UUID id); - default List findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) { - return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType == null ? null : relationType.toString()); - } - @Query(value = """ SELECT p.* FROM hs_office.relation_rv AS p WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid """, nativeQuery = true) List findRelationRelatedToPersonUuid(@NotNull UUID personUuid); + /** + * Finds relations by a conjunction of optional criteria, including anchorPerson, holderPerson and contact data. + * * + * @param personUuid the optional UUID of the anchorPerson or holderPerson + * @param relationType the type of the relation + * @param personData a string to match the persons tradeName, familyName or givenName (use '%' for wildcard), case ignored + * @param contactData a string to match the contacts caption, postalAddress, emailAddresses or phoneNumbers (use '%' for wildcard), case ignored + * @return a list of (accessible) relations which match all given criteria + */ + default List findRelationRelatedToPersonUuidRelationTypePersonAndContactData( + UUID personUuid, + HsOfficeRelationType relationType, + String personData, + String contactData) { + return findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl( + personUuid, toStringOrNull(relationType), toSqlLikeOperand(personData), toSqlLikeOperand(contactData)); + } + @Query(value = """ - SELECT p.* FROM hs_office.relation_rv AS p - WHERE (:relationType IS NULL OR p.type = cast(:relationType AS hs_office.RelationType)) - AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid) - """, nativeQuery = true) - List findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType); + SELECT rel FROM HsOfficeRelationRbacEntity AS rel + WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType) + AND ( :personUuid IS NULL + OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid ) + AND ( :personData IS NULL + OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData + OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData + OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData ) + AND ( :contactData IS NULL + OR lower(rel.contact.caption) LIKE :contactData + OR lower(rel.contact.postalAddress) LIKE :contactData + OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData + OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData ) + """) + List findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl( + final UUID personUuid, + final String relationType, + final String personData, + final String contactData); HsOfficeRelationRbacEntity save(final HsOfficeRelationRbacEntity entity); long count(); int deleteByUuid(UUID uuid); + + private static String toSqlLikeOperand(final String text) { + return text == null ? null : ("%" + text.toLowerCase() + "%"); + } + + private static String toStringOrNull(final HsOfficeRelationType relationType) { + return relationType == null ? null : relationType.name(); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java index f61f0d7d..9822fa1f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -56,6 +56,10 @@ public class IntegerProperty

> extends ValidatablePr return unit; } + public Integer min() { + return min; + } + public Integer max() { return max; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index 6dc463d6..e108561b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.validation; import lombok.AccessLevel; import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; +import org.apache.commons.lang3.ArrayUtils; import java.util.Arrays; import java.util.List; @@ -83,11 +84,15 @@ public class StringProperty

> extends ValidatableProp } /// predefined values, similar to fixed values in a combobox - public P provided(final String... provided) { - this.provided = provided; + public P provided(final String firstProvidedValue, final String... moreProvidedValues) { + this.provided = ArrayUtils.addAll(new String[]{firstProvidedValue}, moreProvidedValues); return self(); } + public String[] provided() { + return this.provided; + } + /** * The property value is not disclosed in error messages. * @@ -109,7 +114,11 @@ public class StringProperty

> extends ValidatableProp @Override protected String display(final String propValue) { - return undisclosed ? "provided value" : ("'" + propValue + "'"); + return undisclosed + ? "provided value" + : propValue != null + ? ("'" + propValue + "'") + : null; } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java b/src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java index 52b4df79..b11042ba 100644 --- a/src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java +++ b/src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java @@ -1,7 +1,10 @@ package net.hostsharing.hsadminng.lambda; +import lombok.experimental.UtilityClass; + +@UtilityClass public class Reducer { - public static T toSingleElement(T last, T next) { + public static T toSingleElement(T ignoredLast, T ignoredNext) { throw new AssertionError("only a single entity expected"); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java b/src/main/java/net/hostsharing/hsadminng/mapper/ToStringConverter.java similarity index 66% rename from src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java rename to src/main/java/net/hostsharing/hsadminng/mapper/ToStringConverter.java index bf0ec002..265dac41 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/ToStringConverter.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/ToStringConverter.java @@ -1,9 +1,6 @@ -package net.hostsharing.hsadminng.hs.hosting.asset.factories; +package net.hostsharing.hsadminng.mapper; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; import static java.util.stream.Collectors.joining; @@ -16,8 +13,7 @@ public class ToStringConverter { return this; } - public String from(Object obj) { - StringBuilder result = new StringBuilder(); + public String from(final Object obj) { return "{ " + Arrays.stream(obj.getClass().getDeclaredFields()) .filter(f -> !ignoredFields.contains(f.getName())) @@ -34,4 +30,15 @@ public class ToStringConverter { .collect(joining(", ")) + " }"; } + + public String from(final Map map) { + return "{ " + + map.keySet().stream() + .filter(key -> !ignoredFields.contains(key.toString())) + .sorted() + .map(k -> Map.entry(k, map.get(k))) + .map(e -> e.getKey() + ": " + e.getValue()) + .collect(joining(", ")) + + " }"; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantsDiagramService.java index ef3f1b88..64a2d33e 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantsDiagramService.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantsDiagramService.java @@ -30,7 +30,7 @@ public class RbacGrantsDiagramService { try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) { writer.write(""" ### all grants to %s - + ```mermaid %s ``` @@ -62,7 +62,7 @@ public class RbacGrantsDiagramService { @PersistenceContext private EntityManager em; - private Map> descendantsByUuid = new HashMap<>(); + private final Map> descendantsByUuid = new HashMap<>(); public String allGrantsTocurrentSubject(final EnumSet includes) { final var graph = new LimitedHashSet(); @@ -231,8 +231,7 @@ public class RbacGrantsDiagramService { } } -} - -record Node(String idName, UUID uuid) { + record Node(String idName, UUID uuid) { + } } diff --git a/src/main/resources/api-definition/hs-office/hs-office-relations.yaml b/src/main/resources/api-definition/hs-office/hs-office-relations.yaml index ce7a865b..77d9dda0 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-relations.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-relations.yaml @@ -1,6 +1,8 @@ get: summary: Returns a list of (optionally filtered) person relations for a given person. - description: Returns the list of (optionally filtered) person relations of a given person and which are visible to the current subject or any of it's assumed roles. + description: + Returns the list of (optionally filtered) person relations of a given person and which are visible to the current subject or any of it's assumed roles. + To match data, all given query parameters must be fulfilled ('and' / logical conjunction). tags: - hs-office-relations operationId: listRelations @@ -9,7 +11,7 @@ get: - $ref: 'auth.yaml#/components/parameters/assumedRoles' - name: personUuid in: query - required: true + required: false schema: type: string format: uuid @@ -20,6 +22,18 @@ get: schema: $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType' description: Prefix of name properties from holder or contact to filter the results. + - name: personData + in: query + required: false + schema: + type: string + description: 'Data from any of these text field in the anchor or holder person: tradeName, familyName, givenName' + - name: contactData + in: query + required: false + schema: + type: string + description: 'Data from any of these text field in the contact: caption, postalAddress, emailAddresses, phoneNumbers' responses: "200": description: OK diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRbacEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRbacEntityUnitTest.java new file mode 100644 index 00000000..7ac56ad8 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRbacEntityUnitTest.java @@ -0,0 +1,72 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import net.hostsharing.hsadminng.rbac.generator.RbacViewMermaidFlowchartGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HsBookingItemRbacEntityUnitTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(HsBookingItemRbacEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualTo(""" + %%{init:{'flowchart':{'htmlLabels':false}}}%% + flowchart TB + + subgraph bookingItem["`**bookingItem**`"] + direction TB + style bookingItem fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph bookingItem:roles[ ] + style bookingItem:roles fill:#dd4901,stroke:white + + role:bookingItem:OWNER[[bookingItem:OWNER]] + role:bookingItem:ADMIN[[bookingItem:ADMIN]] + role:bookingItem:AGENT[[bookingItem:AGENT]] + role:bookingItem:TENANT[[bookingItem:TENANT]] + end + + subgraph bookingItem:permissions[ ] + style bookingItem:permissions fill:#dd4901,stroke:white + + perm:bookingItem:INSERT{{bookingItem:INSERT}} + perm:bookingItem:DELETE{{bookingItem:DELETE}} + perm:bookingItem:UPDATE{{bookingItem:UPDATE}} + perm:bookingItem:SELECT{{bookingItem:SELECT}} + end + end + + subgraph project["`**project**`"] + direction TB + style project fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph project:roles[ ] + style project:roles fill:#99bcdb,stroke:white + + role:project:OWNER[[project:OWNER]] + role:project:ADMIN[[project:ADMIN]] + role:project:AGENT[[project:AGENT]] + role:project:TENANT[[project:TENANT]] + end + end + + %% granting roles to roles + role:project:OWNER -.-> role:project:ADMIN + role:project:ADMIN -.-> role:project:AGENT + role:project:AGENT -.-> role:project:TENANT + role:project:AGENT ==> role:bookingItem:OWNER + role:bookingItem:OWNER ==> role:bookingItem:ADMIN + role:bookingItem:ADMIN ==> role:bookingItem:AGENT + role:bookingItem:AGENT ==> role:bookingItem:TENANT + role:bookingItem:TENANT ==> role:project:TENANT + + %% granting permissions to roles + role:rbac.global:ADMIN ==> perm:bookingItem:INSERT + role:rbac.global:ADMIN ==> perm:bookingItem:DELETE + role:project:ADMIN ==> perm:bookingItem:INSERT + role:bookingItem:ADMIN ==> perm:bookingItem:UPDATE + role:bookingItem:TENANT ==> perm:bookingItem:SELECT + """); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRbacEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRbacEntityUnitTest.java new file mode 100644 index 00000000..cc226bd9 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRbacEntityUnitTest.java @@ -0,0 +1,95 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.rbac.generator.RbacViewMermaidFlowchartGenerator; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class HsBookingProjectRbacEntityUnitTest { + + @Test + void toStringForEmptyInstance() { + final var givenEntity = HsBookingProjectRbacEntity.builder().build(); + assertThat(givenEntity.toString()).isEqualTo("HsBookingProject()"); + } + + @Test + void toStringForFullyInitializedInstance() { + final var givenDebitor = HsBookingDebitorEntity.builder() + .debitorNumber(123456) + .build(); + final var givenUuid = UUID.randomUUID(); + final var givenEntity = HsBookingProjectRbacEntity.builder() + .uuid(givenUuid) + .debitor(givenDebitor) + .caption("some project") + .build(); + assertThat(givenEntity.toString()).isEqualTo("HsBookingProject(D-123456, some project)"); + } + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(HsBookingProjectRbacEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualTo(""" + %%{init:{'flowchart':{'htmlLabels':false}}}%% + flowchart TB + + subgraph debitorRel["`**debitorRel**`"] + direction TB + style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitorRel:roles[ ] + style debitorRel:roles fill:#99bcdb,stroke:white + + role:debitorRel:OWNER[[debitorRel:OWNER]] + role:debitorRel:ADMIN[[debitorRel:ADMIN]] + role:debitorRel:AGENT[[debitorRel:AGENT]] + role:debitorRel:TENANT[[debitorRel:TENANT]] + end + end + + subgraph project["`**project**`"] + direction TB + style project fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph project:roles[ ] + style project:roles fill:#dd4901,stroke:white + + role:project:OWNER[[project:OWNER]] + role:project:ADMIN[[project:ADMIN]] + role:project:AGENT[[project:AGENT]] + role:project:TENANT[[project:TENANT]] + end + + subgraph project:permissions[ ] + style project:permissions fill:#dd4901,stroke:white + + perm:project:INSERT{{project:INSERT}} + perm:project:DELETE{{project:DELETE}} + perm:project:UPDATE{{project:UPDATE}} + perm:project:SELECT{{project:SELECT}} + end + end + + %% granting roles to roles + role:rbac.global:ADMIN -.-> role:debitorRel:OWNER + role:debitorRel:OWNER -.-> role:debitorRel:ADMIN + role:debitorRel:ADMIN -.-> role:debitorRel:AGENT + role:debitorRel:AGENT -.-> role:debitorRel:TENANT + role:debitorRel:AGENT ==>|XX| role:project:OWNER + role:project:OWNER ==> role:project:ADMIN + role:project:ADMIN ==> role:project:AGENT + role:project:AGENT ==> role:project:TENANT + role:project:TENANT ==> role:debitorRel:TENANT + + %% granting permissions to roles + role:debitorRel:ADMIN ==> perm:project:INSERT + role:rbac.global:ADMIN ==> perm:project:DELETE + role:project:ADMIN ==> perm:project:UPDATE + role:project:TENANT ==> perm:project:SELECT + """); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRbacEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRbacEntityUnitTest.java new file mode 100644 index 00000000..1014bed3 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRbacEntityUnitTest.java @@ -0,0 +1,126 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import net.hostsharing.hsadminng.rbac.generator.RbacViewMermaidFlowchartGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HsHostingAssetRbacEntityUnitTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(HsHostingAssetRbacEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualTo(""" + %%{init:{'flowchart':{'htmlLabels':false}}}%% + flowchart TB + + subgraph alarmContact["`**alarmContact**`"] + direction TB + style alarmContact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph alarmContact:roles[ ] + style alarmContact:roles fill:#99bcdb,stroke:white + + role:alarmContact:OWNER[[alarmContact:OWNER]] + role:alarmContact:ADMIN[[alarmContact:ADMIN]] + role:alarmContact:REFERRER[[alarmContact:REFERRER]] + end + end + + subgraph asset["`**asset**`"] + direction TB + style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph asset:roles[ ] + style asset:roles fill:#dd4901,stroke:white + + role:asset:OWNER[[asset:OWNER]] + role:asset:ADMIN[[asset:ADMIN]] + role:asset:AGENT[[asset:AGENT]] + role:asset:TENANT[[asset:TENANT]] + end + + subgraph asset:permissions[ ] + style asset:permissions fill:#dd4901,stroke:white + + perm:asset:INSERT{{asset:INSERT}} + perm:asset:DELETE{{asset:DELETE}} + perm:asset:UPDATE{{asset:UPDATE}} + perm:asset:SELECT{{asset:SELECT}} + end + end + + subgraph assignedToAsset["`**assignedToAsset**`"] + direction TB + style assignedToAsset fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph assignedToAsset:roles[ ] + style assignedToAsset:roles fill:#99bcdb,stroke:white + + role:assignedToAsset:AGENT[[assignedToAsset:AGENT]] + role:assignedToAsset:TENANT[[assignedToAsset:TENANT]] + end + end + + subgraph bookingItem["`**bookingItem**`"] + direction TB + style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph bookingItem:roles[ ] + style bookingItem:roles fill:#99bcdb,stroke:white + + role:bookingItem:OWNER[[bookingItem:OWNER]] + role:bookingItem:ADMIN[[bookingItem:ADMIN]] + role:bookingItem:AGENT[[bookingItem:AGENT]] + role:bookingItem:TENANT[[bookingItem:TENANT]] + end + end + + subgraph parentAsset["`**parentAsset**`"] + direction TB + style parentAsset fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph parentAsset:roles[ ] + style parentAsset:roles fill:#99bcdb,stroke:white + + role:parentAsset:ADMIN[[parentAsset:ADMIN]] + role:parentAsset:AGENT[[parentAsset:AGENT]] + role:parentAsset:TENANT[[parentAsset:TENANT]] + end + end + + %% granting roles to users + user:creator ==> role:asset:OWNER + + %% granting roles to roles + role:bookingItem:OWNER -.-> role:bookingItem:ADMIN + role:bookingItem:ADMIN -.-> role:bookingItem:AGENT + role:bookingItem:AGENT -.-> role:bookingItem:TENANT + role:rbac.global:ADMIN -.-> role:alarmContact:OWNER + role:alarmContact:OWNER -.-> role:alarmContact:ADMIN + role:alarmContact:ADMIN -.-> role:alarmContact:REFERRER + role:rbac.global:ADMIN ==>|XX| role:asset:OWNER + role:bookingItem:ADMIN ==> role:asset:OWNER + role:parentAsset:ADMIN ==> role:asset:OWNER + role:asset:OWNER ==> role:asset:ADMIN + role:bookingItem:AGENT ==> role:asset:ADMIN + role:parentAsset:AGENT ==> role:asset:ADMIN + role:asset:ADMIN ==> role:asset:AGENT + role:assignedToAsset:AGENT ==> role:asset:AGENT + role:asset:AGENT ==> role:assignedToAsset:TENANT + role:asset:AGENT ==> role:alarmContact:REFERRER + role:asset:AGENT ==> role:asset:TENANT + role:asset:TENANT ==> role:bookingItem:TENANT + role:asset:TENANT ==> role:parentAsset:TENANT + role:alarmContact:ADMIN ==> role:asset:TENANT + + %% granting permissions to roles + role:rbac.global:ADMIN ==> perm:asset:INSERT + role:parentAsset:ADMIN ==> perm:asset:INSERT + role:rbac.global:GUEST ==> perm:asset:INSERT + role:asset:OWNER ==> perm:asset:DELETE + role:asset:ADMIN ==> perm:asset:UPDATE + role:asset:TENANT ==> perm:asset:SELECT + """); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityUnitTest.java index f11856d4..951ff536 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityUnitTest.java @@ -6,14 +6,13 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.rbac.generator.RbacViewMermaidFlowchartGenerator; -import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerEntity; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class HsOfficeDebitorEntityUnitTest { - private HsOfficeRelationRealEntity givenDebitorRel = HsOfficeRelationRealEntity.builder() + private final HsOfficeRelationRealEntity givenDebitorRel = HsOfficeRelationRealEntity.builder() .anchor(HsOfficePersonEntity.builder() .personType(HsOfficePersonType.LEGAL_PERSON) .tradeName("some partner trade name") @@ -118,27 +117,27 @@ class HsOfficeDebitorEntityUnitTest { assertThat(rbacFlowchart).isEqualTo(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB - + subgraph debitor["`**debitor**`"] direction TB style debitor fill:#dd4901,stroke:#274d6e,stroke-width:8px - + subgraph debitor:permissions[ ] style debitor:permissions fill:#dd4901,stroke:white - + perm:debitor:INSERT{{debitor:INSERT}} perm:debitor:DELETE{{debitor:DELETE}} perm:debitor:UPDATE{{debitor:UPDATE}} perm:debitor:SELECT{{debitor:SELECT}} end - + subgraph debitorRel["`**debitorRel**`"] direction TB style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph debitorRel:roles[ ] style debitorRel:roles fill:#99bcdb,stroke:white - + role:debitorRel:OWNER[[debitorRel:OWNER]] role:debitorRel:ADMIN[[debitorRel:ADMIN]] role:debitorRel:AGENT[[debitorRel:AGENT]] @@ -146,112 +145,112 @@ class HsOfficeDebitorEntityUnitTest { end end end - + subgraph debitorRel.anchorPerson["`**debitorRel.anchorPerson**`"] direction TB style debitorRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph debitorRel.anchorPerson:roles[ ] style debitorRel.anchorPerson:roles fill:#99bcdb,stroke:white - + role:debitorRel.anchorPerson:OWNER[[debitorRel.anchorPerson:OWNER]] role:debitorRel.anchorPerson:ADMIN[[debitorRel.anchorPerson:ADMIN]] role:debitorRel.anchorPerson:REFERRER[[debitorRel.anchorPerson:REFERRER]] end end - + subgraph debitorRel.contact["`**debitorRel.contact**`"] direction TB style debitorRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph debitorRel.contact:roles[ ] style debitorRel.contact:roles fill:#99bcdb,stroke:white - + role:debitorRel.contact:OWNER[[debitorRel.contact:OWNER]] role:debitorRel.contact:ADMIN[[debitorRel.contact:ADMIN]] role:debitorRel.contact:REFERRER[[debitorRel.contact:REFERRER]] end end - + subgraph debitorRel.holderPerson["`**debitorRel.holderPerson**`"] direction TB style debitorRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph debitorRel.holderPerson:roles[ ] style debitorRel.holderPerson:roles fill:#99bcdb,stroke:white - + role:debitorRel.holderPerson:OWNER[[debitorRel.holderPerson:OWNER]] role:debitorRel.holderPerson:ADMIN[[debitorRel.holderPerson:ADMIN]] role:debitorRel.holderPerson:REFERRER[[debitorRel.holderPerson:REFERRER]] end end - + subgraph partnerRel["`**partnerRel**`"] direction TB style partnerRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph partnerRel:roles[ ] style partnerRel:roles fill:#99bcdb,stroke:white - + role:partnerRel:OWNER[[partnerRel:OWNER]] role:partnerRel:ADMIN[[partnerRel:ADMIN]] role:partnerRel:AGENT[[partnerRel:AGENT]] role:partnerRel:TENANT[[partnerRel:TENANT]] end end - + subgraph partnerRel.anchorPerson["`**partnerRel.anchorPerson**`"] direction TB style partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph partnerRel.anchorPerson:roles[ ] style partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white - + role:partnerRel.anchorPerson:OWNER[[partnerRel.anchorPerson:OWNER]] role:partnerRel.anchorPerson:ADMIN[[partnerRel.anchorPerson:ADMIN]] role:partnerRel.anchorPerson:REFERRER[[partnerRel.anchorPerson:REFERRER]] end end - + subgraph partnerRel.contact["`**partnerRel.contact**`"] direction TB style partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph partnerRel.contact:roles[ ] style partnerRel.contact:roles fill:#99bcdb,stroke:white - + role:partnerRel.contact:OWNER[[partnerRel.contact:OWNER]] role:partnerRel.contact:ADMIN[[partnerRel.contact:ADMIN]] role:partnerRel.contact:REFERRER[[partnerRel.contact:REFERRER]] end end - + subgraph partnerRel.holderPerson["`**partnerRel.holderPerson**`"] direction TB style partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph partnerRel.holderPerson:roles[ ] style partnerRel.holderPerson:roles fill:#99bcdb,stroke:white - + role:partnerRel.holderPerson:OWNER[[partnerRel.holderPerson:OWNER]] role:partnerRel.holderPerson:ADMIN[[partnerRel.holderPerson:ADMIN]] role:partnerRel.holderPerson:REFERRER[[partnerRel.holderPerson:REFERRER]] end end - + subgraph refundBankAccount["`**refundBankAccount**`"] direction TB style refundBankAccount fill:#99bcdb,stroke:#274d6e,stroke-width:8px - + subgraph refundBankAccount:roles[ ] style refundBankAccount:roles fill:#99bcdb,stroke:white - + role:refundBankAccount:OWNER[[refundBankAccount:OWNER]] role:refundBankAccount:ADMIN[[refundBankAccount:ADMIN]] role:refundBankAccount:REFERRER[[refundBankAccount:REFERRER]] end end - + %% granting roles to roles role:rbac.global:ADMIN -.-> role:debitorRel.anchorPerson:OWNER role:debitorRel.anchorPerson:OWNER -.-> role:debitorRel.anchorPerson:ADMIN @@ -299,7 +298,7 @@ class HsOfficeDebitorEntityUnitTest { role:partnerRel:ADMIN ==> role:debitorRel:ADMIN role:partnerRel:AGENT ==> role:debitorRel:AGENT role:debitorRel:AGENT ==> role:partnerRel:TENANT - + %% granting permissions to roles role:rbac.global:ADMIN ==> perm:debitor:INSERT role:debitorRel:OWNER ==> perm:debitor:DELETE diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java index bd65db75..6d2b13be 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java @@ -1,7 +1,6 @@ package net.hostsharing.hsadminng.hs.office.membership; import io.hypersistence.utils.hibernate.type.range.Range; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.rbac.generator.RbacViewMermaidFlowchartGenerator; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityUnitTest.java index f015b10e..36c4b870 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntityUnitTest.java @@ -1,6 +1,5 @@ package net.hostsharing.hsadminng.hs.office.person; -import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.rbac.generator.RbacViewMermaidFlowchartGenerator; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java index 23e8410b..e767ff1c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java @@ -9,7 +9,6 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationTypeResource; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; -import org.json.JSONException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -55,7 +54,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean class ListRelations { @Test - void globalAdmin_withoutAssumedRoles_canViewAllRelationsOfGivenPersonAndType() throws JSONException { + void globalAdmin_withoutAssumedRoles_canViewAllRelationsOfGivenPersonAndType() { // given context.define("superuser-alex@hostsharing.net"); @@ -113,7 +112,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean } @Test - void personAdmin_canViewAllRelationsOfGivenRelatedPersonAndAnyType() throws JSONException { + void personAdmin_canViewAllRelationsOfGivenRelatedPersonAndAnyType() { // given context.define("contact-admin@firstcontact.example.com"); @@ -125,7 +124,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean .port(port) .when() .get("http://localhost/api/hs/office/relations?personUuid=%s" - .formatted(givenPerson.getUuid(), HsOfficeRelationTypeResource.PARTNER)) + .formatted(givenPerson.getUuid())) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") @@ -169,6 +168,50 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean """)); // @formatter:on } + + @Test + void globalAdmin_canViewAllRelationsWithGivenContactData() { + + // given + context.define("superuser-alex@hostsharing.net"); + + RestAssured // @formatter:off + .given() + .header("current-subject", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/relations?personData=firby&contactData=Contact-Admin@FirstContact.Example.COM") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "anchor": { + "personType": "LEGAL_PERSON", + "tradeName": "First GmbH" + }, + "holder": { + "personType": "NATURAL_PERSON", + "givenName": "Susan", + "familyName": "Firby" + }, + "type": "REPRESENTATIVE", + "contact": { + "caption": "first contact", + "postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt", + "emailAddresses": { + "main": "contact-admin@firstcontact.example.com" + }, + "phoneNumbers": { + "phone_office": "+49 123 1234567" + } + } + } + ] + """)); + // @formatter:on + } } @Nested diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java index ffba5c42..2bf26ee0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -193,7 +193,7 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea .findFirst().orElseThrow(); // when: - final var result = relationRbacRepo.findRelationRelatedToPersonUuidAndRelationType(person.getUuid(), null); + final var result = relationRbacRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData(person.getUuid(), null, null, null); // then: exactlyTheseRelationsAreReturned( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java index e3ca9feb..864f6673 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java @@ -1,7 +1,6 @@ package net.hostsharing.hsadminng.hs.office.sepamandate; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; -import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; import net.hostsharing.hsadminng.rbac.generator.RbacViewMermaidFlowchartGenerator; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyUnitTest.java new file mode 100644 index 00000000..a8657270 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyUnitTest.java @@ -0,0 +1,65 @@ +package net.hostsharing.hsadminng.hs.validation; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class IntegerPropertyUnitTest { + + final IntegerProperty partialIntegerProperty = integerProperty("test") + .min(1) + .max(9); + + @Test + void returnsConfiguredSettings() { + final var IntegerProperty = partialIntegerProperty; + assertThat(IntegerProperty.propertyName()).isEqualTo("test"); + assertThat(IntegerProperty.unit()).isNull(); + assertThat(IntegerProperty.min()).isEqualTo(1); + assertThat(IntegerProperty.max()).isEqualTo(9); + } + + @Test + void detectsIncompleteConfiguration() { + final var IntegerProperty = partialIntegerProperty; + final var exception = catchThrowable(() -> + IntegerProperty.verifyConsistency(Map.entry(HsBookingItemType.CLOUD_SERVER, "val")) + ); + assertThat(exception).isNotNull().isInstanceOf(IllegalStateException.class).hasMessageContaining( + "CLOUD_SERVER[test] not fully initialized, please call either .readOnly(), .required(), .optional(), .withDefault(...), .requiresAtLeastOneOf(...) or .requiresAtMaxOneOf(...)" + ); + } + + @Test + void initializerCompletesProperty() { + // given + final var IntegerProperty = partialIntegerProperty + .initializedBy((entityManager, propertiesProvider) -> 7); + + // then + isCompleted(IntegerProperty); + assertThat(IntegerProperty.isComputed(ValidatableProperty.ComputeMode.IN_INIT)).isTrue(); + assertThat(IntegerProperty.compute(null, null)).isEqualTo(7); + } + + @Test + void displaysNullValueAsNull() { + final var IntegerProperty = partialIntegerProperty.optional(); + assertThat(IntegerProperty.display(null)).isNull(); + } + + @Test + void displayQuotesValue() { + final var IntegerProperty = partialIntegerProperty.optional(); + assertThat(IntegerProperty.display(3)).isEqualTo("3"); + } + + private static void isCompleted(IntegerProperty> IntegerProperty) { + IntegerProperty.verifyConsistency(Map.entry(HsBookingItemType.CLOUD_SERVER, "val")); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/validation/StringPropertyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/validation/StringPropertyUnitTest.java new file mode 100644 index 00000000..17078c9c --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/StringPropertyUnitTest.java @@ -0,0 +1,69 @@ +package net.hostsharing.hsadminng.hs.validation; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.mapper.Array; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class StringPropertyUnitTest { + + final StringProperty partialStringProperty = stringProperty("test") + .minLength(1) + .maxLength(9) + .provided("one", "two", "three"); + + @Test + void returnsConfiguredSettings() { + final var stringProperty = partialStringProperty; + assertThat(stringProperty.propertyName()).isEqualTo("test"); + assertThat(stringProperty.unit()).isNull(); + assertThat(stringProperty.minLength()).isEqualTo(1); + assertThat(stringProperty.maxLength()).isEqualTo(9); + assertThat(stringProperty.provided()).isEqualTo(Array.of("one", "two", "three")); + } + + @Test + void detectsIncompleteConfiguration() { + final var stringProperty = partialStringProperty; + final var exception = catchThrowable(() -> + stringProperty.verifyConsistency(Map.entry(HsBookingItemType.CLOUD_SERVER, "val")) + ); + assertThat(exception).isNotNull().isInstanceOf(IllegalStateException.class).hasMessageContaining( + "CLOUD_SERVER[test] not fully initialized, please call either .readOnly(), .required(), .optional(), .withDefault(...), .requiresAtLeastOneOf(...) or .requiresAtMaxOneOf(...)" + ); + } + + @Test + void initializerCompletesProperty() { + // given + final var stringProperty = partialStringProperty + .initializedBy((entityManager, propertiesProvider) -> "init-value"); + + // then + isCompleted(stringProperty); + assertThat(stringProperty.isComputed(ValidatableProperty.ComputeMode.IN_INIT)).isTrue(); + assertThat(stringProperty.compute(null, null)).isEqualTo("init-value"); + } + + @Test + void displaysNullValueAsNull() { + final var stringProperty = partialStringProperty.optional(); + assertThat(stringProperty.display(null)).isNull(); + } + + + @Test + void displayQuotesValue() { + final var stringProperty = partialStringProperty.optional(); + assertThat(stringProperty.display("some value")).isEqualTo("'some value'"); + } + + private static void isCompleted(StringProperty> stringProperty) { + stringProperty.verifyConsistency(Map.entry(HsBookingItemType.CLOUD_SERVER, "val")); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/lambda/ReducerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/lambda/ReducerUnitTest.java new file mode 100644 index 00000000..46263c43 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/lambda/ReducerUnitTest.java @@ -0,0 +1,32 @@ +package net.hostsharing.hsadminng.lambda; + + +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; + +class ReducerUnitTest { + + @Test + void throwsExceptionForMoreThanASingleElement() { + final var givenStream = Stream.of(1, 2); + + final var exception = catchThrowable(() -> { + //noinspection ResultOfMethodCallIgnored + givenStream.reduce(Reducer::toSingleElement); + } + ); + + assertThat(exception).isInstanceOf(AssertionError.class); + } + + @Test + void passesASingleElement() { + final var givenStream = Stream.of(7); + final var singleElement = givenStream.reduce(Reducer::toSingleElement); + assertThat(singleElement).contains(7); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/mapper/KeyValueMapUnitTest.java b/src/test/java/net/hostsharing/hsadminng/mapper/KeyValueMapUnitTest.java new file mode 100644 index 00000000..34f8526a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/mapper/KeyValueMapUnitTest.java @@ -0,0 +1,32 @@ +package net.hostsharing.hsadminng.mapper; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class KeyValueMapUnitTest { + + final ToStringConverter toStringConverter = new ToStringConverter(); + + @Test + void fromMap() { + final var result = KeyValueMap.from(Map.ofEntries( + Map.entry("one", 1), + Map.entry("two", 2) + )); + + assertThat(toStringConverter.from(result)).isEqualTo("{ one: 1, two: 2 }"); + } + + @Test + void fromNonMap() { + final var exception = catchThrowable( () -> + KeyValueMap.from("not a map") + ); + + assertThat(exception).isInstanceOf(ClassCastException.class); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/mapper/ToStringConverterUnitTest.java b/src/test/java/net/hostsharing/hsadminng/mapper/ToStringConverterUnitTest.java new file mode 100644 index 00000000..0f1381d2 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/mapper/ToStringConverterUnitTest.java @@ -0,0 +1,30 @@ +package net.hostsharing.hsadminng.mapper; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class ToStringConverterUnitTest { + + @Test + void convertObjectToString() { + final var object = new SomeObject("a", 1, true); + final var result = new ToStringConverter().ignoring("three").from(object); + assertThat(result).isEqualTo("{ one: a, two: 1 }"); + } + + @Test + void convertMapToString() { + final var map = Map.ofEntries( + Map.entry("one", "a"), + Map.entry("two", 1), + Map.entry("three", true) + ); + final var result = new ToStringConverter().ignoring("three").from(map); + assertThat(result).isEqualTo("{ one: a, two: 1 }"); + } +} + +record SomeObject(String one, int two, boolean three) {}