diff --git a/README.md b/README.md index 61b93490..1ddad2f4 100644 --- a/README.md +++ b/README.md @@ -227,3 +227,21 @@ You can explore the prototype as follows: (the example tables are currently not compatible with RBAC), - then run `historization.sql` in the database, - finally run `examples.sql` in the database. + +## How To + +### How to Use a Persistent Database for Integration Tests? + +Usually, the `DataJpaTest` integration tests run against a database in a temporary docker container. +As soon as the test ends, the database is gone; this might make debugging difficult. + +Alternatively + +If the persistent database and the temporary database show different results, one of these reasons could be the cause: + +1. You might have some changesets only running in either context, + check the `context: ...` in the changeset control lines. +2. You might have changes in the database which interfere with the tests, + e.g. from a previous run of tests or manually applied. + It's best to run `pg-sql-reset && gw bootRun` before each test run, to have a clean database. + diff --git a/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql b/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql index 8255ca2b..2929adec 100644 --- a/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql +++ b/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql @@ -1,6 +1,8 @@ --liquibase formatted sql ---changeset rbac-base-reference:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-REFERENCE:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -27,7 +29,9 @@ end; $$; --// ---changeset rbac-base-user:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-USER:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -86,7 +90,9 @@ $$; --// ---changeset rbac-base-object:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-OBJECT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -119,7 +125,9 @@ end; $$; --// ---changeset rbac-base-role:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-ROLE:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -203,7 +211,9 @@ begin end; $$; ---changeset rbac-base-permission:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-PERMISSION:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -217,7 +227,6 @@ create domain RbacOp as varchar(67) or VALUE ~ '^add-[a-z]+$' ); --- DROP TABLE IF EXISTS RbacPermission; create table RbacPermission ( uuid uuid primary key references RbacReference (uuid) on delete cascade, @@ -226,11 +235,7 @@ create table RbacPermission unique (objectUuid, op) ); --- SET SESSION SESSION AUTHORIZATION DEFAULT; --- alter table rbacpermission add constraint rbacpermission_objectuuid_fkey foreign key (objectUuid) references rbacobject(uuid); --- alter table rbacpermission drop constraint rbacpermission_objectuuid; - -create or replace function hasPermission(forObjectUuid uuid, forOp RbacOp) +create or replace function permissionExists(forObjectUuid uuid, forOp RbacOp) returns bool language sql as $$ select exists( @@ -291,7 +296,9 @@ $$; --// ---changeset rbac-base-grants:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-GRANTS:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -306,8 +313,6 @@ create index on RbacGrants (ascendantUuid); create index on RbacGrants (descendantUuid); ---// - create or replace function findGrantees(grantedId uuid) returns setof RbacReference returns null on null input @@ -377,7 +382,8 @@ begin insert into RbacGrants (ascendantUuid, descendantUuid, follow) - values (roleUuid, permissionIds[i], true); + values (roleUuid, permissionIds[i], true) + on conflict do nothing; -- allow granting multiple times end loop; end; $$; @@ -395,7 +401,7 @@ begin insert into RbacGrants (ascendantUuid, descendantUuid, follow) values (superRoleId, subRoleId, doFollow) - on conflict do nothing; -- TODO: remove? + on conflict do nothing; -- allow granting multiple times end; $$; create or replace procedure revokeRoleFromRole(subRoleId uuid, superRoleId uuid) @@ -418,11 +424,13 @@ begin insert into RbacGrants (ascendantUuid, descendantUuid, follow) values (userId, roleId, true) - on conflict do nothing; -- TODO: remove? + on conflict do nothing; -- allow granting multiple times end; $$; --// ---changeset rbac-base-query-accessible-object-uuids:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -467,7 +475,9 @@ $$; --// ---changeset rbac-base-query-granted-permissions:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-QUERY-GRANTED-PERMISSIONS:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -494,7 +504,9 @@ $$; --// ---changeset rbac-base-query-users-with-permission-for-object:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-base-QUERY-USERS-WITH-PERMISSION-FOR-OBJECT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -520,7 +532,9 @@ $$; --// ---changeset rbac-current-user:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-CURRENT-USER:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -553,13 +567,16 @@ declare begin currentUser := currentUser(); currentUserId = (select uuid from RbacUser where name = currentUser); + if currentUserId is null then + raise exception 'hsadminng.currentUser defined as %, but does not exists', currentUser; + end if; return currentUserId; end; $$; - - --// ---changeset rbac-assumed-roles:1 endDelimiter:--// +-- ============================================================================ +--changeset rbac-ASSUMED-ROLES:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- /* */ @@ -595,7 +612,7 @@ create or replace function findUuidByIdName(objectTable varchar, objectIdName va returns null on null input language plpgsql as $$ declare - sql varchar; + sql varchar; uuid uuid; begin objectTable := pureIdentifier(objectTable); @@ -604,10 +621,26 @@ begin begin raise notice 'sql: %', sql; execute sql into uuid; - exception when OTHERS then - raise exception 'function %UuidByIdName(...) not found, add identity view support for table %', objectTable, objectTable; + exception + when others then + raise exception 'function %UuidByIdName(...) not found, add identity view support for table %', objectTable, objectTable; end; return uuid; +end ; $$; + +create or replace function currentSubjects() + returns varchar(63)[] + stable leakproof + language plpgsql as $$ +declare + assumedRoles varchar(63)[]; +begin + assumedRoles := assumedRoles(); + if array_length(assumedRoles(), 1) > 0 then + return assumedRoles(); + else + return array[currentUser()]::varchar(63)[]; + end if; end; $$; create or replace function currentSubjectIds() @@ -664,9 +697,8 @@ end; $$; -- ============================================================================ --- PGSQL-ROLES ---changeset rbac-base-pgsql-roles:1 endDelimiter:--// --- ------------------------------------------------------------------ +--changeset rbac-base-PGSQL-ROLES:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- create role admin; grant all privileges on all tables in schema public to admin; @@ -675,3 +707,4 @@ create role restricted; grant all privileges on all tables in schema public to restricted; --// + diff --git a/src/main/resources/db/changelog/2022-07-29-050-hs-base.sql b/src/main/resources/db/changelog/2022-07-29-050-hs-base.sql index d1b42878..f8edf9a5 100644 --- a/src/main/resources/db/changelog/2022-07-29-050-hs-base.sql +++ b/src/main/resources/db/changelog/2022-07-29-050-hs-base.sql @@ -16,6 +16,8 @@ create table Global ); create unique index Global_Singleton on Global ((0)); +grant select on global to restricted; + /** A single row to be referenced as a global object. */ @@ -25,6 +27,23 @@ insert into Global (uuid, name) values ((select uuid from RbacObject where objectTable = 'global'), 'hostsharing'); --// + +-- ============================================================================ +--changeset rhs-base-HAS-GLOBAL-PERMISSION:1 endDelimiter:--// +-- ------------------------------------------------------------------ + +create or replace function hasGlobalPermission(op RbacOp) + returns boolean + language sql as +$$ + -- TODO: this could to be optimized +select (select uuid from global) in + (select queryAccessibleObjectUuidsOfSubjectIds( + op, 'global', currentSubjectIds())); +$$; +--// + + -- ============================================================================ --changeset hs-base-GLOBAL-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -34,7 +53,7 @@ insert */ drop view if exists global_iv; create or replace view global_iv as -select distinct target.uuid, target.name as idName +select target.uuid, target.name as idName from global as target; grant all privileges on global_iv to restricted; @@ -65,7 +84,7 @@ $$; select createRole(hostsharingAdmin()); -- ============================================================================ ---changeset hs-base-ADMIN-USERS:1 context:dev,test,tc endDelimiter:--// +--changeset hs-base-ADMIN-USERS:1 context:dev,tc endDelimiter:--// -- ---------------------------------------------------------------------------- /* Create two users and assign both to the administrators role. @@ -83,7 +102,7 @@ $$; -- ============================================================================ ---changeset hs-base-hostsharing-TEST:1 context:dev,test,tc runAlways:true endDelimiter:--// +--changeset hs-base-hostsharing-TEST:1 context:dev,tc runAlways:true endDelimiter:--// -- ---------------------------------------------------------------------------- /* diff --git a/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql b/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql index 3c7d3bba..c7baea62 100644 --- a/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql +++ b/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql @@ -149,7 +149,7 @@ execute procedure deleteRbacRulesForCustomer(); */ drop view if exists customer_iv; create or replace view customer_iv as -select distinct target.uuid, target.prefix as idName +select target.uuid, target.prefix as idName from customer as target; -- TODO: Is it ok that everybody has access to this information? grant all privileges on customer_iv to restricted; @@ -176,8 +176,51 @@ $$; set session session authorization default; drop view if exists customer_rv; create or replace view customer_rv as -select distinct target.* +select target.* from customer as target where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'customer', currentSubjectIds())); grant all privileges on customer_rv to restricted; --// + + +-- ============================================================================ +--changeset hs-customer-rbac-ADD-CUSTOMER:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +/* + Creates a global permission for add-customer and assigns it to the hostsharing admins role. + */ +do language plpgsql $$ + declare + addCustomerPermissions uuid[]; + hostsharingObjectUuid uuid; + hsAdminRoleUuid uuid ; + begin + hsAdminRoleUuid := findRoleId(hostsharingAdmin()); + hostsharingObjectUuid := (select uuid from global); + addCustomerPermissions := createPermissions(hostsharingObjectUuid, array ['add-customer']); + call grantPermissionsToRole(hsAdminRoleUuid, addCustomerPermissions); + end; +$$; + +/** + Used by the trigger to prevent the add-customer to current user respectively assumed roles. + */ +create or replace function addCustomerNotAllowedForCurrentSubjects() + returns trigger + language PLPGSQL +as $$ +begin + raise exception 'add-customer not permitted for %', array_to_string(currentSubjects()); +end; $$; + +/** + Checks if the user or assumed roles are allowed to add a new customer. + */ +create trigger customer_insert_trigger + before insert + on customer + for each row + when ( currentUser() <> 'mike@hostsharing.net' or not hasGlobalPermission('add-customer') ) +execute procedure addCustomerNotAllowedForCurrentSubjects(); +--// + diff --git a/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql b/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql index 8dc717f9..ea54d12a 100644 --- a/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql +++ b/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql @@ -175,7 +175,7 @@ $$; */ drop view if exists package_rv; create or replace view package_rv as -select distinct target.* +select target.* from package as target where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'package', currentSubjectIds())); grant all privileges on package_rv to restricted; diff --git a/src/main/resources/db/changelog/23-hs-unixuser.sql b/src/main/resources/db/changelog/23-hs-unixuser.sql index 98e640e0..233ef5a8 100644 --- a/src/main/resources/db/changelog/23-hs-unixuser.sql +++ b/src/main/resources/db/changelog/23-hs-unixuser.sql @@ -115,7 +115,7 @@ set session session authorization default; -- ALTER TABLE unixuser ENABLE ROW LEVEL SECURITY; drop view if exists unixuser_rv; create or replace view unixuser_rv as -select distinct target.* +select target.* from unixuser as target where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'unixuser', currentSubjectIds())); grant all privileges on unixuser_rv to restricted; diff --git a/src/main/resources/db/changelog/24-hs-domain.sql b/src/main/resources/db/changelog/24-hs-domain.sql index ac7d7205..ece72c8a 100644 --- a/src/main/resources/db/changelog/24-hs-domain.sql +++ b/src/main/resources/db/changelog/24-hs-domain.sql @@ -100,7 +100,7 @@ set session session authorization default; -- ALTER TABLE Domain ENABLE ROW LEVEL SECURITY; drop view if exists domain_rv; create or replace view domain_rv as -select distinct target.* +select target.* from Domain as target where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'domain', currentSubjectIds())); grant all privileges on domain_rv to restricted; diff --git a/src/main/resources/db/changelog/25-hs-emailaddress.sql b/src/main/resources/db/changelog/25-hs-emailaddress.sql index 97b884a7..9aa82621 100644 --- a/src/main/resources/db/changelog/25-hs-emailaddress.sql +++ b/src/main/resources/db/changelog/25-hs-emailaddress.sql @@ -85,7 +85,7 @@ set session session authorization default; -- ALTER TABLE EMailAddress ENABLE ROW LEVEL SECURITY; drop view if exists EMailAddress_rv; create or replace view EMailAddress_rv as -select distinct target.* +select target.* from EMailAddress as target where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'emailaddress', currentSubjectIds())); grant all privileges on EMailAddress_rv to restricted; diff --git a/src/test/java/net/hostsharing/hsadminng/hscustomer/CustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hscustomer/CustomerRepositoryIntegrationTest.java index 642405dd..b516948a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hscustomer/CustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hscustomer/CustomerRepositoryIntegrationTest.java @@ -6,15 +6,15 @@ 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.context.annotation.ComponentScan; -import org.springframework.core.NestedRuntimeException; import org.springframework.orm.jpa.JpaSystemException; import javax.persistence.EntityManager; +import javax.persistence.PersistenceException; import javax.transaction.Transactional; import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; +import java.util.UUID; +import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @@ -30,153 +30,179 @@ class CustomerRepositoryIntegrationTest { @Autowired EntityManager em; @Nested - class FindAll { - - private final Given given = new Given(); - private When> when; - private final Then then = new Then(); + class CreateCustomer { @Test - public void hostsharingAdminWithoutAssumedRoleCanViewAllCustomers() { - given.currentUser("mike@hostsharing.net"); + public void hostsharingAdmin_withoutAssumedRole_canCreateNewCustomer() { + // given + currentUser("mike@hostsharing.net"); - when(() -> customerRepository.findAll()); + // when + final var newCustomer = new CustomerEntity( + UUID.randomUUID(), "xxx", 90001, "admin@xxx.example.com"); + final var result = customerRepository.save(newCustomer); - then.exactlyTheseCustomersAreReturned("aaa", "aab", "aac"); + // then + assertThat(result).isNotNull().extracting(CustomerEntity::getUuid).isNotNull(); + assertThatCustomerIsPersisted(result); } @Test - public void hostsharingAdminWithAssumedHostsharingAdminRoleCanViewAllCustomers() { - given.currentUser("mike@hostsharing.net"). - and().assumedRoles("global#hostsharing.admin"); + public void hostsharingAdmin_withAssumedCustomerRole_cannotCreateNewCustomer() { + // given + currentUser("mike@hostsharing.net"); + assumedRoles("customer#aaa.admin"); - when(() -> customerRepository.findAll()); + // when + final var attempt = attempt(em, () -> { + final var newCustomer = new CustomerEntity( + UUID.randomUUID(), "xxx", 90001, "admin@xxx.example.com"); + return customerRepository.save(newCustomer); + }); - then.exactlyTheseCustomersAreReturned("aaa", "aab", "aac"); + // then + attempt.assertExceptionWithRootCauseMessage( + PersistenceException.class, + "add-customer not permitted for customer#aaa.admin"); } @Test - public void customerAdminWithoutAssumedRoleCanViewOnlyItsOwnCustomer() { - given.currentUser("admin@aaa.example.com"); + public void customerAdmin_withoutAssumedRole_cannotCreateNewCustomer() { + // given + currentUser("admin@aaa.example.com"); - when(() -> customerRepository.findAll()); + // when + final var attempt = attempt(em, () -> { + final var newCustomer = new CustomerEntity( + UUID.randomUUID(), "yyy", 90002, "admin@yyy.example.com"); + return customerRepository.save(newCustomer); + }); - then.exactlyTheseCustomersAreReturned("aaa"); + // then + attempt.assertExceptionWithRootCauseMessage( + PersistenceException.class, + "add-customer not permitted for admin@aaa.example.com"); + + } + + private void assertThatCustomerIsPersisted(final CustomerEntity saved) { + final var found = customerRepository.findById(saved.getUuid()); + assertThat(found).hasValue(saved); + } + } + + @Nested + class FindAllCustomers { + + @Test + public void hostsharingAdmin_withoutAssumedRole_canViewAllCustomers() { + // given + currentUser("mike@hostsharing.net"); + + // when + final var result = customerRepository.findAll(); + + // then + exactlyTheseCustomersAreReturned(result, "aaa", "aab", "aac"); } @Test - public void customerAdminWithAssumedOwnedPackageAdminRoleCanViewOnlyItsOwnCustomer() { - given.currentUser("admin@aaa.example.com"). - and().assumedRoles("package#aaa00.admin"); + public void hostsharingAdmin_withAssumedHostsharingAdminRole_canViewAllCustomers() { + given: + currentUser("mike@hostsharing.net"); + assumedRoles("global#hostsharing.admin"); - when(() -> customerRepository.findAll()); + // when + final var result = customerRepository.findAll(); - then.exactlyTheseCustomersAreReturned("aaa"); + then: + exactlyTheseCustomersAreReturned(result, "aaa", "aab", "aac"); + } + + @Test + public void customerAdmin_withoutAssumedRole_canViewOnlyItsOwnCustomer() { + // given: + currentUser("admin@aaa.example.com"); + + // when: + final var result = customerRepository.findAll(); + + // then: + exactlyTheseCustomersAreReturned(result, "aaa"); + } + + @Test + public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer() { + currentUser("admin@aaa.example.com"); + assumedRoles("package#aaa00.admin"); + + final var result = customerRepository.findAll(); + + exactlyTheseCustomersAreReturned(result, "aaa"); } @Test public void customerAdmin_withAssumedAlienPackageAdminRole_cannotViewAnyCustomer() { - given.currentUser("admin@aaa.example.com"). - and().assumedRoles("package#aab00.admin"); + // given: + currentUser("admin@aaa.example.com"); + assumedRoles("package#aab00.admin"); - when(() -> customerRepository.findAll()); + // when + final var attempt = attempt( + em, + () -> customerRepository.findAll()); - then.expectJpaSystemExceptionHasBeenThrown(). - and() - .expectRootCauseMessageMatches( - ".* user admin@aaa.example.com .* has no permission to assume role package#aab00#admin .*"); + // then + attempt.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "user admin@aaa.example.com .* has no permission to assume role package#aab00#admin"); } @Test void unknownUser_withoutAssumedRole_cannotViewAnyCustomers() { - given.currentUser("unknown@example.org"); + currentUser("unknown@example.org"); - when(() -> customerRepository.findAll()); + final var attempt = attempt( + em, + () -> customerRepository.findAll()); - then.expectJpaSystemExceptionHasBeenThrown(). - and().expectRootCauseMessageMatches(".* user unknown@example.org does not exist.*"); + attempt.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "hsadminng.currentUser defined as unknown@example.org, but does not exists"); } @Test @Transactional - void unknownUserWithAssumedCustomerRoleCannotViewAnyCustomers() { - given.currentUser("unknown@example.org"). - and().assumedRoles("customer#aaa.admin"); + void unknownUser_withAssumedCustomerRole_cannotViewAnyCustomers() { + currentUser("unknown@example.org"); + assumedRoles("customer#aaa.admin"); - when(() -> customerRepository.findAll()); + final var attempt = attempt( + em, + () -> customerRepository.findAll()); - then.expectJpaSystemExceptionHasBeenThrown(). - and().expectRootCauseMessageMatches(".* user unknown@example.org does not exist.*"); + attempt.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "hsadminng.currentUser defined as unknown@example.org, but does not exists"); } - void when(final Supplier> code) { - try { - when = new When<>(code.get()); - } catch (final NestedRuntimeException exc) { - when = new When<>(exc); - } - } - - private class Then { - - Then and() { - return this; - } - - void exactlyTheseCustomersAreReturned(final String... customerPrefixes) { - assertThat(when.actualResult) - .hasSize(customerPrefixes.length) - .extracting(CustomerEntity::getPrefix) - .containsExactlyInAnyOrder(customerPrefixes); - } - - Then expectJpaSystemExceptionHasBeenThrown() { - assertThat(when.actualException).isInstanceOf(JpaSystemException.class); - return this; - } - - void expectRootCauseMessageMatches(final String expectedMessage) { - assertThat(firstRootCauseMessageLineOf(when.actualException)).matches(expectedMessage); - } - } } - private String firstRootCauseMessageLineOf(final NestedRuntimeException exception) { - return Optional.ofNullable(exception.getRootCause()) - .map(Throwable::getMessage) - .map(message -> message.split("\\r|\\n|\\r\\n", 0)[0]) - .orElse(null); + void currentUser(final String currentUser) { + context.setCurrentUser(currentUser); + assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser); } - private class Given { - - Given and() { - return this; - } - - Given currentUser(final String currentUser) { - context.setCurrentUser(currentUser); - assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser); - return this; - } - - void assumedRoles(final String assumedRoles) { - context.assumeRoles(assumedRoles); - assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";")); - } + void assumedRoles(final String assumedRoles) { + context.assumeRoles(assumedRoles); + assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";")); } - private static class When { - - T actualResult; - NestedRuntimeException actualException; - - When(final T actualResult) { - this.actualResult = actualResult; - } - - When(final NestedRuntimeException exception) { - this.actualException = exception; - } + void exactlyTheseCustomersAreReturned(final List actualResult, final String... customerPrefixes) { + assertThat(actualResult) + .hasSize(customerPrefixes.length) + .extracting(CustomerEntity::getPrefix) + .containsExactlyInAnyOrder(customerPrefixes); } + } diff --git a/src/test/java/net/hostsharing/test/JpaAttempt.java b/src/test/java/net/hostsharing/test/JpaAttempt.java new file mode 100644 index 00000000..358b1a6e --- /dev/null +++ b/src/test/java/net/hostsharing/test/JpaAttempt.java @@ -0,0 +1,79 @@ +package net.hostsharing.test; + +import junit.framework.AssertionFailedError; +import org.springframework.core.NestedExceptionUtils; + +import javax.persistence.EntityManager; +import java.util.Optional; +import java.util.function.Supplier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Wraps the 'when' part of a DataJpaTest to improve readability of tests. + *

+ * It + * - makes sure that the SQL code is actually performed (em.flush()), + * - if any exception is throw, it's caught and stored, + * - makes the result available for assertions, + * - cleans the JPA first level cache to force assertions read from the database, not just cache, + * - offers some assertions based on the exception. + * * + * + * @param success result type + */ +public class JpaAttempt { + + private T result = null; + private RuntimeException exception = null; + + private String firstRootCauseMessageLineOf(final RuntimeException exception) { + final var rootCause = NestedExceptionUtils.getRootCause(exception); + return Optional.ofNullable(rootCause) + .map(Throwable::getMessage) + .map(message -> message.split("\\r|\\n|\\r\\n", 0)[0]) + .orElse(null); + } + + public static JpaAttempt attempt(final EntityManager em, final Supplier code) { + return new JpaAttempt<>(em, code); + } + + public JpaAttempt(final EntityManager em, final Supplier code) { + try { + result = code.get(); + em.flush(); + em.clear(); + } catch (RuntimeException exc) { + exception = exc; + } + } + + public boolean wasSuccessful() { + return exception == null; + } + + public T returnedResult() { + return result; + } + + public RuntimeException caughtException() { + return exception; + } + + @SuppressWarnings("unchecked") + public E caughtException(final Class expectedExceptionClass) { + if (expectedExceptionClass.isAssignableFrom(exception.getClass())) { + return (E) exception; + } + throw new AssertionFailedError("expected " + expectedExceptionClass + " but got " + exception); + } + + public void assertExceptionWithRootCauseMessage( + final Class expectedExceptionClass, + final String expectedRootCauseMessage) { + assertThat( + firstRootCauseMessageLineOf(caughtException(expectedExceptionClass))) + .matches(".*" + expectedRootCauseMessage + ".*"); + } +}