add hs-office-membership entity+repo + fix rbac

This commit is contained in:
Michael Hoennig 2022-10-17 19:42:14 +02:00
parent 28bdd9220d
commit e6f9484f99
10 changed files with 662 additions and 10 deletions

View File

@ -0,0 +1,88 @@
package net.hostsharing.hsadminng.hs.office.membership;
import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType;
import com.vladmihalcea.hibernate.type.range.PostgreSQLRangeType;
import com.vladmihalcea.hibernate.type.range.Range;
import lombok.*;
import net.hostsharing.hsadminng.Stringify;
import net.hostsharing.hsadminng.Stringifyable;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import javax.persistence.*;
import java.time.LocalDate;
import java.util.UUID;
import static net.hostsharing.hsadminng.Stringify.stringify;
@Entity
@Table(name = "hs_office_membership_rv")
@TypeDef(
name = "pgsql_enum",
typeClass = PostgreSQLEnumType.class
)
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("Membership")
@TypeDef(
typeClass = PostgreSQLRangeType.class,
defaultForType = Range.class
)
public class HsOfficeMembershipEntity implements Stringifyable {
private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class)
.withProp(HsOfficeMembershipEntity::getMemberNumber)
.withProp(e -> e.getPartner().toShortString())
.withProp(e -> e.getMainDebitor().toShortString())
.withProp(e -> e.getValidity().asString())
.withProp(HsOfficeMembershipEntity::getReasonForTermination)
.withSeparator(", ")
.quotedValues(false);
private @Id UUID uuid;
@ManyToOne
@JoinColumn(name = "partneruuid")
private HsOfficePartnerEntity partner;
@ManyToOne
@Fetch(FetchMode.JOIN)
@JoinColumn(name = "maindebitoruuid")
private HsOfficeDebitorEntity mainDebitor;
@Column(name = "membernumber")
private int memberNumber;
@Column(name = "validity", columnDefinition = "daterange")
private Range<LocalDate> validity;
@Column(name = "reasonfortermination")
@Enumerated(EnumType.STRING)
@Type(type = "pgsql_enum")
private HsOfficeReasonForTermination reasonForTermination;
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return String.valueOf(memberNumber);
}
@PrePersist
void init() {
if (getReasonForTermination() == null) {
setReasonForTermination(HsOfficeReasonForTermination.NONE);
}
}
}

View File

@ -0,0 +1,29 @@
package net.hostsharing.hsadminng.hs.office.membership;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsOfficeMembershipRepository extends Repository<HsOfficeMembershipEntity, UUID> {
Optional<HsOfficeMembershipEntity> findByUuid(UUID id);
@Query("""
SELECT membership FROM HsOfficeMembershipEntity membership
WHERE :memberNumber is null
OR membership.memberNumber = :memberNumber
ORDER BY membership.memberNumber
""")
List<HsOfficeMembershipEntity> findMembershipByOptionalMemberNumber(Integer memberNumber);
List<HsOfficeMembershipEntity> findMembershipsByPartnerUuid(UUID partnerUuid);
HsOfficeMembershipEntity save(final HsOfficeMembershipEntity entity);
long count();
int deleteByUuid(UUID uuid);
}

View File

@ -0,0 +1,5 @@
package net.hostsharing.hsadminng.hs.office.membership;
public enum HsOfficeReasonForTermination {
NONE, CANCELLATION, TRANSFER, DEATH, LIQUIDATION, EXPULSION;
}

View File

@ -15,7 +15,7 @@ create table if not exists hs_office_membership
mainDebitorUuid uuid not null references hs_office_debitor(uuid), mainDebitorUuid uuid not null references hs_office_debitor(uuid),
memberNumber numeric(5) not null, memberNumber numeric(5) not null,
validity daterange not null, validity daterange not null,
reasonForTermination HsOfficeReasonForTermination not null reasonForTermination HsOfficeReasonForTermination not null default 'NONE'
); );
--// --//

View File

@ -51,11 +51,13 @@ subgraph hsOfficeMembership
role:hsOfficeDebitor.admin --> role:hsOfficeMembership.agent role:hsOfficeDebitor.admin --> role:hsOfficeMembership.agent
%% outgoing %% outgoing
role:hsOfficeMembership.agent --> role:hsOfficePartner.tenant role:hsOfficeMembership.agent --> role:hsOfficePartner.tenant
role:hsOfficeMembership.admin --> role:hsOfficeDebitor.tenant role:hsOfficeMembership.agent --> role:hsOfficeDebitor.tenant
role:hsOfficeMembership.tenant[membership.tenant] role:hsOfficeMembership.tenant[membership.tenant]
%% incoming %% incoming
role:hsOfficeMembership.agent --> role:hsOfficeMembership.tenant role:hsOfficeMembership.agent --> role:hsOfficeMembership.tenant
role:hsOfficePartner.agent --> role:hsOfficeMembership.tenant
role:hsOfficeDebitor.agent --> role:hsOfficeMembership.tenant
%% outgoing %% outgoing
role:hsOfficeMembership.tenant --> role:hsOfficePartner.guest role:hsOfficeMembership.tenant --> role:hsOfficePartner.guest
role:hsOfficeMembership.tenant --> role:hsOfficeDebitor.guest role:hsOfficeMembership.tenant --> role:hsOfficeDebitor.guest
@ -65,6 +67,8 @@ subgraph hsOfficeMembership
role:hsOfficeMembership.guest --> perm:hsOfficeMembership.view{{membership.view}} role:hsOfficeMembership.guest --> perm:hsOfficeMembership.view{{membership.view}}
%% incoming %% incoming
role:hsOfficeMembership.tenant --> role:hsOfficeMembership.guest role:hsOfficeMembership.tenant --> role:hsOfficeMembership.guest
role:hsOfficePartner.tenant --> role:hsOfficeMembership.guest
role:hsOfficeDebitor.tenant --> role:hsOfficeMembership.guest
end end

View File

@ -47,26 +47,25 @@ begin
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeMembershipAdmin(NEW), hsOfficeMembershipAdmin(NEW),
permissions => array['edit'], permissions => array['edit'],
incomingSuperRoles => array[hsOfficeMembershipOwner(NEW)], incomingSuperRoles => array[hsOfficeMembershipOwner(NEW)]
outgoingSubRoles => array[hsOfficeDebitorTenant(newHsOfficeDebitor)]
); );
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeMembershipAgent(NEW), hsOfficeMembershipAgent(NEW),
incomingSuperRoles => array[hsOfficeMembershipAdmin(NEW), hsOfficePartnerAdmin(newHsOfficePartner), hsOfficeDebitorAdmin(newHsOfficeDebitor)], incomingSuperRoles => array[hsOfficeMembershipAdmin(NEW), hsOfficePartnerAdmin(newHsOfficePartner), hsOfficeDebitorAdmin(newHsOfficeDebitor)],
outgoingSubRoles => array[hsOfficePartnerTenant(newHsOfficePartner)] outgoingSubRoles => array[hsOfficePartnerTenant(newHsOfficePartner), hsOfficeDebitorTenant(newHsOfficeDebitor)]
); );
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeMembershipTenant(NEW), hsOfficeMembershipTenant(NEW),
incomingSuperRoles => array[hsOfficeMembershipAgent(NEW)], incomingSuperRoles => array[hsOfficeMembershipAgent(NEW), hsOfficePartnerAgent(newHsOfficePartner), hsOfficeDebitorAgent(newHsOfficeDebitor)],
outgoingSubRoles => array[hsOfficePartnerGuest(newHsOfficePartner), hsOfficeDebitorGuest(newHsOfficeDebitor)] outgoingSubRoles => array[hsOfficePartnerGuest(newHsOfficePartner), hsOfficeDebitorGuest(newHsOfficeDebitor)]
); );
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeMembershipGuest(NEW), hsOfficeMembershipGuest(NEW),
permissions => array['view'], permissions => array['view'],
incomingSuperRoles => array[hsOfficeMembershipTenant(NEW)] incomingSuperRoles => array[hsOfficeMembershipTenant(NEW), hsOfficePartnerTenant(newHsOfficePartner), hsOfficeDebitorTenant(newHsOfficeDebitor)]
); );
-- === END of code generated from Mermaid flowchart. === -- === END of code generated from Mermaid flowchart. ===

View File

@ -18,7 +18,7 @@ declare
newMemberNumber numeric; newMemberNumber numeric;
begin begin
idName := cleanIdentifier( forPartnerTradeName || '#' || forMainDebitorNumber); idName := cleanIdentifier( forPartnerTradeName || '#' || forMainDebitorNumber);
currentTask := 'creating SEPA-mandate test-data ' || idName; currentTask := 'creating Membership test-data ' || idName;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin'); call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
execute format('set local hsadminng.currentTask to %L', currentTask); execute format('set local hsadminng.currentTask to %L', currentTask);
@ -28,7 +28,7 @@ begin
select d.* from hs_office_debitor d where d.debitorNumber = forMainDebitorNumber into relatedDebitor; select d.* from hs_office_debitor d where d.debitorNumber = forMainDebitorNumber into relatedDebitor;
select coalesce(max(memberNumber)+1, 10001) from hs_office_membership into newMemberNumber; select coalesce(max(memberNumber)+1, 10001) from hs_office_membership into newMemberNumber;
raise notice 'creating test SEPA-mandate: %', idName; raise notice 'creating test Membership: %', idName;
raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner; raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
insert insert

View File

@ -84,7 +84,14 @@ public class ArchitectureTest {
public static final ArchRule HsOfficePartnerPackageRule = classes() public static final ArchRule HsOfficePartnerPackageRule = classes()
.that().resideInAPackage("..hs.office.partner..") .that().resideInAPackage("..hs.office.partner..")
.should().onlyBeAccessed().byClassesThat() .should().onlyBeAccessed().byClassesThat()
.resideInAnyPackage("..hs.office.partner..", "..hs.office.debitor.."); .resideInAnyPackage("..hs.office.partner..", "..hs.office.debitor..", "..hs.office.membership..");
@ArchTest
@SuppressWarnings("unused")
public static final ArchRule HsOfficeMembershipPackageRule = classes()
.that().resideInAPackage("..hs.office.membership..")
.should().onlyBeAccessed().byClassesThat()
.resideInAnyPackage("..hs.office.membership..");
@ArchTest @ArchTest
@SuppressWarnings("unused") @SuppressWarnings("unused")

View File

@ -0,0 +1,64 @@
package net.hostsharing.hsadminng.hs.office.membership;
import com.vladmihalcea.hibernate.type.range.Range;
import org.junit.jupiter.api.Test;
import javax.persistence.PrePersist;
import java.lang.reflect.InvocationTargetException;
import java.time.LocalDate;
import java.util.Arrays;
import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.testDebitor;
import static net.hostsharing.hsadminng.hs.office.partner.TestHsOfficePartner.testPartner;
import static org.assertj.core.api.Assertions.assertThat;
class HsOfficeMembershipEntityUnitTest {
final HsOfficeMembershipEntity givenMembership = HsOfficeMembershipEntity.builder()
.memberNumber(10001)
.partner(testPartner)
.mainDebitor(testDebitor)
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
.build();
@Test
void toStringContainsAllProps() {
final var result = givenMembership.toString();
assertThat(result).isEqualTo("Membership(10001, Test Ltd., 10001, [2020-01-01,))");
}
@Test
void toShortStringContainsMemberNumberOnly() {
final var result = givenMembership.toShortString();
assertThat(result).isEqualTo("10001");
}
@Test
void initializesReasonForTerminationInPrePersistIfNull() throws Exception {
final var givenUninitializedMembership = new HsOfficeMembershipEntity();
assertThat(givenUninitializedMembership.getReasonForTermination()).as("precondition failed").isNull();
invokePrePersist(givenUninitializedMembership);
assertThat(givenUninitializedMembership.getReasonForTermination()).isEqualTo(HsOfficeReasonForTermination.NONE);
}
@Test
void doesNotOverwriteReasonForTerminationInPrePersistIfNotNull() throws Exception {
givenMembership.setReasonForTermination(HsOfficeReasonForTermination.CANCELLATION);
invokePrePersist(givenMembership);
assertThat(givenMembership.getReasonForTermination()).isEqualTo(HsOfficeReasonForTermination.CANCELLATION);
}
private static void invokePrePersist(final HsOfficeMembershipEntity membershipEntity)
throws IllegalAccessException, InvocationTargetException {
final var prePersistMethod = Arrays.stream(HsOfficeMembershipEntity.class.getDeclaredMethods())
.filter(f -> f.getAnnotation(PrePersist.class) != null)
.findFirst();
assertThat(prePersistMethod).as("@PrePersist method not found").isPresent();
prePersistMethod.get().invoke(membershipEntity);
}
}

View File

@ -0,0 +1,456 @@
package net.hostsharing.hsadminng.hs.office.membership;
import com.vladmihalcea.hibernate.type.range.Range;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.context.ContextBasedTest;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository;
import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
import net.hostsharing.test.Array;
import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
import java.util.*;
import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.grantDisplaysOf;
import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.roleNamesOf;
import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { HsOfficeMembershipRepository.class, Context.class, JpaAttempt.class })
@DirtiesContext
class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTest {
@Autowired
HsOfficeMembershipRepository membershipRepo;
@Autowired
HsOfficePartnerRepository partnerRepo;
@Autowired
HsOfficeDebitorRepository debitorRepo;
@Autowired
RawRbacRoleRepository rawRoleRepo;
@Autowired
RawRbacGrantRepository rawGrantRepo;
@Autowired
EntityManager em;
@Autowired
JpaAttempt jpaAttempt;
@MockBean
HttpServletRequest request;
Set<HsOfficeMembershipEntity> tempEntities = new HashSet<>();
@Nested
class CreateMembership {
@Test
public void globalAdmin_withoutAssumedRole_canCreateNewMembership() {
// given
context("superuser-alex@hostsharing.net");
final var count = membershipRepo.count();
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0);
// when
final var result = attempt(em, () -> {
final var newMembership = toCleanup(HsOfficeMembershipEntity.builder()
.uuid(UUID.randomUUID())
.memberNumber(20001)
.partner(givenPartner)
.mainDebitor(givenDebitor)
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
.build());
return membershipRepo.save(newMembership);
});
// then
result.assertSuccessful();
assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeMembershipEntity::getUuid).isNotNull();
assertThatMembershipIsPersisted(result.returnedValue());
assertThat(membershipRepo.count()).isEqualTo(count + 1);
}
@Test
public void createsAndGrantsRoles() {
// given
context("superuser-alex@hostsharing.net");
final var initialRoleNames = roleNamesOf(rawRoleRepo.findAll());
final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll()).stream()
.map(s -> s.replace("GmbH-firstcontact", ""))
.map(s -> s.replace("hs_office_", ""))
.toList();
// when
attempt(em, () -> {
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0);
final var newMembership = toCleanup(HsOfficeMembershipEntity.builder()
.uuid(UUID.randomUUID())
.memberNumber(20002)
.partner(givenPartner)
.mainDebitor(givenDebitor)
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
.build());
return membershipRepo.save(newMembership);
});
// then
final var all = rawRoleRepo.findAll();
assertThat(roleNamesOf(all)).containsExactlyInAnyOrder(Array.from(
initialRoleNames,
"hs_office_membership#20002FirstGmbH-firstcontact.admin",
"hs_office_membership#20002FirstGmbH-firstcontact.agent",
"hs_office_membership#20002FirstGmbH-firstcontact.guest",
"hs_office_membership#20002FirstGmbH-firstcontact.owner",
"hs_office_membership#20002FirstGmbH-firstcontact.tenant"));
assertThat(grantDisplaysOf(rawGrantRepo.findAll()))
.map(s -> s.replace("GmbH-firstcontact", ""))
.map(s -> s.replace("hs_office_", ""))
.containsExactlyInAnyOrder(Array.fromFormatted(
initialGrantNames,
// owner
"{ grant perm * on membership#20002First to role membership#20002First.owner by system and assume }",
"{ grant role membership#20002First.owner to role global#global.admin by system and assume }",
// admin
"{ grant perm edit on membership#20002First to role membership#20002First.admin by system and assume }",
"{ grant role membership#20002First.admin to role membership#20002First.owner by system and assume }",
// agent
"{ grant role membership#20002First.agent to role membership#20002First.admin by system and assume }",
"{ grant role partner#First.tenant to role membership#20002First.agent by system and assume }",
"{ grant role membership#20002First.agent to role debitor#10001First.admin by system and assume }",
"{ grant role membership#20002First.agent to role partner#First.admin by system and assume }",
"{ grant role debitor#10001First.tenant to role membership#20002First.agent by system and assume }",
// tenant
"{ grant role membership#20002First.tenant to role membership#20002First.agent by system and assume }",
"{ grant role partner#First.guest to role membership#20002First.tenant by system and assume }",
"{ grant role debitor#10001First.guest to role membership#20002First.tenant by system and assume }",
"{ grant role membership#20002First.tenant to role debitor#10001First.agent by system and assume }",
"{ grant role membership#20002First.tenant to role partner#First.agent by system and assume }",
// guest
"{ grant perm view on membership#20002First to role membership#20002First.guest by system and assume }",
"{ grant role membership#20002First.guest to role membership#20002First.tenant by system and assume }",
"{ grant role membership#20002First.guest to role partner#First.tenant by system and assume }",
"{ grant role membership#20002First.guest to role debitor#10001First.tenant by system and assume }",
null));
}
private void assertThatMembershipIsPersisted(final HsOfficeMembershipEntity saved) {
final var found = membershipRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
}
}
@Nested
class FindByPartnerUuidMemberships {
@Test
public void globalAdmin_withoutAssumedRole_canViewAllMemberships() {
// given
context("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
// when
final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid());
// then
allTheseMembershipsAreReturned(result, "Membership(10001, First GmbH, 10001, [2022-10-01,), NONE)");
}
@Test
public void normalUser_canViewOnlyRelatedMemberships() {
// given:
context("person-FirstGmbH@example.com");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
// when:
final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid());
// then:
exactlyTheseMembershipsAreReturned(result, "Membership(10001, First GmbH, 10001, [2022-10-01,), NONE)");
}
}
@Nested
class FindByOptionalMemberNumber {
@Test
public void globalAdmin_canViewArbitraryMembership() {
// given
context("superuser-alex@hostsharing.net");
// when
final var result = membershipRepo.findMembershipByOptionalMemberNumber(10002);
// then
exactlyTheseMembershipsAreReturned(result, "Membership(10002, Second e.K., 10002, [2022-10-01,), NONE)");
}
@Test
public void debitorAdmin_canViewRelatedMemberships() {
// given
// context("person-FirstGmbH@example.com");
context("superuser-alex@hostsharing.net", "hs_office_partner#FirstGmbH-firstcontact.agent");
// context("superuser-alex@hostsharing.net", "hs_office_debitor#10001FirstGmbH-firstcontact.agent");
// context("superuser-alex@hostsharing.net", "hs_office_membership#10001FirstGmbH-firstcontact.admin");
// when
final var result = membershipRepo.findMembershipByOptionalMemberNumber(null);
// then
exactlyTheseMembershipsAreReturned(result, "Membership(10001, First GmbH, 10001, [2022-10-01,), NONE)");
}
}
@Nested
class UpdateMembership {
@Test
public void globalAdmin_canUpdateValidityOfArbitraryMembership() {
// given
context("superuser-alex@hostsharing.net");
final var givenMembership = givenSomeTemporaryMembership("First", "First");
assertThatMembershipIsVisibleForUserWithRole(
givenMembership,
"hs_office_debitor#10001FirstGmbH-firstcontact.admin");
assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership);
final var newValidityEnd = LocalDate.now();
// when
context("superuser-alex@hostsharing.net");
final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
givenMembership.setValidity(Range.closedOpen(
givenMembership.getValidity().lower(), newValidityEnd));
givenMembership.setReasonForTermination(HsOfficeReasonForTermination.CANCELLATION);
return toCleanup(membershipRepo.save(givenMembership));
});
// then
result.assertSuccessful();
membershipRepo.deleteByUuid(givenMembership.getUuid());
}
@Test
public void debitorAdmin_canViewButNotUpdateRelatedMembership() {
// given
context("superuser-alex@hostsharing.net");
final var givenMembership = givenSomeTemporaryMembership("First", "First");
assertThatMembershipIsVisibleForUserWithRole(
givenMembership,
"hs_office_debitor#10001FirstGmbH-firstcontact.admin");
assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership);
final var newValidityEnd = LocalDate.now();
// when
final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", "hs_office_debitor#10001FirstGmbH-firstcontact.admin");
givenMembership.setValidity(Range.closedOpen(
givenMembership.getValidity().lower(), newValidityEnd));
return membershipRepo.save(givenMembership);
});
// then
result.assertExceptionWithRootCauseMessage(JpaSystemException.class,
"[403] Subject ", " is not allowed to update hs_office_membership uuid");
}
private void assertThatMembershipExistsAndIsAccessibleToCurrentContext(final HsOfficeMembershipEntity saved) {
final var found = membershipRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().isNotSameAs(saved).usingRecursiveComparison().isEqualTo(saved);
}
private void assertThatMembershipIsVisibleForUserWithRole(
final HsOfficeMembershipEntity entity,
final String assumedRoles) {
jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", assumedRoles);
assertThatMembershipExistsAndIsAccessibleToCurrentContext(entity);
}).assertSuccessful();
}
private void assertThatMembershipIsNotVisibleForUserWithRole(
final HsOfficeMembershipEntity entity,
final String assumedRoles) {
jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", assumedRoles);
final var found = membershipRepo.findByUuid(entity.getUuid());
assertThat(found).isEmpty();
}).assertSuccessful();
}
}
@Nested
class DeleteByUuid {
@Test
public void globalAdmin_withoutAssumedRole_canDeleteAnyMembership() {
// given
context("superuser-alex@hostsharing.net", null);
final var givenMembership = givenSomeTemporaryMembership("First", "Second");
// when
final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
membershipRepo.deleteByUuid(givenMembership.getUuid());
});
// then
result.assertSuccessful();
assertThat(jpaAttempt.transacted(() -> {
context("superuser-fran@hostsharing.net", null);
return membershipRepo.findByUuid(givenMembership.getUuid());
}).assertSuccessful().returnedValue()).isEmpty();
}
@Test
public void nonGlobalAdmin_canNotDeleteTheirRelatedMembership() {
// given
context("superuser-alex@hostsharing.net");
final var givenMembership = givenSomeTemporaryMembership("First", "Third");
// when
final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", "hs_office_debitor#10003ThirdOHG-thirdcontact.admin");
assumeThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent();
membershipRepo.deleteByUuid(givenMembership.getUuid());
});
// then
result.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"[403] Subject ", " not allowed to delete hs_office_membership");
assertThat(jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
return membershipRepo.findByUuid(givenMembership.getUuid());
}).assertSuccessful().returnedValue()).isPresent(); // still there
}
@Test
public void deletingAMembershipAlsoDeletesRelatedRolesAndGrants() {
// given
context("superuser-alex@hostsharing.net");
final var initialRoleNames = Array.from(roleNamesOf(rawRoleRepo.findAll()));
final var initialGrantNames = Array.from(grantDisplaysOf(rawGrantRepo.findAll()));
final var givenMembership = givenSomeTemporaryMembership("First", "First");
assertThat(rawRoleRepo.findAll().size()).as("precondition failed: unexpected number of roles created")
.isEqualTo(initialRoleNames.length + 5);
assertThat(rawGrantRepo.findAll().size()).as("precondition failed: unexpected number of grants created")
.isEqualTo(initialGrantNames.length + 18);
// when
final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
return membershipRepo.deleteByUuid(givenMembership.getUuid());
});
// then
result.assertSuccessful();
assertThat(result.returnedValue()).isEqualTo(1);
assertThat(roleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames);
assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames);
}
}
@Test
public void auditJournalLogIsAvailable() {
// given
final var query = em.createNativeQuery("""
select c.currenttask, j.targettable, j.targetop
from tx_journal j
join tx_context c on j.contextId = c.contextId
where targettable = 'hs_office_membership';
""");
// when
@SuppressWarnings("unchecked") final List<Object[]> customerLogEntries = query.getResultList();
// then
assertThat(customerLogEntries).map(Arrays::toString).contains(
"[creating Membership test-data FirstGmbH10001, hs_office_membership, INSERT]",
"[creating Membership test-data Seconde.K.10002, hs_office_membership, INSERT]");
}
@BeforeEach
@AfterEach
void cleanup() {
tempEntities.forEach(tempMembership -> {
jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null);
System.out.println("DELETING temporary membership: " + tempMembership.toString());
membershipRepo.deleteByUuid(tempMembership.getUuid());
});
});
jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null);
em.createQuery("DELETE FROM HsOfficeMembershipEntity WHERE memberNumber >= 20000");
});
}
private HsOfficeMembershipEntity givenSomeTemporaryMembership(final String partnerTradeName, final String debitorName) {
return jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partnerTradeName).get(0);
final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).get(0);
final var newMembership = HsOfficeMembershipEntity.builder()
.uuid(UUID.randomUUID())
.memberNumber(20002)
.partner(givenPartner)
.mainDebitor(givenDebitor)
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
.build();
toCleanup(newMembership);
return membershipRepo.save(newMembership);
}).assertSuccessful().returnedValue();
}
private HsOfficeMembershipEntity toCleanup(final HsOfficeMembershipEntity tempEntity) {
tempEntities.add(tempEntity);
return tempEntity;
}
void exactlyTheseMembershipsAreReturned(
final List<HsOfficeMembershipEntity> actualResult,
final String... membershipNames) {
assertThat(actualResult)
.extracting(membershipEntity -> membershipEntity.toString())
.containsExactlyInAnyOrder(membershipNames);
}
void allTheseMembershipsAreReturned(final List<HsOfficeMembershipEntity> actualResult, final String... membershipNames) {
assertThat(actualResult)
.extracting(membershipEntity -> membershipEntity.toString())
.contains(membershipNames);
}
}