feature/api-for-email-address-search-in-contacts #113

Merged
hsh-michaelhoennig merged 19 commits from feature/api-for-email-address-search-in-contacts into master 2024-10-11 17:06:46 +02:00
21 changed files with 645 additions and 164 deletions
Showing only changes of commit 0f7387229e - Show all commits

View File

@ -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-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
alias gw-test='. .aliases; ./gradlew test' 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 # etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries
alias gw-importOfficeData-in-docker-compose=' alias gw-importOfficeData-in-docker-compose='

View File

@ -1,28 +1,29 @@
#!/bin/bash #!/bin/bash
# waits for commits on any branch on origin, checks it out and builds it
# get the current branch name . .aliases
BRANCH=$(git rev-parse --abbrev-ref HEAD)
while true; do 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 if [ -n "$branch_with_new_commits" ]; then
git fetch origin echo "checking out branch: $branch_with_new_commits"
LOCAL=$(git rev-parse HEAD) if git show-ref --quiet --heads "$branch_with_new_commits"; then
REMOTE=$(git rev-parse origin/$BRANCH) 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 echo "building ..."
if [ "$LOCAL" != "$REMOTE" ]; then ./gradlew gw clean test check -x pitest
echo "local $LOCAL differs from remote $REMOTE => pulling changes from origin"
git pull origin $BRANCH
# run the command
echo "Running ./gradlew test"
source .aliases # only variables, aliases are not expanded in scripts
./gradlew test
fi fi
# wait 10s with a little animation # wait 10s with a little animation
echo -e -n " waiting for changes (/) ..." echo -e -n "\r\033[K waiting for changes (/) ..."
sleep 2 sleep 2
echo -e -n "\r\033[K waiting for changes (-) ..." echo -e -n "\r\033[K waiting for changes (-) ..."
sleep 2 sleep 2
@ -32,5 +33,6 @@ while true; do
sleep 2 sleep 2
echo -e -n "\r\033[K waiting for changes ( ) ... " echo -e -n "\r\033[K waiting for changes ( ) ... "
sleep 2 sleep 2
echo -e -n "\r\033[K" echo -e -n "\r\033[K checking for changes"
done done

View File

@ -349,14 +349,14 @@ pitest {
targetClasses = ['net.hostsharing.hsadminng.**'] targetClasses = ['net.hostsharing.hsadminng.**']
excludedClasses = [ excludedClasses = [
'net.hostsharing.hsadminng.config.**', 'net.hostsharing.hsadminng.config.**',
'net.hostsharing.hsadminng.**.*Controller', // 'net.hostsharing.hsadminng.**.*Controller',
'net.hostsharing.hsadminng.**.generated.**' 'net.hostsharing.hsadminng.**.generated.**'
] ]
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest'] targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*'] excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
pitestVersion = '1.15.3' pitestVersion = '1.17.0'
junit5PluginVersion = '1.1.0' junit5PluginVersion = '1.1.0'
threads = 4 threads = 4

View File

@ -3,30 +3,14 @@ package net.hostsharing.hsadminng.hs.booking.project;
import lombok.*; import lombok.*;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; 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.persistence.BaseEntity;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import static java.util.Optional.ofNullable; 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; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@MappedSuperclass @MappedSuperclass
@ -66,50 +50,4 @@ public abstract class HsBookingProject implements Stringifyable, BaseEntity<HsBo
return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") + return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") +
":" + caption; ":" + caption;
} }
public static RbacView rbac() {
return rbacViewFor("project", HsBookingProjectRbacEntity.class)
.withIdentityView(SQL.query("""
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName
FROM hs_booking.project bookingProject
JOIN hs_office.debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
"""))
.withRestrictedViewOrderBy(SQL.expression("caption"))
.withUpdatableColumns("version", "caption")
.importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(),
dependsOnColumn("debitorUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)
.importEntityAlias("debitorRel", HsOfficeRelationRbacEntity.class, usingCase(DEBITOR),
dependsOnColumn("debitorUuid"),
fetchedBySql("""
SELECT ${columns}
FROM hs_office.relation debitorRel
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = ${REF}.debitorUuid
"""),
NOT_NULL)
.toRole("debitorRel", ADMIN).grantPermission(INSERT)
.toRole(GLOBAL, ADMIN).grantPermission(DELETE)
.createRole(OWNER, (with) -> {
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");
}
} }

View File

@ -10,12 +10,14 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.lambda.Reducer; import net.hostsharing.hsadminng.lambda.Reducer;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.ToStringConverter;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
import java.net.IDN; import java.net.IDN;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.function.Function; import java.util.function.Function;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP;
@ -109,7 +111,7 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
final var subAssetResourceOptional = findSubHostingAssetResource(resourceType); final var subAssetResourceOptional = findSubHostingAssetResource(resourceType);
subAssetResourceOptional.ifPresentOrElse( subAssetResourceOptional.ifPresentOrElse(
subAssetResource -> verifyNotOverspecified(subAssetResource), this::verifyNotOverspecified,
() -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); } () -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); }
); );
@ -150,4 +152,8 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
super.persist(newHostingAsset); super.persist(newHostingAsset);
newHostingAsset.getSubHostingAssets().forEach(super::persist); newHostingAsset.getSubHostingAssets().forEach(super::persist);
} }
private <T> T ref(final Class<T> entityClass, final UUID uuid) {
return uuid != null ? emw.getReference(entityClass, uuid) : null;
}
} }

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset.factories; package net.hostsharing.hsadminng.hs.hosting.asset.factories;
import jakarta.validation.ValidationException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; 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.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import java.util.UUID;
@RequiredArgsConstructor @RequiredArgsConstructor
abstract class HostingAssetFactory { abstract class HostingAssetFactory {
@ -20,13 +20,13 @@ abstract class HostingAssetFactory {
protected abstract HsHostingAsset create(); protected abstract HsHostingAsset create();
public String performSaveProcess() { public String createAndPersist() {
try { try {
final var newHostingAsset = create(); final HsHostingAsset newHostingAsset = create();
persist(newHostingAsset); persist(newHostingAsset);
return null; return null;
} catch (final Exception e) { } catch (final ValidationException exc) {
return e.getMessage(); return exc.getMessage();
} }
} }
@ -38,8 +38,4 @@ abstract class HostingAssetFactory {
.save() .save()
.validateContext(); .validateContext();
} }
protected <T> T ref(final Class<T> entityClass, final UUID uuid) {
return uuid != null ? emw.getReference(entityClass, uuid) : null;
}
} }

View File

@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.hs.hosting.asset.factories;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.ValidationException;
import jakarta.validation.constraints.NotNull;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; 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.context.ApplicationListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> { public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
@ -28,7 +29,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
@Override @Override
@SneakyThrows @SneakyThrows
public void onApplicationEvent(final BookingItemCreatedAppEvent bookingItemCreatedAppEvent) { public void onApplicationEvent(@NotNull BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
if (containsAssetJson(bookingItemCreatedAppEvent)) { if (containsAssetJson(bookingItemCreatedAppEvent)) {
createRelatedHostingAsset(bookingItemCreatedAppEvent); createRelatedHostingAsset(bookingItemCreatedAppEvent);
} }
@ -48,7 +49,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
}; };
if (factory != null) { 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) // TODO.impl: once we implement retry, we need to amend this code (persist/merge/delete)
if (statusMessage != null) { if (statusMessage != null) {
event.getEntity().setStatusMessage(statusMessage); event.getEntity().setStatusMessage(statusMessage);
@ -68,12 +69,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
@Override @Override
protected HsHostingAsset create() { protected HsHostingAsset create() {
// TODO.impl: we should validate the asset JSON, but some violations are un-avoidable at that stage // TODO.impl: we should validate the asset JSON, but some violations are un-avoidable at that stage
return null; throw new ValidationException("waiting for manual setup of hosting asset for booking item of type " + fromBookingItem.getType());
}
@Override
public String performSaveProcess() {
return "waiting for manual setup of hosting asset for booking item of type " + fromBookingItem.getType();
} }
}; };
} }

View File

@ -56,6 +56,10 @@ public class IntegerProperty<P extends IntegerProperty<P>> extends ValidatablePr
return unit; return unit;
} }
public Integer min() {
return min;
}
public Integer max() { public Integer max() {
return max; return max;
} }

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.validation;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.mapper.Array;
import org.apache.commons.lang3.ArrayUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -83,11 +84,15 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
} }
/// predefined values, similar to fixed values in a combobox /// predefined values, similar to fixed values in a combobox
public P provided(final String... provided) { public P provided(final String firstProvidedValue, final String... moreProvidedValues) {
this.provided = provided; this.provided = ArrayUtils.addAll(new String[]{firstProvidedValue}, moreProvidedValues);
return self(); return self();
} }
public String[] provided() {
return this.provided;
}
/** /**
* The property value is not disclosed in error messages. * The property value is not disclosed in error messages.
* *
@ -109,7 +114,11 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
@Override @Override
protected String display(final String propValue) { protected String display(final String propValue) {
return undisclosed ? "provided value" : ("'" + propValue + "'"); return undisclosed
? "provided value"
: propValue != null
? ("'" + propValue + "'")
: null;
} }
@Override @Override

View File

@ -1,7 +1,10 @@
package net.hostsharing.hsadminng.lambda; package net.hostsharing.hsadminng.lambda;
import lombok.experimental.UtilityClass;
@UtilityClass
public class Reducer { public class Reducer {
public static <T> T toSingleElement(T last, T next) { public static <T> T toSingleElement(T ignoredLast, T ignoredNext) {
throw new AssertionError("only a single entity expected"); throw new AssertionError("only a single entity expected");
} }

View File

@ -1,9 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset.factories; package net.hostsharing.hsadminng.mapper;
import java.util.Arrays; import java.util.*;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
@ -16,8 +13,7 @@ public class ToStringConverter {
return this; return this;
} }
public String from(Object obj) { public String from(final Object obj) {
StringBuilder result = new StringBuilder();
return "{ " + return "{ " +
Arrays.stream(obj.getClass().getDeclaredFields()) Arrays.stream(obj.getClass().getDeclaredFields())
.filter(f -> !ignoredFields.contains(f.getName())) .filter(f -> !ignoredFields.contains(f.getName()))
@ -34,4 +30,15 @@ public class ToStringConverter {
.collect(joining(", ")) .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(", "))
+ " }";
}
} }

View File

@ -62,7 +62,7 @@ public class RbacGrantsDiagramService {
@PersistenceContext @PersistenceContext
private EntityManager em; private EntityManager em;
private Map<UUID, List<RawRbacGrantEntity>> descendantsByUuid = new HashMap<>(); private final Map<UUID, List<RawRbacGrantEntity>> descendantsByUuid = new HashMap<>();
public String allGrantsTocurrentSubject(final EnumSet<Include> includes) { public String allGrantsTocurrentSubject(final EnumSet<Include> includes) {
final var graph = new LimitedHashSet<RawRbacGrantEntity>(); final var graph = new LimitedHashSet<RawRbacGrantEntity>();
@ -231,8 +231,7 @@ public class RbacGrantsDiagramService {
} }
} }
}
record Node(String idName, UUID uuid) { record Node(String idName, UUID uuid) {
} }
}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class HsOfficeDebitorEntityUnitTest { class HsOfficeDebitorEntityUnitTest {
private HsOfficeRelationRealEntity givenDebitorRel = HsOfficeRelationRealEntity.builder() private final HsOfficeRelationRealEntity givenDebitorRel = HsOfficeRelationRealEntity.builder()
.anchor(HsOfficePersonEntity.builder() .anchor(HsOfficePersonEntity.builder()
.personType(HsOfficePersonType.LEGAL_PERSON) .personType(HsOfficePersonType.LEGAL_PERSON)
.tradeName("some partner trade name") .tradeName("some partner trade name")

View File

@ -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<? extends IntegerProperty<?>> IntegerProperty) {
IntegerProperty.verifyConsistency(Map.entry(HsBookingItemType.CLOUD_SERVER, "val"));
}
}

View File

@ -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<? extends StringProperty<?>> stringProperty) {
stringProperty.verifyConsistency(Map.entry(HsBookingItemType.CLOUD_SERVER, "val"));
}
}

View File

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

View File

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

View File

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