From 7dea3607f1fd1e32f9e70117cc1b80c802213916 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 6 Jan 2025 12:20:02 +0100 Subject: [PATCH] add relation constraints, especially just 1 partner relation per person. and amend test-data --- .../hsadminng/persistence/BaseEntity.java | 11 ++++++++++ .../db/changelog/1-rbac/1050-rbac-base.sql | 4 ++-- .../503-relation/5030-hs-office-relation.sql | 20 +++++++++++++++++-- .../5038-hs-office-relation-test-data.sql | 4 ++-- ...OfficeDebitorControllerAcceptanceTest.java | 16 +++++++-------- ...fficeDebitorRepositoryIntegrationTest.java | 18 +++++++++-------- ...OfficePartnerControllerAcceptanceTest.java | 14 ++++++------- ...fficePartnerRepositoryIntegrationTest.java | 7 ++++--- 8 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/persistence/BaseEntity.java b/src/main/java/net/hostsharing/hsadminng/persistence/BaseEntity.java index b3e5a535..5998407d 100644 --- a/src/main/java/net/hostsharing/hsadminng/persistence/BaseEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/persistence/BaseEntity.java @@ -1,8 +1,10 @@ package net.hostsharing.hsadminng.persistence; +import net.hostsharing.hsadminng.rbac.role.RbacRoleType; import org.hibernate.Hibernate; +import jakarta.persistence.Table; import java.util.UUID; public interface BaseEntity> { @@ -15,4 +17,13 @@ public interface BaseEntity> { //noinspection unchecked return (T) this; }; + + default String role(RbacRoleType rbacRoleType) { + if ( getUuid() == null ) { + throw new IllegalStateException("UUID missing => role can't be determined"); + } + final Table tableAnnot = getClass().getAnnotation(Table.class); + final var qualifiedTableName = tableAnnot.schema() + "." + tableAnnot.name(); + return qualifiedTableName + "#" + getUuid() + ":" + rbacRoleType.name(); + } } diff --git a/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql b/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql index d2a5c7cb..bdcd9368 100644 --- a/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql +++ b/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql @@ -251,7 +251,7 @@ begin execute sql into uuid; exception when others then - raise exception 'function %_uuid_by_id_name(''%'') failed: %, SQLSTATE: %. If it could not be found, add identity view support to %\nSQL:%', + raise exception 'function %_uuid_by_id_name(''%'') failed: %, SQLSTATE: %. If the function itself could not be found, add identity view support to %\nSQL:%', objectTable, objectIdName, SQLERRM, SQLSTATE, objectTable, sql; end; if uuid is null then @@ -275,7 +275,7 @@ begin execute sql into idName; exception when others then - raise exception 'function %_id_name_by_uuid(''%'') failed: %, SQLSTATE: %. If it could not be found, add identity view support to %', + raise exception 'function %_id_name_by_uuid(''%'') failed: %, SQLSTATE: %. If the function itself could not be found, add identity view support to %', objectTable, objectUuid, SQLERRM, SQLSTATE, objectTable; end; return idName; diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql index b470c295..813c6565 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql @@ -29,8 +29,24 @@ create table if not exists hs_office.relation ); --// --- TODO.impl: unique constraint, to prevent using the same person multiple times as a partner, or better: --- ( anchorUuid, holderUuid, type) + +-- ============================================================================ +--changeset michael.hoennig:hs-office-relation-unique-constraints endDelimiter:--// +-- ---------------------------------------------------------------------------- + +CREATE UNIQUE INDEX unique_relation_with_mark + ON hs_office.relation (type, anchorUuid, holderUuid, contactUuid, mark) + WHERE mark IS NOT NULL; + +CREATE UNIQUE INDEX unique_relation_without_mark + ON hs_office.relation (type, anchorUuid, holderUuid, contactUuid) + WHERE mark IS NULL; + +CREATE UNIQUE INDEX unique_partner_relation + ON hs_office.relation (type, anchorUuid, holderUuid) + WHERE mark IS NULL AND type = 'PARTNER'; + +--// -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql index 1e88bbe0..6673d572 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql @@ -100,8 +100,8 @@ do language plpgsql $$ call hs_office.relation_create_test_data('Third OHG', 'DEBITOR', 'Third OHG', 'third contact'); call hs_office.relation_create_test_data('Fourth eG', 'PARTNER', 'Hostsharing eG', 'fourth contact'); - call hs_office.relation_create_test_data('Fouler', 'REPRESENTATIVE', 'Third OHG', 'third contact'); - call hs_office.relation_create_test_data('Third OHG', 'DEBITOR', 'Third OHG', 'third contact'); + call hs_office.relation_create_test_data('Fouler', 'REPRESENTATIVE', 'Fourth eG', 'fourth contact'); + call hs_office.relation_create_test_data('Fourth eG', 'DEBITOR', 'Fourth eG', 'fourth contact'); call hs_office.relation_create_test_data('Smith', 'PARTNER', 'Hostsharing eG', 'sixth contact'); call hs_office.relation_create_test_data('Smith', 'DEBITOR', 'Smith', 'third contact'); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java index 81d40a0d..7d72d56b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java @@ -616,7 +616,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu context.define("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor(); - final var givenContact = contactRealRepo.findContactByOptionalCaptionLike("fourth").get(0); + final var givenContact = contactRealRepo.findContactByOptionalCaptionLike("tenth").get(0); final var location = RestAssured // @formatter:off .given() @@ -644,7 +644,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "holder": { "tradeName": "Fourth eG" }, "type": "DEBITOR", "mark": null, - "contact": { "caption": "fourth contact" } + "contact": { "caption": "tenth contact" } }, "debitorNumber": "D-10004${debitorNumberSuffix}", "debitorNumberSuffix": "${debitorNumberSuffix}", @@ -685,7 +685,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .matches(debitor -> { assertThat(debitor.getDebitorRel().getHolder().getTradeName()) .isEqualTo(givenDebitor.getDebitorRel().getHolder().getTradeName()); - assertThat(debitor.getDebitorRel().getContact().getCaption()).isEqualTo("fourth contact"); + assertThat(debitor.getDebitorRel().getContact().getCaption()).isEqualTo("tenth contact"); assertThat(debitor.getVatId()).isEqualTo("VAT222222"); assertThat(debitor.getVatCountryCode()).isEqualTo("AA"); assertThat(debitor.isVatBusiness()).isEqualTo(true); @@ -704,7 +704,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu RestAssured // @formatter:off .given() .header("current-subject", "superuser-alex@hostsharing.net") - .header("assumed-roles", "hs_office.contact#fourthcontact:ADMIN") + .header("assumed-roles", "hs_office.contact#tenthcontact:ADMIN") .contentType(ContentType.JSON) .body(""" { @@ -747,11 +747,11 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu void contactAdminUser_canNotDeleteRelatedDebitor() { context.define("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor(); - assertThat(givenDebitor.getDebitorRel().getContact().getCaption()).isEqualTo("fourth contact"); + assertThat(givenDebitor.getDebitorRel().getContact().getCaption()).isEqualTo("tenth contact"); RestAssured // @formatter:off .given() - .header("current-subject", "contact-admin@fourthcontact.example.com") + .header("current-subject", "contact-admin@tenthcontact.example.com") .port(port) .when() .delete("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid()) @@ -766,7 +766,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu void normalUser_canNotDeleteUnrelatedDebitor() { context.define("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor(); - assertThat(givenDebitor.getDebitorRel().getContact().getCaption()).isEqualTo("fourth contact"); + assertThat(givenDebitor.getDebitorRel().getContact().getCaption()).isEqualTo("tenth contact"); RestAssured // @formatter:off .given() @@ -786,7 +786,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0).load(); - final var givenContact = contactRealRepo.findContactByOptionalCaptionLike("fourth contact").get(0); + final var givenContact = contactRealRepo.findContactByOptionalCaptionLike("tenth contact").get(0); final var newDebitor = HsOfficeDebitorEntity.builder() .debitorNumberSuffix(nextDebitorSuffix()) .billable(true) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index 27bc08bc..846b6b3d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java @@ -33,6 +33,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.List; +import static net.hostsharing.hsadminng.rbac.role.RbacRoleType.ADMIN; import static net.hostsharing.hsadminng.rbac.test.EntityList.one; import static net.hostsharing.hsadminng.rbac.grant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.role.RawRbacRoleEntity.distinctRoleNamesOf; @@ -85,7 +86,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean final var count = debitorRepo.count(); final var givenPartner = partnerRepo.findPartnerByPartnerNumber(10001).orElseThrow(); final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First GmbH")); - final var givenContact = one(contactRealRepo.findContactByOptionalCaptionLike("first contact")); + final var givenContact = one(contactRealRepo.findContactByOptionalCaptionLike("eleventh contact")); // when final var result = attempt(em, () -> { @@ -119,7 +120,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // given context("superuser-alex@hostsharing.net"); final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First GmbH")); - final var givenContact = one(contactRealRepo.findContactByOptionalCaptionLike("first contact")); + final var givenContact = one(contactRealRepo.findContactByOptionalCaptionLike("eleventh contact")); // when final var result = attempt(em, () -> { @@ -335,10 +336,11 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // given context("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "fifth contact", "Fourth", "fif"); + final var originalDebitorRel = givenDebitor.getDebitorRel(); assertThatDebitorIsVisibleForUserWithRole( givenDebitor, - "hs_office.relation#FourtheG-with-DEBITOR-FourtheG:ADMIN", true); + givenDebitor.getDebitorRel().role(ADMIN), true); final var givenNewPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First")); final var givenNewBillingPerson = one(personRepo.findPersonByOptionalNameLike("Firby")); final var givenNewContact = one(contactRealRepo.findContactByOptionalCaptionLike("sixth contact")); @@ -371,10 +373,10 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // ... partner role was reassigned: assertThatDebitorIsNotVisibleForUserWithRole( result.returnedValue(), - "hs_office.relation#FourtheG-with-DEBITOR-FourtheG:ADMIN"); + originalDebitorRel.role(ADMIN)); assertThatDebitorIsVisibleForUserWithRole( result.returnedValue(), - "hs_office.relation#FirstGmbH-with-DEBITOR-FirbySusan:AGENT", true); + result.returnedValue().getDebitorRel().role(ADMIN), true); // ... contact role was reassigned: assertThatDebitorIsNotVisibleForUserWithRole( @@ -397,10 +399,10 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean public void globalAdmin_canUpdateNullRefundBankAccountToNotNullBankAccountForArbitraryDebitor() { // given context("superuser-alex@hostsharing.net"); - final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "fifth contact", null, "fig"); + final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "tenth contact", null, "fig"); assertThatDebitorIsVisibleForUserWithRole( givenDebitor, - "hs_office.relation#FourtheG-with-DEBITOR-FourtheG:ADMIN", true); + givenDebitor.getDebitorRel().role(ADMIN), true); assertThatDebitorActuallyInDatabase(givenDebitor, true); final var givenNewBankAccount = one(bankAccountRepo.findByOptionalHolderLike("first")); @@ -564,7 +566,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // when final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", "hs_office.relation#FourtheG-with-DEBITOR-FourtheG:ADMIN"); + context("superuser-alex@hostsharing.net", givenDebitor.getDebitorRel().role(ADMIN)); assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent(); debitorRepo.deleteByUuid(givenDebitor.getUuid()); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java index dc66c74b..54a4baf2 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java @@ -94,7 +94,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu context.define("superuser-alex@hostsharing.net"); final var givenMandantPerson = personRealRepo.findPersonByOptionalNameLike("Hostsharing eG").stream().findFirst().orElseThrow(); - final var givenPerson = personRealRepo.findPersonByOptionalNameLike("Third").stream().findFirst().orElseThrow(); + final var givenPerson = personRealRepo.findPersonByOptionalNameLike("Winkler").stream().findFirst().orElseThrow(); final var givenContact = contactRealRepo.findContactByOptionalCaptionLike("fourth").stream().findFirst().orElseThrow(); final var location = RestAssured // @formatter:off @@ -129,7 +129,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu "partnerNumber": "P-20002", "partnerRel": { "anchor": { "tradeName": "Hostsharing eG" }, - "holder": { "tradeName": "Third OHG" }, + "holder": { "familyName": "Winkler" }, "type": "PARTNER", "mark": null, "contact": { "caption": "fourth contact" } @@ -315,7 +315,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu context.define("superuser-alex@hostsharing.net"); final var givenPartner = givenSomeTemporaryPartnerBessler(20011); - final var givenPartnerRel = givenSomeTemporaryPartnerRel("Third OHG", "third contact"); + final var givenPartnerRel = givenSomeTemporaryPartnerRel("Winkler", "third contact"); RestAssured // @formatter:off .given() @@ -345,7 +345,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu "partnerNumber": "P-20011", "partnerRel": { "anchor": { "tradeName": "Hostsharing eG" }, - "holder": { "tradeName": "Third OHG" }, + "holder": { "familyName": "Winkler" }, "type": "PARTNER", "contact": { "caption": "third contact" } }, @@ -366,7 +366,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isPresent().get() .matches(partner -> { assertThat(partner.getPartnerNumber()).isEqualTo(givenPartner.getPartnerNumber()); - assertThat(partner.getPartnerRel().getHolder().getTradeName()).isEqualTo("Third OHG"); + assertThat(partner.getPartnerRel().getHolder().getFamilyName()).isEqualTo("Winkler"); assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("third contact"); assertThat(partner.getDetails().getRegistrationOffice()).isEqualTo("Temp Registergericht Aurich"); assertThat(partner.getDetails().getRegistrationNumber()).isEqualTo("222222"); @@ -382,7 +382,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu context.define("superuser-alex@hostsharing.net"); final var givenPartner = givenSomeTemporaryPartnerBessler(20011); - final var givenPartnerRel = givenSomeTemporaryPartnerRel("Third OHG", "third contact"); + final var givenPartnerRel = givenSomeTemporaryPartnerRel("Winkler", "third contact"); RestAssured // @formatter:off .given() @@ -404,7 +404,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu context.define("superuser-alex@hostsharing.net"); assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isPresent().get() .matches(partner -> { - assertThat(partner.getPartnerRel().getHolder().getTradeName()).isEqualTo("Third OHG"); + assertThat(partner.getPartnerRel().getHolder().getFamilyName()).isEqualTo("Winkler"); assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("third contact"); return true; }); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java index f2d09c12..8a2b8d44 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java @@ -31,6 +31,7 @@ import static net.hostsharing.hsadminng.rbac.grant.RawRbacGrantEntity.distinctGr import static net.hostsharing.hsadminng.rbac.role.RawRbacObjectEntity.objectDisplaysOf; import static net.hostsharing.hsadminng.rbac.role.RawRbacRoleEntity.distinctRoleNamesOf; import static net.hostsharing.hsadminng.mapper.Array.from; +import static net.hostsharing.hsadminng.rbac.role.RbacRoleType.ADMIN; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -76,7 +77,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean // given context("superuser-alex@hostsharing.net"); final var count = partnerRepo.count(); - final var partnerRel = givenSomeTemporaryHostsharingPartnerRel("First GmbH", "first contact"); + final var partnerRel = givenSomeTemporaryHostsharingPartnerRel("Winkler", "first contact"); // when final var result = attempt(em, () -> { @@ -269,7 +270,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean // when final var result = jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - givenPartner.setPartnerRel(givenSomeTemporaryHostsharingPartnerRel("Third OHG", "sixth contact")); + givenPartner.setPartnerRel(givenSomeTemporaryHostsharingPartnerRel("Winkler", "sixth contact")); return partnerRepo.save(givenPartner); }); @@ -281,7 +282,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean "rbac.global#global:ADMIN"); assertThatPartnerIsVisibleForUserWithRole( givenPartner, - "hs_office.person#ThirdOHG:ADMIN"); + givenPartner.getPartnerRel().getHolder().role(ADMIN)); assertThatPartnerIsNotVisibleForUserWithRole( givenPartner, "hs_office.person#ErbenBesslerMelBessler:ADMIN");