diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilter.java index 4993429f..44ab3dfc 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilter.java @@ -130,9 +130,10 @@ public class JSonDeserializationWithAccessFilter extends JSonAccessFilter private void checkAccessToWrittenFields(final T currentDto) { writtenFields.forEach(field -> { + // TODO this ugly code needs cleanup if (!field.equals(selfIdField)) { final Role role = getLoginUserRole(); - if (getId() == null) { + if (isInitAccess()) { if (!role.isAllowedToInit(field)) { if (!field.equals(parentIdField)) { throw new BadRequestAlertException("Initialization of field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "initializationProhibited"); @@ -140,14 +141,18 @@ public class JSonDeserializationWithAccessFilter extends JSonAccessFilter throw new BadRequestAlertException("Referencing field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "referencingProhibited"); } } - } else if (isUpdate(field, dto, currentDto) && !getLoginUserRole().isAllowedToUpdate(field)) { + } else if ( !Role.toBeIgnoredForUpdates(field) && isActuallyUpdated(field, dto, currentDto) && !getLoginUserRole().isAllowedToUpdate(field)) { throw new BadRequestAlertException("Update of field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "updateProhibited"); } } }); } - private boolean isUpdate(final Field field, final T dto, T currentDto) { + private boolean isInitAccess() { + return getId() == null; + } + + private boolean isActuallyUpdated(final Field field, final T dto, T currentDto) { return ObjectUtils.notEqual(ReflectionUtil.getValue(dto, field), ReflectionUtil.getValue(currentDto, field)); } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java index e1de24cf..46e279f9 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java @@ -2,14 +2,16 @@ package org.hostsharing.hsadminng.service.accessfilter; import java.lang.reflect.Field; +import static com.google.common.base.Verify.verify; + /** * These enum values are on the one hand used to define the minimum role required to grant access to resources, * but on the other hand also for the roles users can be assigned to. - * + *

* TODO: Maybe splitting it up into UserRole and RequiredRole would make it more clear? - * And maybe instead of a level, we could then add the comprised roles in the constructor? - * This could also be a better way to express that the financial contact has no rights to - * other users resources (see also ACTUAL_CUSTOMER_USEr vs. ANY_CUSTOMER_USER). + * And maybe instead of a level, we could then add the comprised roles in the constructor? + * This could also be a better way to express that the financial contact has no rights to + * other users resources (see also ACTUAL_CUSTOMER_USEr vs. ANY_CUSTOMER_USER). */ public enum Role { /** @@ -79,14 +81,45 @@ public enum Role { * This role is meant to specify that a resources can be accessed by anybody, even without login. * It's currently only used for technical purposes. */ - ANYBODY(99); + ANYBODY(99), - private final int level; + /** + * Pseudo-role to mark init/update access as ignored because the field is display-only. + * This allows REST clients to send the whole response back as a new update request. + * This role is not covered by any and covers itself no role. + */ + IGNORED; + + private final Integer level; + + Role() { + this.level = null; + } Role(final int level) { this.level = level; } + /** + * @param field a field of a DTO with AccessMappings + * @return true if update access can be ignored because the field is just for display anyway + */ + public static boolean toBeIgnoredForUpdates(final Field field) { + final AccessFor accessForAnnot = field.getAnnotation(AccessFor.class); + if (accessForAnnot == null) { + return true; + } + final Role[] updateAccessFor = field.getAnnotation(AccessFor.class).update(); + return updateAccessFor.length == 1 && updateAccessFor[0].isIgnored(); + } + + /** + * @return true if the role is the IGNORED role + */ + public boolean isIgnored() { + return this == Role.IGNORED; + } + /** * @return true if this role is independent of a target object, false otherwise. */ @@ -95,12 +128,12 @@ public enum Role { } /** - @return the role with the broadest access rights + * @return the role with the broadest access rights */ public static Role broadest(final Role role, final Role... roles) { Role broadests = role; - for ( Role r: roles ) { - if ( r.covers(broadests)) { + for (Role r : roles) { + if (r.covers(broadests)) { broadests = r; } } @@ -108,27 +141,51 @@ public enum Role { } /** - * Determines if the given role is covered by this role. - * + * Determines if 'this' actual role covered the given required role. + *

* Where 'this' means the Java instance itself as a role of a system user. - * + *

* {@code * Role.HOSTMASTER.covers(Role.ANY_CUSTOMER_USER) == true * } * * @param role The required role for a resource. - * * @return whether this role comprises the given role */ public boolean covers(final Role role) { + if (this.isIgnored() || role.isIgnored()) { + return false; + } return this == role || this.level < role.level; } + /** + * Determines if 'this' actual role covers any of the given required roles. + *

+ * Where 'this' means the Java instance itself as a role of a system user. + *

+ * {@code + * Role.HOSTMASTER.coversAny(Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT) == true + * } + * + * @param roles The alternatively required roles for a resource. Must be at least one. + * @return whether this role comprises any of the given roles + */ + public boolean coversAny(final Role... roles) { + verify(roles != null && roles.length > 0, "roles expected"); + + for (Role role : roles) { + if (this.covers(role)) { + return true; + } + } + return false; + } + /** * Checks if this role of a user allows to initialize the given field when creating the resource. * * @param field a field of the DTO of a resource - * * @return true if allowed */ public boolean isAllowedToInit(final Field field) { @@ -145,7 +202,6 @@ public enum Role { * Checks if this role of a user allows to update the given field. * * @param field a field of the DTO of a resource - * * @return true if allowed */ public boolean isAllowedToUpdate(final Field field) { @@ -162,7 +218,6 @@ public enum Role { * Checks if this role of a user allows to read the given field. * * @param field a field of the DTO of a resource - * * @return true if allowed */ public boolean isAllowedToRead(final Field field) { diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/AssetDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/AssetDTO.java index cfeb355e..143497ce 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/AssetDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/AssetDTO.java @@ -20,23 +20,23 @@ import java.util.Objects; public class AssetDTO implements Serializable, AccessMappings { @SelfId(resolver = AssetService.class) - @AccessFor(read = Role.ANY_CUSTOMER_USER) + @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long id; @NotNull - @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate documentDate; @NotNull - @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate valueDate; @NotNull - @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private AssetAction action; @NotNull - @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private BigDecimal amount; @Size(max = 160) @@ -44,12 +44,10 @@ public class AssetDTO implements Serializable, AccessMappings { private String remark; @ParentId(resolver = MembershipService.class) - @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long membershipId; - // TODO: these init/update rights actually mean "ignore", we might want to express this in a better way - // background: there is no converter for any display label in DTOs to entity field values anyway - @AccessFor(init=Role.ANYBODY, update = Role.ANYBODY, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) + @AccessFor(update = Role.IGNORED, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String membershipDisplayLabel; public Long getId() { @@ -147,7 +145,7 @@ public class AssetDTO implements Serializable, AccessMappings { ", amount=" + getAmount() + ", remark='" + getRemark() + "'" + ", membership=" + getMembershipId() + - ", membership='" + getMembershipDisplayLabel() + "'" + + ", membershipDisplayLabel='" + getMembershipDisplayLabel() + "'" + "}"; } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilterTestFixture.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilterTestFixture.java index fa3505d4..51686cf5 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilterTestFixture.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilterTestFixture.java @@ -35,6 +35,10 @@ public class JSonAccessFilterTestFixture { @SelfId(resolver = GivenService.class) @AccessFor(read = ANYBODY) Long id; + + @AccessFor(update = IGNORED, read = ANYBODY) + String displayLabel; + } static abstract class GivenCustomerService implements IdToDtoResolver { diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java index fe46e209..ec1fd98f 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java @@ -1,10 +1,12 @@ package org.hostsharing.hsadminng.service.accessfilter; +import com.google.common.base.VerifyException; import org.junit.Test; import java.lang.reflect.Field; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; public class RoleUnitTest { @@ -71,6 +73,46 @@ public class RoleUnitTest { assertThat(Role.FINANCIAL_CONTACT.covers(Role.ACTUAL_CUSTOMER_USER)).isFalse(); } + @Test + public void ignoredCoversNothingAndIsNotCovered() { + assertThat(Role.IGNORED.covers(Role.HOSTMASTER)).isFalse(); + assertThat(Role.IGNORED.covers(Role.ANYBODY)).isFalse(); + assertThat(Role.IGNORED.covers(Role.IGNORED)).isFalse(); + assertThat(Role.HOSTMASTER.covers(Role.IGNORED)).isFalse(); + assertThat(Role.ANYBODY.covers(Role.IGNORED)).isFalse(); + } + + @Test + public void coversAny() { + assertThat(Role.HOSTMASTER.coversAny(Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT)).isTrue(); + assertThat(Role.CONTRACTUAL_CONTACT.coversAny(Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT)).isTrue(); + assertThat(Role.FINANCIAL_CONTACT.coversAny(Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT)).isTrue(); + + assertThat(Role.ANY_CUSTOMER_USER.coversAny(Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT)).isFalse(); + + assertThat(catchThrowable(() -> Role.HOSTMASTER.coversAny())).isInstanceOf(VerifyException.class); + assertThat(catchThrowable(() -> Role.HOSTMASTER.coversAny(null))).isInstanceOf(VerifyException.class); + } + + @Test + public void isIgnored() { + for (Role role : Role.values()) { + if (role == Role.IGNORED) { + assertThat(role.isIgnored()).isTrue(); + } else { + assertThat(role.isIgnored()).isFalse(); + } + } + } + + @Test + public void toBeIgnoredForUpdates() { + assertThat(Role.toBeIgnoredForUpdates(someFieldWithoutAccessForAnnotation)).isTrue(); + assertThat(Role.toBeIgnoredForUpdates(someFieldWithAccessForAnnotationToBeIgnoredForUpdates)).isTrue(); + assertThat(Role.toBeIgnoredForUpdates(someFieldWithAccessForAnnotationToBeIgnoredForUpdatesAmongOthers)).isFalse(); + assertThat(Role.toBeIgnoredForUpdates(someFieldWithAccessForAnnotation)).isFalse(); + } + @Test public void isIndependent() { assertThat(Role.HOSTMASTER.isIndependent()).isTrue(); @@ -114,14 +156,25 @@ public class RoleUnitTest { @AccessFor(init = Role.ADMIN, update = Role.SUPPORTER, read = Role.ANY_CUSTOMER_CONTACT) private Integer someFieldWithAccessForAnnotation; + @AccessFor(update = Role.IGNORED, read = Role.ANY_CUSTOMER_CONTACT) + private Integer someFieldWithAccessForAnnotationToBeIgnoredForUpdates; + + @AccessFor(update = {Role.IGNORED, Role.SUPPORTER}, read = Role.ANY_CUSTOMER_CONTACT) + private Integer someFieldWithAccessForAnnotationToBeIgnoredForUpdatesAmongOthers; + private Integer someFieldWithoutAccessForAnnotation; } private static Field someFieldWithoutAccessForAnnotation; + private static Field someFieldWithAccessForAnnotationToBeIgnoredForUpdates; + private static Field someFieldWithAccessForAnnotationToBeIgnoredForUpdatesAmongOthers; private static Field someFieldWithAccessForAnnotation; + static { try { someFieldWithoutAccessForAnnotation = TestDto.class.getDeclaredField("someFieldWithoutAccessForAnnotation"); + someFieldWithAccessForAnnotationToBeIgnoredForUpdates = TestDto.class.getDeclaredField("someFieldWithAccessForAnnotationToBeIgnoredForUpdates"); + someFieldWithAccessForAnnotationToBeIgnoredForUpdatesAmongOthers = TestDto.class.getDeclaredField("someFieldWithAccessForAnnotationToBeIgnoredForUpdatesAmongOthers"); someFieldWithAccessForAnnotation = TestDto.class.getDeclaredField("someFieldWithAccessForAnnotation"); } catch (NoSuchFieldException e) { throw new AssertionError("precondition failed", e); diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/AccessMappingsUnitTestBase.java b/src/test/java/org/hostsharing/hsadminng/service/dto/AccessMappingsUnitTestBase.java new file mode 100644 index 00000000..cf13506b --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/AccessMappingsUnitTestBase.java @@ -0,0 +1,86 @@ +package org.hostsharing.hsadminng.service.dto; + +import org.hostsharing.hsadminng.service.accessfilter.AccessFor; +import org.hostsharing.hsadminng.service.accessfilter.Role; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Usually base classes for unit tests are not a good idea, but because + * DTOs which implement AccessMapping are more like a DSL, + * this base class should be used to enforce its required structure. + */ +public abstract class AccessMappingsUnitTestBase { + + protected AccessRightsMatcher initAccesFor(final Class dtoClass, final Role role) { + return new AccessRightsMatcher(dtoClass, role, AccessFor::init); + } + + protected AccessRightsMatcher updateAccesFor(final Class dtoClass, final Role role) { + return new AccessRightsMatcher(dtoClass, role, AccessFor::update); + } + + protected AccessRightsMatcher readAccesFor(final Class dtoClass, final Role role) { + return new AccessRightsMatcher(dtoClass, role, AccessFor::read); + } + + protected static class AccessRightsMatcher { + private final Class dtoClass; + private final Role role; + + private final String[] namesOfFieldsWithAccessForAnnotation; + private final String[] namesOfAccessibleFields; + + AccessRightsMatcher(final Class dtoClass, final Role role, final Function access) { + this.dtoClass = dtoClass; + this.role = role; + + final Set fieldsWithAccessForAnnotation = determineFieldsWithAccessForAnnotation(dtoClass); + this.namesOfFieldsWithAccessForAnnotation = fieldsWithAccessForAnnotation.stream() + .map(Field::getName).collect(Collectors.toList()).toArray(new String[]{}); + this.namesOfAccessibleFields = fieldsWithAccessForAnnotation.stream() + .filter(f -> allows(f, access, role)).map(Field::getName).collect(Collectors.toList()).toArray(new String[]{}); + } + + public void shouldBeExactlyFor(final String... expectedFields) { + assertThat(namesOfAccessibleFields).containsExactlyInAnyOrder(expectedFields); + } + + public void shouldBeForNothing() { + assertThat(namesOfAccessibleFields).isEmpty(); + } + + public void shouldBeForAllFields() { + assertThat(namesOfAccessibleFields).containsExactlyInAnyOrder(namesOfFieldsWithAccessForAnnotation); + } + + + private static Set determineFieldsWithAccessForAnnotation(final Class dtoClass) { + + final Set fieldsWithAccessForAnnotation = new HashSet<>(); + + for (Field field : dtoClass.getDeclaredFields()) { + if (field.isAnnotationPresent(AccessFor.class)) { + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + fieldsWithAccessForAnnotation.add(field); + } + } + + return fieldsWithAccessForAnnotation; + } + + private static boolean allows(final Field field, final Function access, final Role role) { + if (field.isAnnotationPresent(AccessFor.class)) { + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + return role.coversAny(access.apply(accessFor)); + } + return false; + } + } +} diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/AssetDTOIntTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/AssetDTOIntTest.java new file mode 100644 index 00000000..49f4f390 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/AssetDTOIntTest.java @@ -0,0 +1,212 @@ +package org.hostsharing.hsadminng.service.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.RandomUtils; +import org.hostsharing.hsadminng.domain.Asset; +import org.hostsharing.hsadminng.domain.Customer; +import org.hostsharing.hsadminng.domain.Membership; +import org.hostsharing.hsadminng.domain.enumeration.AssetAction; +import org.hostsharing.hsadminng.repository.AssetRepository; +import org.hostsharing.hsadminng.repository.CustomerRepository; +import org.hostsharing.hsadminng.repository.MembershipRepository; +import org.hostsharing.hsadminng.service.AssetService; +import org.hostsharing.hsadminng.service.AssetValidator; +import org.hostsharing.hsadminng.service.MembershipValidator; +import org.hostsharing.hsadminng.service.accessfilter.JSonBuilder; +import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.mapper.AssetMapper; +import org.hostsharing.hsadminng.service.mapper.AssetMapperImpl; +import org.hostsharing.hsadminng.service.mapper.CustomerMapperImpl; +import org.hostsharing.hsadminng.service.mapper.MembershipMapperImpl; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.persistence.EntityManager; +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; +import static org.junit.Assert.assertEquals; +import static org.mockito.BDDMockito.given; + +@JsonTest +@SpringBootTest(classes = { + CustomerMapperImpl.class, + MembershipMapperImpl.class, + AssetMapperImpl.class, + AssetDTO.AssetJsonSerializer.class, + AssetDTO.AssetJsonDeserializer.class +}) +@RunWith(SpringRunner.class) +public class AssetDTOIntTest { + + private static final Long SOME_CUSTOMER_ID = RandomUtils.nextLong(100, 199); + private static final Integer SOME_CUSTOMER_REFERENCE = 10001; + private static final String SOME_CUSTOMER_PREFIX = "abc"; + private static final String SOME_CUSTOMER_NAME = "Some Customer Name"; + private static final Customer SOME_CUSTOMER = new Customer().id(SOME_CUSTOMER_ID) + .reference(SOME_CUSTOMER_REFERENCE).prefix(SOME_CUSTOMER_PREFIX).name(SOME_CUSTOMER_NAME); + + private static final Long SOME_MEMBERSHIP_ID = RandomUtils.nextLong(200, 299); + private static final LocalDate SOME_MEMBER_FROM_DATE = LocalDate.parse("2000-12-06"); + private static final Membership SOME_MEMBERSHIP = new Membership().id(SOME_MEMBERSHIP_ID) + .customer(SOME_CUSTOMER).memberFromDate(SOME_MEMBER_FROM_DATE); + private static final String SOME_MEMBERSHIP_DISPLAY_LABEL = "Some Customer Name [10001:abc] 2000-12-06 - ..."; + + private static final Long SOME_ASSET_ID = RandomUtils.nextLong(300, 399); + private static final Asset SOME_ASSET = new Asset().id(SOME_ASSET_ID).membership(SOME_MEMBERSHIP); + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private AssetMapper assetMapper; + + @MockBean + private AssetRepository assetRepository; + + @MockBean + private AssetValidator assetValidator; + + @MockBean + private CustomerRepository customerRepository; + + @MockBean + private MembershipRepository membershipRepository; + + @MockBean + private MembershipValidator membershipValidator; + + @MockBean + private AssetService assetService; + + @MockBean + private EntityManager em; + + @Before + public void init() { + given(customerRepository.findById(SOME_CUSTOMER_ID)).willReturn(Optional.of(SOME_CUSTOMER)); + given(membershipRepository.findById(SOME_MEMBERSHIP_ID)).willReturn(Optional.of(SOME_MEMBERSHIP)); + given(assetRepository.findById(SOME_ASSET_ID)).willReturn((Optional.of(SOME_ASSET))); + } + + @Test + public void shouldSerializePartiallyForFinancialCustomerContact() throws JsonProcessingException { + + // given + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.FINANCIAL_CONTACT); + final AssetDTO given = createSomeAssetDTO(SOME_ASSET_ID); + + // when + final String actual = objectMapper.writeValueAsString(given); + + // then + given.setRemark(null); + assertEquals(createExpectedJSon(given), actual); + } + + @Test + public void shouldSerializeCompletelyForSupporter() throws JsonProcessingException { + + // given + givenAuthenticatedUser(); + givenUserHavingRole(Role.SUPPORTER); + final AssetDTO given = createSomeAssetDTO(SOME_ASSET_ID); + + // when + final String actual = objectMapper.writeValueAsString(given); + + // then + assertEquals(createExpectedJSon(given), actual); + } + + @Test + public void shouldNotDeserializeForContractualCustomerContact() { + // given + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.CONTRACTUAL_CONTACT); + final String json = new JSonBuilder() + .withFieldValue("id", SOME_ASSET_ID) + .withFieldValue("remark", "Updated Remark") + .toString(); + + // when + final Throwable actual = catchThrowable(() -> objectMapper.readValue(json, AssetDTO.class)); + + // then + assertThat(actual).isInstanceOfSatisfying(BadRequestAlertException.class, bre -> + assertThat(bre.getMessage()).isEqualTo("Update of field AssetDTO.remark prohibited for current user role CONTRACTUAL_CONTACT") + ); + } + + @Test + public void shouldDeserializeForAdminIfRemarkIsChanged() throws IOException { + // given + givenAuthenticatedUser(); + givenUserHavingRole(Role.ADMIN); + final String json = new JSonBuilder() + .withFieldValue("id", SOME_ASSET_ID) + .withFieldValue("remark", "Updated Remark") + .toString(); + + // when + final AssetDTO actual = objectMapper.readValue(json, AssetDTO.class); + + // then + final AssetDTO expected = new AssetDTO(); + expected.setId(SOME_ASSET_ID); + expected.setMembershipId(SOME_MEMBERSHIP_ID); + expected.setRemark("Updated Remark"); + expected.setMembershipDisplayLabel(SOME_MEMBERSHIP_DISPLAY_LABEL); + assertThat(actual).isEqualToIgnoringGivenFields(expected, "displayLabel"); + } + + // --- only test fixture below --- + + private String createExpectedJSon(AssetDTO dto) { + return new JSonBuilder() + .withFieldValueIfPresent("id", dto.getId()) + .withFieldValueIfPresent("documentDate", dto.getDocumentDate().toString()) + .withFieldValueIfPresent("valueDate", dto.getValueDate().toString()) + .withFieldValueIfPresent("action", dto.getAction().name()) + .withFieldValueIfPresent("amount", dto.getAmount().doubleValue()) + .withFieldValueIfPresent("remark", dto.getRemark()) + .withFieldValueIfPresent("membershipId", dto.getMembershipId()) + .withFieldValue("membershipDisplayLabel", dto.getMembershipDisplayLabel()) + .toString(); + } + + + private AssetDTO createSomeAssetDTO(final long id) { + final AssetDTO given = new AssetDTO(); + given.setId(id); + given.setAction(AssetAction.PAYMENT); + given.setAmount(new BigDecimal("512.01")); + given.setDocumentDate(LocalDate.parse("2019-04-27")); + given.setValueDate(LocalDate.parse("2019-04-28")); + given.setMembershipId(SOME_MEMBERSHIP_ID); + given.setRemark("Some Remark"); + given.setMembershipDisplayLabel("Display Label for Membership #" + SOME_MEMBERSHIP_ID); + return given; + } +} diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/AssetDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/AssetDTOUnitTest.java index 234a190a..759f37fd 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/AssetDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/AssetDTOUnitTest.java @@ -1,214 +1,101 @@ package org.hostsharing.hsadminng.service.dto; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; -import org.hostsharing.hsadminng.domain.Asset; -import org.hostsharing.hsadminng.domain.Customer; -import org.hostsharing.hsadminng.domain.Membership; import org.hostsharing.hsadminng.domain.enumeration.AssetAction; -import org.hostsharing.hsadminng.repository.AssetRepository; -import org.hostsharing.hsadminng.repository.CustomerRepository; -import org.hostsharing.hsadminng.repository.MembershipRepository; -import org.hostsharing.hsadminng.service.AssetService; -import org.hostsharing.hsadminng.service.AssetValidator; -import org.hostsharing.hsadminng.service.MembershipValidator; -import org.hostsharing.hsadminng.service.accessfilter.JSonBuilder; import org.hostsharing.hsadminng.service.accessfilter.Role; -import org.hostsharing.hsadminng.service.mapper.AssetMapper; -import org.hostsharing.hsadminng.service.mapper.AssetMapperImpl; -import org.hostsharing.hsadminng.service.mapper.CustomerMapperImpl; -import org.hostsharing.hsadminng.service.mapper.MembershipMapperImpl; -import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; -import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.json.JsonTest; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.junit4.SpringRunner; -import javax.persistence.EntityManager; -import java.io.IOException; import java.math.BigDecimal; import java.time.LocalDate; -import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; -import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; -import static org.junit.Assert.assertEquals; -import static org.mockito.BDDMockito.given; +public class AssetDTOUnitTest extends AccessMappingsUnitTestBase { -@JsonTest -@SpringBootTest(classes = { - AssetMapperImpl.class, - AssetDTO.AssetJsonSerializer.class, - AssetDTO.AssetJsonDeserializer.class, - - MembershipMapperImpl.class, - - CustomerMapperImpl.class -}) -@RunWith(SpringRunner.class) -public class AssetDTOUnitTest { - - - private static final Long SOME_CUSTOMER_ID = RandomUtils.nextLong(100, 199); - private static final Integer SOME_CUSTOMER_REFERENCE = 10001; - private static final String SOME_CUSTOMER_PREFIX = "abc"; - private static final String SOME_CUSTOMER_NAME = "Some Customer Name"; - private static final Customer SOME_CUSTOMER = new Customer().id(SOME_CUSTOMER_ID).reference(SOME_CUSTOMER_REFERENCE).prefix(SOME_CUSTOMER_PREFIX).name(SOME_CUSTOMER_NAME); - - private static final Long SOME_MEMBERSHIP_ID = RandomUtils.nextLong(200, 299); - private static final LocalDate SOME_MEMBER_FROM_DATE = LocalDate.parse("2000-12-06") ; - private static final Membership SOME_MEMBERSHIP = new Membership().id(SOME_MEMBERSHIP_ID).customer(SOME_CUSTOMER).memberFromDate(SOME_MEMBER_FROM_DATE); - public static final String SOME_MEMBERSHIP_DISPLAY_LABEL = "Some Customer Name [10001:abc] 2000-12-06 - ..."; - - private static final Long SOME_ASSET_ID = RandomUtils.nextLong(300, 399); - private static final Asset SOME_ASSET = new Asset().id(SOME_ASSET_ID).membership(SOME_MEMBERSHIP); - - @Rule - public MockitoRule mockito = MockitoJUnit.rule(); - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private AssetMapper assetMapper; - - @MockBean - private AssetRepository assetRepository; - - @MockBean - private AssetValidator assetValidator; - - @MockBean - private CustomerRepository customerRepository; - - @MockBean - private MembershipRepository membershipRepository; - - @MockBean - private MembershipValidator membershipValidator; - - @MockBean - private AssetService assetService; - - @MockBean - private EntityManager em; - - @Before - public void init() { - given(customerRepository.findById(SOME_CUSTOMER_ID)).willReturn(Optional.of(SOME_CUSTOMER)); - given(membershipRepository.findById(SOME_MEMBERSHIP_ID)).willReturn(Optional.of(SOME_MEMBERSHIP)); - given(assetRepository.findById(SOME_ASSET_ID)).willReturn((Optional.of(SOME_ASSET))); + @Test + public void shouldHaveProperAccessForAdmin() { + initAccesFor(AssetDTO.class, Role.ADMIN).shouldBeExactlyFor( + "membershipId", "documentDate", "amount", "action", "valueDate", "remark"); + updateAccesFor(AssetDTO.class, Role.ADMIN).shouldBeExactlyFor("remark"); + readAccesFor(AssetDTO.class, Role.ADMIN).shouldBeForAllFields(); } @Test - public void shouldSerializePartiallyForFinancialCustomerContact() throws JsonProcessingException { - - // given - givenAuthenticatedUser(); - givenUserHavingRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.FINANCIAL_CONTACT); - final AssetDTO given = createSomeAssetDTO(SOME_ASSET_ID); - - // when - final String actual = objectMapper.writeValueAsString(given); - - // then - given.setRemark(null); - assertEquals(createExpectedJSon(given), actual); + public void shouldHaveProperAccessForContractualContact() { + initAccesFor(AssetDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeForNothing(); + updateAccesFor(AssetDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeForNothing(); + readAccesFor(AssetDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeExactlyFor( + "id", "membershipId", "documentDate", "amount", "action", "valueDate", "membershipDisplayLabel"); } @Test - public void shouldSerializeCompletelyForSupporter() throws JsonProcessingException { - - // given - givenAuthenticatedUser(); - givenUserHavingRole(Role.SUPPORTER); - final AssetDTO given = createSomeAssetDTO(SOME_ASSET_ID); - - // when - final String actual = objectMapper.writeValueAsString(given); - - // then - assertEquals(createExpectedJSon(given), actual); + public void shouldHaveNoAccessForTechnicalContact() { + initAccesFor(AssetDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing(); + updateAccesFor(AssetDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing(); + readAccesFor(AssetDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing(); } @Test - public void shouldNotDeserializeForContractualCustomerContact() { - // given - givenAuthenticatedUser(); - givenUserHavingRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.CONTRACTUAL_CONTACT); - final String json = new JSonBuilder() - .withFieldValue("id", SOME_ASSET_ID) - .withFieldValue("remark", "Updated Remark") - .toString(); - - // when - final Throwable actual = catchThrowable(() -> objectMapper.readValue(json, AssetDTO.class)); - - // then - assertThat(actual).isInstanceOfSatisfying(BadRequestAlertException.class, bre -> - assertThat(bre.getMessage()).isEqualTo("Update of field AssetDTO.remark prohibited for current user role CONTRACTUAL_CONTACT") - ); + public void shouldHaveNoAccessForNormalUsersWithinCustomerRealm() { + initAccesFor(AssetDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing(); + updateAccesFor(AssetDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing(); + readAccesFor(AssetDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing(); } @Test - public void shouldDeserializeForAdminIfRemarkIsChanged() throws IOException { - // given - givenAuthenticatedUser(); - givenUserHavingRole(Role.ADMIN); - final String json = new JSonBuilder() - .withFieldValue("id", SOME_ASSET_ID) - .withFieldValue("remark", "Updated Remark") - .toString(); + public void shouldConvertToString() { + final AssetDTO dto = createDto(1234L); + assertThat(dto.toString()).isEqualTo("AssetDTO{id=1234, documentDate='2000-12-07', valueDate='2000-12-18', action='PAYMENT', amount=512.01, remark='Some Remark', membership=888, membershipDisplayLabel='Some Membership'}"); + } - // when - final AssetDTO actual = objectMapper.readValue(json, AssetDTO.class); + @Test + public void shouldImplementEqualsJustUsingClassAndId() { + final AssetDTO dto = createDto(1234L); + assertThat(dto.equals(dto)).isTrue(); - // then - final AssetDTO expected = new AssetDTO(); - expected.setId(SOME_ASSET_ID); - expected.setMembershipId(SOME_MEMBERSHIP_ID); - expected.setRemark("Updated Remark"); - expected.setMembershipDisplayLabel(SOME_MEMBERSHIP_DISPLAY_LABEL); - assertThat(actual).isEqualToIgnoringGivenFields(expected, "displayLabel"); + final AssetDTO dtoWithSameId = createRandomDto(1234L); + assertThat(dto.equals(dtoWithSameId)).isTrue(); + + final AssetDTO dtoWithAnotherId = createRandomDto(RandomUtils.nextLong(2000, 9999)); + assertThat(dtoWithAnotherId.equals(dtoWithSameId)).isFalse(); + + final AssetDTO dtoWithoutId = createRandomDto(null); + assertThat(dto.equals(dtoWithoutId)).isFalse(); + assertThat(dtoWithoutId.equals(dto)).isFalse(); + + assertThat(dto.equals(null)).isFalse(); + assertThat(dto.equals("")).isFalse(); } // --- only test fixture below --- - private String createExpectedJSon(AssetDTO dto) { - return new JSonBuilder() - .withFieldValueIfPresent("id", dto.getId()) - .withFieldValueIfPresent("documentDate", dto.getDocumentDate().toString()) - .withFieldValueIfPresent("valueDate", dto.getValueDate().toString()) - .withFieldValueIfPresent("action", dto.getAction().name()) - .withFieldValueIfPresent("amount", dto.getAmount().doubleValue()) - .withFieldValueIfPresent("remark", dto.getRemark()) - .withFieldValueIfPresent("membershipId", dto.getMembershipId()) - .withFieldValue("membershipDisplayLabel", dto.getMembershipDisplayLabel()) - .toString(); + private AssetDTO createDto(final Long id) { + final AssetDTO dto = new AssetDTO(); + dto.setId(id); + dto.setDocumentDate(LocalDate.parse("2000-12-07")); + dto.setAmount(new BigDecimal("512.01")); + dto.setAction(AssetAction.PAYMENT); + dto.setRemark("Some Remark"); + dto.setValueDate(LocalDate.parse("2000-12-18")); + dto.setMembershipId(888L); + dto.setMembershipDisplayLabel("Some Membership"); + return dto; } - private AssetDTO createSomeAssetDTO(final long id) { - final AssetDTO given = new AssetDTO(); - given.setId(id); - given.setAction(AssetAction.PAYMENT); - given.setAmount(new BigDecimal("512.01")); - given.setDocumentDate(LocalDate.parse("2019-04-27")); - given.setValueDate(LocalDate.parse("2019-04-28")); - given.setMembershipId(SOME_MEMBERSHIP_ID); - given.setRemark("Some Remark"); - given.setMembershipDisplayLabel("Display Label for Membership #" + SOME_MEMBERSHIP_ID); - return given; + private AssetDTO createRandomDto(final Long id) { + final AssetDTO dto = new AssetDTO(); + dto.setId(id); + final LocalDate randomDate = LocalDate.parse("2000-12-07").plusDays(RandomUtils.nextInt(1, 999)); + dto.setDocumentDate(randomDate); + dto.setAmount(new BigDecimal("512.01")); + dto.setAction(AssetAction.PAYMENT); + dto.setRemark("Some Remark"); + dto.setValueDate(randomDate.plusDays(RandomUtils.nextInt(1, 99))); + dto.setMembershipId(RandomUtils.nextLong()); + dto.setMembershipDisplayLabel(RandomStringUtils.randomAlphabetic(20)); + return dto; } + } diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java index 9cc5cc0d..2652f139 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java @@ -73,7 +73,6 @@ public class ShareDTOUnitTest { public void init() { given(jsonParser.getCodec()).willReturn(codec); - given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory); given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory); given(autowireCapableBeanFactory.createBean(CustomerService.class)).willReturn(customerService); given(autowireCapableBeanFactory.createBean(MembershipService.class)).willReturn(membershipService); diff --git a/src/test/java/org/hostsharing/hsadminng/web/rest/AssetResourceIntTest.java b/src/test/java/org/hostsharing/hsadminng/web/rest/AssetResourceIntTest.java index f094f9b1..1475a9a8 100644 --- a/src/test/java/org/hostsharing/hsadminng/web/rest/AssetResourceIntTest.java +++ b/src/test/java/org/hostsharing/hsadminng/web/rest/AssetResourceIntTest.java @@ -162,6 +162,7 @@ public class AssetResourceIntTest { // Create the Asset AssetDTO assetDTO = assetMapper.toDto(asset); + assetDTO.setMembershipDisplayLabel(null); restAssetMockMvc.perform(post("/api/assets") .contentType(TestUtil.APPLICATION_JSON_UTF8) .content(TestUtil.convertObjectToJsonBytes(assetDTO)))