From a93143ff0041cf218e0a2da9fa451b9bab7f24af Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 4 Oct 2022 19:09:37 +0200 Subject: [PATCH] add HsOfficeBankAccount* --- .../HsOfficeBankAccountController.java | 97 +++++ .../HsOfficeBankAccountEntity.java | 48 +++ .../HsOfficeBankAccountRepository.java | 26 ++ .../hs-office/api-mappings.yaml | 2 + .../hs-office-bankaccount-schemas.yaml | 31 ++ .../hs-office-bankaccounts-with-uuid.yaml | 51 +++ .../hs-office/hs-office-bankaccounts.yaml | 56 +++ .../api-definition/hs-office/hs-office.yaml | 9 + .../changelog/240-hs-office-bankaccount.sql | 13 + .../243-hs-office-bankaccount-rbac.sql | 138 +++++++ .../248-hs-office-bankaccount-test-data.sql | 49 +++ .../db/changelog/270-hs-office-debitor.sql | 5 +- .../changelog/273-hs-office-debitor-rbac.md | 30 +- .../db/changelog/db.changelog-master.yaml | 6 + .../hsadminng/arch/ArchitectureTest.java | 11 + ...ceBankAccountControllerAcceptanceTest.java | 357 ++++++++++++++++++ .../HsOfficeBankAccountEntityUnitTest.java | 35 ++ ...eBankAccountRepositoryIntegrationTest.java | 323 ++++++++++++++++ .../bankaccount/TestHsOfficeBankAccount.java | 18 + 19 files changed, 1301 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java create mode 100644 src/main/resources/api-definition/hs-office/hs-office-bankaccount-schemas.yaml create mode 100644 src/main/resources/api-definition/hs-office/hs-office-bankaccounts-with-uuid.yaml create mode 100644 src/main/resources/api-definition/hs-office/hs-office-bankaccounts.yaml create mode 100644 src/main/resources/db/changelog/240-hs-office-bankaccount.sql create mode 100644 src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql create mode 100644 src/main/resources/db/changelog/248-hs-office-bankaccount-test-data.sql create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntityUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/TestHsOfficeBankAccount.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java new file mode 100644 index 00000000..81f15555 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java @@ -0,0 +1,97 @@ +package net.hostsharing.hsadminng.hs.office.bankaccount; + +import net.hostsharing.hsadminng.Mapper; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; + +import java.util.List; +import java.util.UUID; + +import static net.hostsharing.hsadminng.Mapper.map; + +@RestController + +public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi { + + @Autowired + private Context context; + + @Autowired + private HsOfficeBankAccountRepository bankAccountRepo; + + @Override + @Transactional(readOnly = true) + public ResponseEntity> listBankAccounts( + final String currentUser, + final String assumedRoles, + final String holder) { + context.define(currentUser, assumedRoles); + + final var entities = bankAccountRepo.findByOptionalHolderLike(holder); + + final var resources = Mapper.mapList(entities, HsOfficeBankAccountResource.class); + return ResponseEntity.ok(resources); + } + + @Override + @Transactional + public ResponseEntity addBankAccount( + final String currentUser, + final String assumedRoles, + final HsOfficeBankAccountInsertResource body) { + + context.define(currentUser, assumedRoles); + + final var entityToSave = map(body, HsOfficeBankAccountEntity.class); + entityToSave.setUuid(UUID.randomUUID()); + + final var saved = bankAccountRepo.save(entityToSave); + + final var uri = + MvcUriComponentsBuilder.fromController(getClass()) + .path("/api/hs/office/BankAccounts/{id}") + .buildAndExpand(entityToSave.getUuid()) + .toUri(); + final var mapped = map(saved, HsOfficeBankAccountResource.class); + return ResponseEntity.created(uri).body(mapped); + } + + @Override + @Transactional(readOnly = true) + public ResponseEntity getBankAccountByUuid( + final String currentUser, + final String assumedRoles, + final UUID BankAccountUuid) { + + context.define(currentUser, assumedRoles); + + final var result = bankAccountRepo.findByUuid(BankAccountUuid); + if (result.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(map(result.get(), HsOfficeBankAccountResource.class)); + } + + @Override + @Transactional + public ResponseEntity deleteBankAccountByUuid( + final String currentUser, + final String assumedRoles, + final UUID BankAccountUuid) { + context.define(currentUser, assumedRoles); + + final var result = bankAccountRepo.deleteByUuid(BankAccountUuid); + if (result == 0) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java new file mode 100644 index 00000000..90f98f13 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java @@ -0,0 +1,48 @@ +package net.hostsharing.hsadminng.hs.office.bankaccount; + +import lombok.*; +import lombok.experimental.FieldNameConstants; +import net.hostsharing.hsadminng.Stringify; +import net.hostsharing.hsadminng.Stringifyable; +import net.hostsharing.hsadminng.errors.DisplayName; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.UUID; + +import static net.hostsharing.hsadminng.Stringify.stringify; + +@Entity +@Table(name = "hs_office_bankaccount_rv") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +@DisplayName("BankAccount") +public class HsOfficeBankAccountEntity implements Stringifyable { + + private static Stringify toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount") + .withProp(Fields.holder, HsOfficeBankAccountEntity::getHolder) + .withProp(Fields.iban, HsOfficeBankAccountEntity::getIban) + .withProp(Fields.bic, HsOfficeBankAccountEntity::getBic); + + private @Id UUID uuid; + private String holder; + + private String iban; + + private String bic; + + @Override + public String toString() { + return toString.apply(this); + } + + @Override + public String toShortString() { + return holder; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java new file mode 100644 index 00000000..19264a54 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java @@ -0,0 +1,26 @@ +package net.hostsharing.hsadminng.hs.office.bankaccount; + +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 HsOfficeBankAccountRepository extends Repository { + + Optional findByUuid(UUID id); + + @Query(""" + SELECT c FROM HsOfficeBankAccountEntity c + WHERE :holder is null + OR lower(c.holder) like lower(concat(:holder, '%')) + """) + List findByOptionalHolderLike(String holder); + + HsOfficeBankAccountEntity save(final HsOfficeBankAccountEntity entity); + + int deleteByUuid(final UUID uuid); + + long count(); +} diff --git a/src/main/resources/api-definition/hs-office/api-mappings.yaml b/src/main/resources/api-definition/hs-office/api-mappings.yaml index 44ea0ea3..efab5975 100644 --- a/src/main/resources/api-definition/hs-office/api-mappings.yaml +++ b/src/main/resources/api-definition/hs-office/api-mappings.yaml @@ -20,5 +20,7 @@ map: null: org.openapitools.jackson.nullable.JsonNullable /api/hs/office/relationships/{relationshipUUID}: null: org.openapitools.jackson.nullable.JsonNullable + /api/hs/office/bankaccounts/{bankAccountUUID}: + null: org.openapitools.jackson.nullable.JsonNullable /api/hs/office/debitors/{debitorUUID}: null: org.openapitools.jackson.nullable.JsonNullable diff --git a/src/main/resources/api-definition/hs-office/hs-office-bankaccount-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-bankaccount-schemas.yaml new file mode 100644 index 00000000..29057410 --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-bankaccount-schemas.yaml @@ -0,0 +1,31 @@ + +components: + + schemas: + + HsOfficeBankAccount: + type: object + properties: + uuid: + type: string + format: uuid + holder: + type: string + iban: + type: string + bic: + type: string + + HsOfficeBankAccountInsert: + type: object + properties: + holder: + type: string + iban: + type: string + bic: + type: string + required: + - holder + - iban + - bic diff --git a/src/main/resources/api-definition/hs-office/hs-office-bankaccounts-with-uuid.yaml b/src/main/resources/api-definition/hs-office/hs-office-bankaccounts-with-uuid.yaml new file mode 100644 index 00000000..bcf80063 --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-bankaccounts-with-uuid.yaml @@ -0,0 +1,51 @@ +get: + tags: + - hs-office-bank-accounts + description: 'Fetch a single bank account by its uuid, if visible for the current subject.' + operationId: getBankAccountByUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: bankAccountUUID + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the bankaccount to fetch. + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: './hs-office-bankaccount-schemas.yaml#/components/schemas/HsOfficeBankAccount' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +delete: + tags: + - hs-office-bank-accounts + description: 'Delete a single bank account by its uuid, if permitted for the current subject.' + operationId: deleteBankAccountByUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: bankAccountUUID + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the bank account to delete. + responses: + "204": + description: No Content + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + "404": + $ref: './error-responses.yaml#/components/responses/NotFound' diff --git a/src/main/resources/api-definition/hs-office/hs-office-bankaccounts.yaml b/src/main/resources/api-definition/hs-office/hs-office-bankaccounts.yaml new file mode 100644 index 00000000..913be50f --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-bankaccounts.yaml @@ -0,0 +1,56 @@ +get: + summary: Returns a list of (optionally filtered) bankaccounts. + description: Returns the list of (optionally filtered) bankaccounts which are visible to the current user or any of it's assumed roles. + tags: + - hs-office-bank-accounts + operationId: listBankAccounts + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: holder + in: query + required: false + schema: + type: string + description: Prefix of holder to filter the results. + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + $ref: './hs-office-bankaccount-schemas.yaml#/components/schemas/HsOfficeBankAccount' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +post: + summary: Adds a new bank account. + tags: + - hs-office-bank-accounts + operationId: addBankAccount + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + requestBody: + content: + 'application/json': + schema: + $ref: './hs-office-bankaccount-schemas.yaml#/components/schemas/HsOfficeBankAccountInsert' + required: true + responses: + "201": + description: Created + content: + 'application/json': + schema: + $ref: './hs-office-bankaccount-schemas.yaml#/components/schemas/HsOfficeBankAccount' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + "409": + $ref: './error-responses.yaml#/components/responses/Conflict' diff --git a/src/main/resources/api-definition/hs-office/hs-office.yaml b/src/main/resources/api-definition/hs-office/hs-office.yaml index 127ae237..cc7a51c2 100644 --- a/src/main/resources/api-definition/hs-office/hs-office.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office.yaml @@ -44,6 +44,15 @@ paths: $ref: "./hs-office-relationships-with-uuid.yaml" + # BankAccounts + + /api/hs/office/bankaccounts: + $ref: "./hs-office-bankaccounts.yaml" + + /api/hs/office/bankaccounts/{bankAccountUUID}: + $ref: "./hs-office-bankaccounts-with-uuid.yaml" + + # Debitors /api/hs/office/debitors: diff --git a/src/main/resources/db/changelog/240-hs-office-bankaccount.sql b/src/main/resources/db/changelog/240-hs-office-bankaccount.sql new file mode 100644 index 00000000..2f7f96a9 --- /dev/null +++ b/src/main/resources/db/changelog/240-hs-office-bankaccount.sql @@ -0,0 +1,13 @@ + +-- ============================================================================ +--changeset hs-office-bankaccount-MAIN-TABLE:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +create table hs_office_bankaccount +( + uuid uuid unique references RbacObject (uuid) initially deferred, + holder varchar(27) not null, + iban varchar(34) not null, + bic varchar(11) not null +); +--// diff --git a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql new file mode 100644 index 00000000..baa63112 --- /dev/null +++ b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql @@ -0,0 +1,138 @@ +--liquibase formatted sql + +-- ============================================================================ +--changeset hs-office-bankaccount-rbac-OBJECT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRelatedRbacObject('hs_office_bankaccount'); +--// + + +-- ============================================================================ +--changeset hs-office-bankaccount-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRbacRoleDescriptors('hsOfficeBankAccount', 'hs_office_bankaccount'); +--// + + +-- ============================================================================ +--changeset hs-office-bankaccount-rbac-ROLES-CREATION:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates the roles and their assignments for a new bankaccount for the AFTER INSERT TRIGGER. + */ + +create or replace function createRbacRolesForHsOfficeBankAccount() + returns trigger + language plpgsql + strict as $$ +declare + ownerRole uuid; +begin + if TG_OP <> 'INSERT' then + raise exception 'invalid usage of TRIGGER AFTER INSERT'; + end if; + + -- the owner role with full access for the creator assigned to the current user + ownerRole := createRole( + hsOfficeBankAccountOwner(NEW), + grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['delete']), + beneathRole(globalAdmin()), + withoutSubRoles(), + withUser(currentUser()), -- TODO.spec: Who is owner of a new bankaccount? + grantedByRole(globalAdmin()) + ); + + -- TODO.spec: assumption can not be updated + -- Where bankaccounts can be created, assigned, re-assigned and deleted, they cannot be updated. + -- Thus SQL UPDATE and 'edit' permission are being implemented. + + -- the tenant role for those related users who can view the data + perform createRole( + hsOfficeBankAccountTenant(NEW), + grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']), + beneathRole(ownerRole) + ); + + return NEW; +end; $$; + +/* + An AFTER INSERT TRIGGER which creates the role structure for a new customer. + */ + +create trigger createRbacRolesForHsOfficeBankAccount_Trigger + after insert + on hs_office_bankaccount + for each row +execute procedure createRbacRolesForHsOfficeBankAccount(); +--// + + +-- ============================================================================ +--changeset hs-office-bankaccount-rbac-IDENTITY-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +call generateRbacIdentityView('hs_office_bankaccount', $idName$ + target.holder + $idName$); +--// + + +-- ============================================================================ +--changeset hs-office-bankaccount-rbac-RESTRICTED-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRbacRestrictedView('hs_office_bankaccount', 'target.holder', + $updates$ + holder = new.holder, + iban = new.iban, + bic = new.bic + $updates$); +--/ + + +-- ============================================================================ +--changeset hs-office-bankaccount-rbac-NEW-BANKACCOUNT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +/* + Creates a global permission for new-bankaccount and assigns it to the hostsharing admins role. + */ +do language plpgsql $$ + declare + addCustomerPermissions uuid[]; + globalObjectUuid uuid; + globalAdminRoleUuid uuid ; + begin + call defineContext('granting global new-bankaccount permission to global admin role', null, null, null); + + globalAdminRoleUuid := findRoleId(globalAdmin()); + globalObjectUuid := (select uuid from global); + addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-bankaccount']); + call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions); + end; +$$; + +/** + Used by the trigger to prevent the add-customer to current user respectively assumed roles. + */ +create or replace function addHsOfficeBankAccountNotAllowedForCurrentSubjects() + returns trigger + language PLPGSQL +as $$ +begin + raise exception '[403] new-bankaccount not permitted for %', + array_to_string(currentSubjects(), ';', 'null'); +end; $$; + +/** + Checks if the user or assumed roles are allowed to create a new customer. + */ +create trigger hs_office_bankaccount_insert_trigger + before insert + on hs_office_bankaccount + for each row + -- TODO.spec: who is allowed to create new bankaccounts + when ( not hasAssumedRole() ) +execute procedure addHsOfficeBankAccountNotAllowedForCurrentSubjects(); +--// + diff --git a/src/main/resources/db/changelog/248-hs-office-bankaccount-test-data.sql b/src/main/resources/db/changelog/248-hs-office-bankaccount-test-data.sql new file mode 100644 index 00000000..16560864 --- /dev/null +++ b/src/main/resources/db/changelog/248-hs-office-bankaccount-test-data.sql @@ -0,0 +1,49 @@ +--liquibase formatted sql + + +-- ============================================================================ +--changeset hs-office-bankaccount-TEST-DATA-GENERATOR:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates a single bankaccount test record. + */ +create or replace procedure createHsOfficeBankAccountTestData(givenHolder varchar, givenIBAN varchar, givenBIC varchar) + language plpgsql as $$ +declare + currentTask varchar; + emailAddr varchar; +begin + currentTask = 'creating RBAC test bankaccount ' || givenHolder; + execute format('set local hsadminng.currentTask to %L', currentTask); + + emailAddr = 'bankaccount-admin@' || cleanIdentifier(givenHolder) || '.example.com'; + call defineContext(currentTask); + perform createRbacUser(emailAddr); + call defineContext(currentTask, null, emailAddr); + + raise notice 'creating test bankaccount: %', givenHolder; + insert + into hs_office_bankaccount(uuid, holder, iban, bic) + values (uuid_generate_v4(), givenHolder, givenIBAN, givenBIC); +end; $$; +--// + + +-- ============================================================================ +--changeset hs-office-bankaccount-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--// +-- ---------------------------------------------------------------------------- + +do language plpgsql $$ + begin + -- IBANs+BICs taken from https://ibanvalidieren.de/beispiele.html + call createHsOfficeBankAccountTestData('First GmbH', 'DE02120300000000202051', 'BYLADEM1001'); + call createHsOfficeBankAccountTestData('Peter Smith', 'DE02500105170137075030', 'INGDDEFF'); + call createHsOfficeBankAccountTestData('Second e.K.', 'DE02100500000054540402', 'BELADEBE'); + call createHsOfficeBankAccountTestData('Third OHG', 'DE02300209000106531065', 'CMCIDEDD'); + call createHsOfficeBankAccountTestData('Fourth e.G.', 'DE02200505501015871393', 'HASPDEHH'); + call createHsOfficeBankAccountTestData('Mel Bessler', 'DE02100100100006820101', 'PBNKDEFF'); + call createHsOfficeBankAccountTestData('Anita Bessler', 'DE02300606010002474689', 'DAAEDEDD'); + call createHsOfficeBankAccountTestData('Paul Winkler', 'DE02600501010002034304', 'SOLADEST600'); + end; +$$; diff --git a/src/main/resources/db/changelog/270-hs-office-debitor.sql b/src/main/resources/db/changelog/270-hs-office-debitor.sql index 7582d709..30808f02 100644 --- a/src/main/resources/db/changelog/270-hs-office-debitor.sql +++ b/src/main/resources/db/changelog/270-hs-office-debitor.sql @@ -12,7 +12,8 @@ create table hs_office_debitor billingContactUuid uuid not null references hs_office_contact(uuid), vatId varchar(24), -- TODO.spec: here or in person? vatCountryCode varchar(2), - vatBusiness boolean not null -- TODO.spec: more of such? - -- TODO.impl: SEPA-mandate + bank account + vatBusiness boolean not null, -- TODO.spec: more of such? + bankAccountUuid uuid references hs_office_bankaccount(uuid) + -- TODO.impl: SEPA-mandate ); --// diff --git a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.md b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.md index 13709f6a..d89030b2 100644 --- a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.md +++ b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.md @@ -1,10 +1,28 @@ ### hs_office_debitor RBAC Roles ```mermaid -graph TD; +flowchart TB; + +subgraph bankaccount; +direction TB; + + %% oversimplified version for now + %% + %% Beware: role:debitor.tenant should NOT be granted role:bankaccount.tenent + %% because otherwise, later in the development, + %% e.g. package admins could see the debitors bank account, + %% except if we do NOT use the debitor in the hosting super module. + + %% role:bankaccount.owner + role:bankaccount.owner --> perm:bankaccount.*; +end; + +subgraph debitor[" "]; +direction TB; + %% role:debitor.owner role:debitor.owner --> perm:debitor.*; - role:global.admin --> role:debitor.owner; + role:debitor.owner --> role:bankaccount.owner; %% role:debitor.admin role:debitor.admin --> perm:debitor.edit; @@ -21,4 +39,12 @@ graph TD; role:debitor.tenant --> role:partner.tenant; role:debitor.tenant --> role:person.tenant; role:debitor.tenant --> role:contact.tenant; +end; + +subgraph global; +direction TB; + role:global.admin --> role:debitor.owner; +end; + + ``` diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 6f9b5434..17fdafa2 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -71,6 +71,12 @@ databaseChangeLog: file: db/changelog/233-hs-office-relationship-rbac.sql - include: file: db/changelog/238-hs-office-relationship-test-data.sql + - include: + file: db/changelog/240-hs-office-bankaccount.sql + - include: + file: db/changelog/243-hs-office-bankaccount-rbac.sql + - include: + file: db/changelog/248-hs-office-bankaccount-test-data.sql - include: file: db/changelog/270-hs-office-debitor.sql - include: diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index e298a7c8..b32c41a9 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -69,6 +69,16 @@ public class ArchitectureTest { .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage("..hs.office.(*).."); + // TODO.test: rule to check if all packages have rules + // TODO.test: rules for contact, person, ... + + @ArchTest + @SuppressWarnings("unused") + public static final ArchRule HsOfficeBankAccountPackageRule = classes() + .that().resideInAPackage("..hs.office.bankaccount..") + .should().onlyBeAccessed().byClassesThat() + .resideInAnyPackage("..hs.office.bankaccount..", "..hs.office.debitor.."); + @ArchTest @SuppressWarnings("unused") public static final ArchRule HsOfficePartnerPackageRule = classes() @@ -76,6 +86,7 @@ public class ArchitectureTest { .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage("..hs.office.partner..", "..hs.office.debitor.."); + @ArchTest @SuppressWarnings("unused") public static final ArchRule acceptsAnnotationOnMethodsRule = methods() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java new file mode 100644 index 00000000..a6e9c1c6 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java @@ -0,0 +1,357 @@ +package net.hostsharing.hsadminng.hs.office.bankaccount; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.Accepts; +import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.test.JpaAttempt; +import org.apache.commons.lang3.RandomStringUtils; +import org.json.JSONException; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; +import static net.hostsharing.test.JsonMatcher.lenientlyEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { HsadminNgApplication.class, JpaAttempt.class } +) +@Transactional +class HsOfficeBankAccountControllerAcceptanceTest { + + @LocalServerPort + private Integer port; + + @Autowired + Context context; + + @Autowired + Context contextMock; + + @Autowired + HsOfficeBankAccountRepository bankAccountRepo; + + @Autowired + JpaAttempt jpaAttempt; + + Set tempBankAccountUuids = new HashSet<>(); + + @Nested + @Accepts({ "bankaccount:F(Find)" }) + class ListBankAccounts { + + @Test + void globalAdmin_withoutAssumedRoles_canViewAllBankAaccounts_ifNoCriteriaGiven() throws JSONException { + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/bankaccounts") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "holder": "Anita Bessler", + "iban": "DE02300606010002474689", + "bic": "DAAEDEDD" + }, + { + "holder": "First GmbH", + "iban": "DE02120300000000202051", + "bic": "BYLADEM1001" + }, + { + "holder": "Fourth e.G.", + "iban": "DE02200505501015871393", + "bic": "HASPDEHH" + }, + { + "holder": "Mel Bessler", + "iban": "DE02100100100006820101", + "bic": "PBNKDEFF" + }, + { + "holder": "Paul Winkler", + "iban": "DE02600501010002034304", + "bic": "SOLADEST600" + }, + { + "holder": "Peter Smith", + "iban": "DE02500105170137075030", + "bic": "INGDDEFF" + }, + { + "holder": "Second e.K.", + "iban": "DE02100500000054540402", + "bic": "BELADEBE" + }, + { + "holder": "Third OHG", + "iban": "DE02300209000106531065", + "bic": "CMCIDEDD" + } + ] + """ + )); + // @formatter:on + } + } + + @Nested + @Accepts({ "bankaccount:C(Create)" }) + class AddBankAccount { + + @Test + void globalAdmin_withoutAssumedRole_canAddBankAccount() { + + context.define("superuser-alex@hostsharing.net"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "holder": "new test holder", + "iban": "DE88100900001234567892", + "bic": "BEVODEBB" + } + """) + .port(port) + .when() + .post("http://localhost/api/hs/office/bankaccounts") + .then().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("uuid", isUuidValid()) + .body("holder", is("new test holder")) + .body("iban", is("DE88100900001234567892")) + .body("bic", is("BEVODEBB")) + .header("Location", startsWith("http://localhost")) + .extract().header("Location"); // @formatter:on + + // finally, the new bankaccount can be accessed under the generated UUID + final var newUserUuid = toCleanup(UUID.fromString( + location.substring(location.lastIndexOf('/') + 1))); + assertThat(newUserUuid).isNotNull(); + } + } + + @Nested + @Accepts({ "bankaccount:R(Read)" }) + class GetBankAccount { + + @Test + void globalAdmin_withoutAssumedRole_canGetArbitraryBankAccount() { + context.define("superuser-alex@hostsharing.net"); + final var givenBankAccountUuid = bankAccountRepo.findByOptionalHolderLike("first").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/bankaccounts/" + givenBankAccountUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "holder": "First GmbH" + } + """)); // @formatter:on + } + + @Test + @Accepts({ "bankaccount:X(Access Control)" }) + void normalUser_canNotGetUnrelatedBankAccount() { + context.define("superuser-alex@hostsharing.net"); + final var givenBankAccountUuid = bankAccountRepo.findByOptionalHolderLike("first").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .get("http://localhost/api/hs/office/bankaccounts/" + givenBankAccountUuid) + .then().log().body().assertThat() + .statusCode(404); // @formatter:on + } + + @Test + @Accepts({ "bankaccount:X(Access Control)" }) + @Disabled("TODO: not implemented yet") + void bankaccountAdminUser_canGetRelatedBankAccount() { + context.define("superuser-alex@hostsharing.net"); + final var givenBankAccountUuid = bankAccountRepo.findByOptionalHolderLike("first").get(0).getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "bankaccount-admin@firstbankaccount.example.com") + .port(port) + .when() + .get("http://localhost/api/hs/office/bankaccounts/" + givenBankAccountUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "label": "first bankaccount", + "emailAddresses": "bankaccount-admin@firstbankaccount.example.com", + "phoneNumbers": "+49 123 1234567" + } + """)); // @formatter:on + } + } + + @Nested + class PatchBankAccount { + + @Test + void patchIsNotImplemented() { + + context.define("superuser-alex@hostsharing.net"); + final var givenBankAccount = givenSomeTemporaryBankAccountCreatedBy("selfregistered-test-user@hostsharing.org"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "holder": "patched holder", + "iban": "DE02701500000000594937", + "bic": "SSKMDEMM" + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/office/bankaccounts/" + givenBankAccount.getUuid()) + .then().assertThat() + .statusCode(405); + // @formatter:on + + // and the bankaccount is unchanged + context.define("superuser-alex@hostsharing.net"); + assertThat(bankAccountRepo.findByUuid(givenBankAccount.getUuid())).isPresent().get() + .matches(person -> { + assertThat(person.getHolder()).isEqualTo(givenBankAccount.getHolder()); + assertThat(person.getIban()).isEqualTo(givenBankAccount.getIban()); + assertThat(person.getBic()).isEqualTo(givenBankAccount.getBic()); + return true; + }); + } + } + + @Nested + @Accepts({ "bankaccount:D(Delete)" }) + class DeleteBankAccount { + + @Test + void globalAdmin_withoutAssumedRole_canDeleteArbitraryBankAccount() { + context.define("superuser-alex@hostsharing.net"); + final var givenBankAccount = givenSomeTemporaryBankAccountCreatedBy("selfregistered-test-user@hostsharing.org"); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .delete("http://localhost/api/hs/office/bankaccounts/" + givenBankAccount.getUuid()) + .then().log().body().assertThat() + .statusCode(204); // @formatter:on + + // then the given bankaccount is gone + assertThat(bankAccountRepo.findByUuid(givenBankAccount.getUuid())).isEmpty(); + } + + @Test + @Accepts({ "bankaccount:X(Access Control)" }) + void bankaccountOwner_canDeleteRelatedBankAaccount() { + final var givenBankAccount = givenSomeTemporaryBankAccountCreatedBy("selfregistered-test-user@hostsharing.org"); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-test-user@hostsharing.org") + .port(port) + .when() + .delete("http://localhost/api/hs/office/bankaccounts/" + givenBankAccount.getUuid()) + .then().log().body().assertThat() + .statusCode(204); // @formatter:on + + // then the given bankaccount is still there + assertThat(bankAccountRepo.findByUuid(givenBankAccount.getUuid())).isEmpty(); + } + + @Test + @Accepts({ "bankaccount:X(Access Control)" }) + void normalUser_canNotDeleteUnrelatedBankAccount() { + context.define("superuser-alex@hostsharing.net"); + final var givenBankAccount = givenSomeTemporaryBankAccountCreatedBy("selfregistered-test-user@hostsharing.org"); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .delete("http://localhost/api/hs/office/bankaccounts/" + givenBankAccount.getUuid()) + .then().log().body().assertThat() + .statusCode(404); // unrelated user cannot even view the bankaccount + // @formatter:on + + // then the given bankaccount is still there + assertThat(bankAccountRepo.findByUuid(givenBankAccount.getUuid())).isNotEmpty(); + } + } + + private HsOfficeBankAccountEntity givenSomeTemporaryBankAccountCreatedBy(final String creatingUser) { + return jpaAttempt.transacted(() -> { + context.define(creatingUser); + final var newBankAccount = HsOfficeBankAccountEntity.builder() + .uuid(UUID.randomUUID()) + .holder("temp acc #" + RandomStringUtils.randomAlphabetic(3)) + .iban("DE93500105179473626226") + .bic("INGDDEFFXXX") + .build(); + + toCleanup(newBankAccount.getUuid()); + + return bankAccountRepo.save(newBankAccount); + }).assertSuccessful().returnedValue(); + } + + private UUID toCleanup(final UUID tempBankAccountUuid) { + tempBankAccountUuids.add(tempBankAccountUuid); + return tempBankAccountUuid; + } + + @BeforeEach + @AfterEach + void cleanup() { + tempBankAccountUuids.forEach(uuid -> { + jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net", null); + System.out.println("DELETING temporary bankaccount: " + uuid); + final var count = bankAccountRepo.deleteByUuid(uuid); + System.out.println("DELETED temporary bankaccount: " + uuid + (count > 0 ? " successful" : " failed")); + }).assertSuccessful(); + }); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntityUnitTest.java new file mode 100644 index 00000000..9fea3b5e --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntityUnitTest.java @@ -0,0 +1,35 @@ +package net.hostsharing.hsadminng.hs.office.bankaccount; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HsOfficeBankAccountEntityUnitTest { + + @Test + void toStringReturnsNullForNullBankAccount() { + final HsOfficeBankAccountEntity givenBankAccount = null; + assertThat("" + givenBankAccount).isEqualTo("null"); + } + + @Test + void toStringReturnsAllProperties() { + final var givenBankAccount = HsOfficeBankAccountEntity.builder() + .holder("given holder") + .iban("DE02370502990000684712") + .bic("COKSDE33") + .build(); + assertThat("" + givenBankAccount).isEqualTo("bankAccount(holder='given holder', iban='DE02370502990000684712', bic='COKSDE33')"); + } + + @Test + void toShotStringReturnsHolder() { + final var givenBankAccount = HsOfficeBankAccountEntity.builder() + .holder("given holder") + .iban("DE02370502990000684712") + .bic("COKSDE33") + .build(); + assertThat(givenBankAccount.toShortString()).isEqualTo("given holder"); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java new file mode 100644 index 00000000..890d218a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java @@ -0,0 +1,323 @@ +package net.hostsharing.hsadminng.hs.office.bankaccount; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.context.ContextBasedTest; +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.modelmapper.internal.bytebuddy.utility.RandomString; +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.test.annotation.DirtiesContext; +import org.testcontainers.junit.jupiter.Container; + +import javax.persistence.EntityManager; +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.function.Supplier; + +import static net.hostsharing.hsadminng.hs.office.bankaccount.TestHsOfficeBankAccount.hsOfficeBankAccount; +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; + +@DataJpaTest +@ComponentScan(basePackageClasses = { HsOfficeBankAccountRepository.class, Context.class, JpaAttempt.class }) +@DirtiesContext +class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { + + @Autowired + HsOfficeBankAccountRepository bankaccountRepo; + + @Autowired + RawRbacRoleRepository rawRoleRepo; + + @Autowired + RawRbacGrantRepository rawGrantRepo; + + @Autowired + EntityManager em; + + @Autowired + JpaAttempt jpaAttempt; + + @MockBean + HttpServletRequest request; + + @Container + Container postgres; + + @Nested + class CreateBankAccount { + + @Test + public void globalAdmin_withoutAssumedRole_canCreateNewBankAccount() { + // given + context("superuser-alex@hostsharing.net"); + final var count = bankaccountRepo.count(); + + // when + final var result = attempt(em, () -> bankaccountRepo.save( + hsOfficeBankAccount("some temp acc A", "DE37500105177419788228", ""))); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeBankAccountEntity::getUuid).isNotNull(); + assertThatBankAccountIsPersisted(result.returnedValue()); + assertThat(bankaccountRepo.count()).isEqualTo(count + 1); + } + + @Test + public void arbitraryUser_canCreateNewBankAccount() { + // given + context("selfregistered-user-drew@hostsharing.org"); + final var count = bankaccountRepo.count(); + + // when + final var result = attempt(em, () -> bankaccountRepo.save( + hsOfficeBankAccount("some temp acc B", "DE49500105174516484892", "INGDDEFFXXX"))); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeBankAccountEntity::getUuid).isNotNull(); + assertThatBankAccountIsPersisted(result.returnedValue()); + assertThat(bankaccountRepo.count()).isEqualTo(count + 1); + } + + @Test + public void createsAndGrantsRoles() { + // given + context("selfregistered-user-drew@hostsharing.org"); + final var initialRoleNames = roleNamesOf(rawRoleRepo.findAll()); + final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll()); + + // when + attempt(em, () -> bankaccountRepo.save( + hsOfficeBankAccount("some temp acc C", "DE25500105176934832579", "INGDDEFFXXX")) + ).assumeSuccessful(); + + // then + final var roles = rawRoleRepo.findAll(); + assertThat(roleNamesOf(roles)).containsExactlyInAnyOrder(Array.from( + initialRoleNames, + "hs_office_bankaccount#sometempaccC.owner", + "hs_office_bankaccount#sometempaccC.tenant" + )); + assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.from( + initialGrantNames, + "{ grant role hs_office_bankaccount#sometempaccC.owner to role global#global.admin by system and assume }", + "{ grant perm delete on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.owner by system and assume }", + "{ grant role hs_office_bankaccount#sometempaccC.tenant to role hs_office_bankaccount#sometempaccC.owner by system and assume }", + "{ grant perm view on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.tenant by system and assume }", + "{ grant role hs_office_bankaccount#sometempaccC.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }" + )); + } + + private void assertThatBankAccountIsPersisted(final HsOfficeBankAccountEntity saved) { + final var found = bankaccountRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); + } + } + + @Nested + class FindAllBankAccounts { + + @Test + public void globalAdmin_withoutAssumedRole_canViewAllBankAccounts() { + // given + context("superuser-alex@hostsharing.net"); + + // when + final var result = bankaccountRepo.findByOptionalHolderLike(null); + + // then + allTheseBankAccountsAreReturned( + result, + "Anita Bessler", + "First GmbH", + "Fourth e.G.", + "Mel Bessler", + "Paul Winkler", + "Peter Smith", + "Second e.K.", + "Third OHG"); + } + + @Test + public void arbitraryUser_canViewOnlyItsOwnBankAccount() { + // given: + final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); + + // when: + context("selfregistered-user-drew@hostsharing.org"); + final var result = bankaccountRepo.findByOptionalHolderLike(null); + + // then: + exactlyTheseBankAccountsAreReturned(result, givenBankAccount.getHolder()); + } + } + + @Nested + class FindByLabelLike { + + @Test + public void globalAdmin_withoutAssumedRole_canViewAllBankAccounts() { + // given + context("superuser-alex@hostsharing.net", null); + + // when + final var result = bankaccountRepo.findByOptionalHolderLike(null); + + // then + exactlyTheseBankAccountsAreReturned( + result, + "Anita Bessler", + "First GmbH", + "Fourth e.G.", + "Mel Bessler", + "Paul Winkler", + "Peter Smith", + "Second e.K.", + "Third OHG"); + } + + @Test + public void arbitraryUser_withoutAssumedRole_canViewOnlyItsOwnBankAccount() { + // given: + final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); + + // when: + context("selfregistered-user-drew@hostsharing.org"); + final var result = bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); + + // then: + exactlyTheseBankAccountsAreReturned(result, givenBankAccount.getHolder()); + } + } + + @Nested + class DeleteByUuid { + + @Test + public void globalAdmin_withoutAssumedRole_canDeleteAnyBankAccount() { + // given + context("superuser-alex@hostsharing.net", null); + final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", null); + bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", null); + return bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); + }).assertSuccessful().returnedValue()).hasSize(0); + } + + @Test + public void arbitraryUser_withoutAssumedRole_canDeleteABankAccountCreatedByItself() { + // given + final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); + + // when + final var result = jpaAttempt.transacted(() -> { + context("selfregistered-user-drew@hostsharing.org", null); + bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", null); + return bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); + }).assertSuccessful().returnedValue()).hasSize(0); + } + + @Test + public void deletingABankAccountAlsoDeletesRelatedRolesAndGrants() { + // given + context("selfregistered-user-drew@hostsharing.org", null); + final var initialRoleNames = roleNamesOf(rawRoleRepo.findAll()); + final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll()); + final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); + assertThat(rawRoleRepo.findAll().size()).as("unexpected number of roles created") + .isEqualTo(initialRoleNames.size() + 2); + assertThat(rawGrantRepo.findAll().size()).as("unexpected number of grants created") + .isEqualTo(initialGrantNames.size() + 5); + + // when + final var result = jpaAttempt.transacted(() -> { + context("selfregistered-user-drew@hostsharing.org", null); + return bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isEqualTo(1); + assertThat(roleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(Array.from( + initialRoleNames + )); + assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.from( + initialGrantNames + )); + } + } + + private HsOfficeBankAccountEntity givenSomeTemporaryBankAccount( + final String createdByUser, + Supplier entitySupplier) { + return jpaAttempt.transacted(() -> { + context(createdByUser); + return bankaccountRepo.save(entitySupplier.get()); + }).assertSuccessful().returnedValue(); + } + + @BeforeEach + @AfterEach + void cleanup() { + context("superuser-alex@hostsharing.net", null); + final var result = bankaccountRepo.findByOptionalHolderLike("some temp acc"); + result.forEach(tempPerson -> { + System.out.println("DELETING temporary bankaccount: " + tempPerson.getHolder()); + bankaccountRepo.deleteByUuid(tempPerson.getUuid()); + }); + } + + private HsOfficeBankAccountEntity givenSomeTemporaryBankAccount(final String createdByUser) { + final var random = RandomString.make(3); + return givenSomeTemporaryBankAccount(createdByUser, () -> + hsOfficeBankAccount( + "some temp acc #" + random, + "DE41500105177739718697", + "INGDDEFFXXX" + )); + } + + void exactlyTheseBankAccountsAreReturned( + final List actualResult, + final String... bankaccountLabels) { + assertThat(actualResult) + .extracting(HsOfficeBankAccountEntity::getHolder) + .containsExactlyInAnyOrder(bankaccountLabels); + } + + void allTheseBankAccountsAreReturned( + final List actualResult, + final String... bankaccountLabels) { + assertThat(actualResult) + .extracting(HsOfficeBankAccountEntity::getHolder) + .contains(bankaccountLabels); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/TestHsOfficeBankAccount.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/TestHsOfficeBankAccount.java new file mode 100644 index 00000000..3ce8601d --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/TestHsOfficeBankAccount.java @@ -0,0 +1,18 @@ +package net.hostsharing.hsadminng.hs.office.bankaccount; + +import java.util.UUID; + +public class TestHsOfficeBankAccount { + + public static final HsOfficeBankAccountEntity someBankAccount = + hsOfficeBankAccount("some bankaccount", "DE67500105173931168623", "INGDDEFFXXX"); + + static public HsOfficeBankAccountEntity hsOfficeBankAccount(final String holder, final String iban, final String bic) { + return HsOfficeBankAccountEntity.builder() + .uuid(UUID.randomUUID()) + .holder(holder) + .iban(iban) + .bic(bic) + .build(); + } +}