split up AssetDTOUnitTest and AssetDTOIntTest, AccessMappingsUnitTestBase

This commit is contained in:
Michael Hoennig 2019-04-27 17:56:38 +02:00
parent 2fdb914f6d
commit 3abc201a8d
10 changed files with 508 additions and 208 deletions

View File

@ -130,9 +130,10 @@ public class JSonDeserializationWithAccessFilter<T> extends JSonAccessFilter<T>
private void checkAccessToWrittenFields(final T currentDto) { private void checkAccessToWrittenFields(final T currentDto) {
writtenFields.forEach(field -> { writtenFields.forEach(field -> {
// TODO this ugly code needs cleanup
if (!field.equals(selfIdField)) { if (!field.equals(selfIdField)) {
final Role role = getLoginUserRole(); final Role role = getLoginUserRole();
if (getId() == null) { if (isInitAccess()) {
if (!role.isAllowedToInit(field)) { if (!role.isAllowedToInit(field)) {
if (!field.equals(parentIdField)) { if (!field.equals(parentIdField)) {
throw new BadRequestAlertException("Initialization of field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "initializationProhibited"); throw new BadRequestAlertException("Initialization of field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "initializationProhibited");
@ -140,14 +141,18 @@ public class JSonDeserializationWithAccessFilter<T> extends JSonAccessFilter<T>
throw new BadRequestAlertException("Referencing field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "referencingProhibited"); 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"); 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)); return ObjectUtils.notEqual(ReflectionUtil.getValue(dto, field), ReflectionUtil.getValue(currentDto, field));
} }
} }

View File

@ -2,10 +2,12 @@ package org.hostsharing.hsadminng.service.accessfilter;
import java.lang.reflect.Field; 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, * 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. * but on the other hand also for the roles users can be assigned to.
* * <p>
* TODO: Maybe splitting it up into UserRole and RequiredRole would make it more clear? * 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? * 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 * This could also be a better way to express that the financial contact has no rights to
@ -79,14 +81,45 @@ public enum Role {
* This role is meant to specify that a resources can be accessed by anybody, even without login. * 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. * 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) { Role(final int level) {
this.level = 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. * @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) { public static Role broadest(final Role role, final Role... roles) {
Role broadests = role; Role broadests = role;
for ( Role r: roles ) { for (Role r : roles) {
if ( r.covers(broadests)) { if (r.covers(broadests)) {
broadests = r; 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.
* * <p>
* Where 'this' means the Java instance itself as a role of a system user. * Where 'this' means the Java instance itself as a role of a system user.
* * <p>
* {@code * {@code
* Role.HOSTMASTER.covers(Role.ANY_CUSTOMER_USER) == true * Role.HOSTMASTER.covers(Role.ANY_CUSTOMER_USER) == true
* } * }
* *
* @param role The required role for a resource. * @param role The required role for a resource.
*
* @return whether this role comprises the given role * @return whether this role comprises the given role
*/ */
public boolean covers(final Role role) { public boolean covers(final Role role) {
if (this.isIgnored() || role.isIgnored()) {
return false;
}
return this == role || this.level < role.level; return this == role || this.level < role.level;
} }
/**
* Determines if 'this' actual role covers any of the given required roles.
* <p>
* Where 'this' means the Java instance itself as a role of a system user.
* <p>
* {@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. * 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 * @param field a field of the DTO of a resource
*
* @return true if allowed * @return true if allowed
*/ */
public boolean isAllowedToInit(final Field field) { 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. * Checks if this role of a user allows to update the given field.
* *
* @param field a field of the DTO of a resource * @param field a field of the DTO of a resource
*
* @return true if allowed * @return true if allowed
*/ */
public boolean isAllowedToUpdate(final Field field) { 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. * Checks if this role of a user allows to read the given field.
* *
* @param field a field of the DTO of a resource * @param field a field of the DTO of a resource
*
* @return true if allowed * @return true if allowed
*/ */
public boolean isAllowedToRead(final Field field) { public boolean isAllowedToRead(final Field field) {

View File

@ -20,23 +20,23 @@ import java.util.Objects;
public class AssetDTO implements Serializable, AccessMappings { public class AssetDTO implements Serializable, AccessMappings {
@SelfId(resolver = AssetService.class) @SelfId(resolver = AssetService.class)
@AccessFor(read = Role.ANY_CUSTOMER_USER) @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT})
private Long id; private Long id;
@NotNull @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; private LocalDate documentDate;
@NotNull @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; private LocalDate valueDate;
@NotNull @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; private AssetAction action;
@NotNull @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; private BigDecimal amount;
@Size(max = 160) @Size(max = 160)
@ -44,12 +44,10 @@ public class AssetDTO implements Serializable, AccessMappings {
private String remark; private String remark;
@ParentId(resolver = MembershipService.class) @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; private Long membershipId;
// TODO: these init/update rights actually mean "ignore", we might want to express this in a better way @AccessFor(update = Role.IGNORED, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT})
// 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})
private String membershipDisplayLabel; private String membershipDisplayLabel;
public Long getId() { public Long getId() {
@ -147,7 +145,7 @@ public class AssetDTO implements Serializable, AccessMappings {
", amount=" + getAmount() + ", amount=" + getAmount() +
", remark='" + getRemark() + "'" + ", remark='" + getRemark() + "'" +
", membership=" + getMembershipId() + ", membership=" + getMembershipId() +
", membership='" + getMembershipDisplayLabel() + "'" + ", membershipDisplayLabel='" + getMembershipDisplayLabel() + "'" +
"}"; "}";
} }

View File

@ -35,6 +35,10 @@ public class JSonAccessFilterTestFixture {
@SelfId(resolver = GivenService.class) @SelfId(resolver = GivenService.class)
@AccessFor(read = ANYBODY) @AccessFor(read = ANYBODY)
Long id; Long id;
@AccessFor(update = IGNORED, read = ANYBODY)
String displayLabel;
} }
static abstract class GivenCustomerService implements IdToDtoResolver<GivenCustomerDto> { static abstract class GivenCustomerService implements IdToDtoResolver<GivenCustomerDto> {

View File

@ -1,10 +1,12 @@
package org.hostsharing.hsadminng.service.accessfilter; package org.hostsharing.hsadminng.service.accessfilter;
import com.google.common.base.VerifyException;
import org.junit.Test; import org.junit.Test;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
public class RoleUnitTest { public class RoleUnitTest {
@ -71,6 +73,46 @@ public class RoleUnitTest {
assertThat(Role.FINANCIAL_CONTACT.covers(Role.ACTUAL_CUSTOMER_USER)).isFalse(); 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 @Test
public void isIndependent() { public void isIndependent() {
assertThat(Role.HOSTMASTER.isIndependent()).isTrue(); assertThat(Role.HOSTMASTER.isIndependent()).isTrue();
@ -114,14 +156,25 @@ public class RoleUnitTest {
@AccessFor(init = Role.ADMIN, update = Role.SUPPORTER, read = Role.ANY_CUSTOMER_CONTACT) @AccessFor(init = Role.ADMIN, update = Role.SUPPORTER, read = Role.ANY_CUSTOMER_CONTACT)
private Integer someFieldWithAccessForAnnotation; 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 Integer someFieldWithoutAccessForAnnotation;
} }
private static Field someFieldWithoutAccessForAnnotation; private static Field someFieldWithoutAccessForAnnotation;
private static Field someFieldWithAccessForAnnotationToBeIgnoredForUpdates;
private static Field someFieldWithAccessForAnnotationToBeIgnoredForUpdatesAmongOthers;
private static Field someFieldWithAccessForAnnotation; private static Field someFieldWithAccessForAnnotation;
static { static {
try { try {
someFieldWithoutAccessForAnnotation = TestDto.class.getDeclaredField("someFieldWithoutAccessForAnnotation"); someFieldWithoutAccessForAnnotation = TestDto.class.getDeclaredField("someFieldWithoutAccessForAnnotation");
someFieldWithAccessForAnnotationToBeIgnoredForUpdates = TestDto.class.getDeclaredField("someFieldWithAccessForAnnotationToBeIgnoredForUpdates");
someFieldWithAccessForAnnotationToBeIgnoredForUpdatesAmongOthers = TestDto.class.getDeclaredField("someFieldWithAccessForAnnotationToBeIgnoredForUpdatesAmongOthers");
someFieldWithAccessForAnnotation = TestDto.class.getDeclaredField("someFieldWithAccessForAnnotation"); someFieldWithAccessForAnnotation = TestDto.class.getDeclaredField("someFieldWithAccessForAnnotation");
} catch (NoSuchFieldException e) { } catch (NoSuchFieldException e) {
throw new AssertionError("precondition failed", e); throw new AssertionError("precondition failed", e);

View File

@ -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<AssetDTO> dtoClass, final Role role) {
return new AccessRightsMatcher(dtoClass, role, AccessFor::init);
}
protected AccessRightsMatcher updateAccesFor(final Class<AssetDTO> dtoClass, final Role role) {
return new AccessRightsMatcher(dtoClass, role, AccessFor::update);
}
protected AccessRightsMatcher readAccesFor(final Class<AssetDTO> dtoClass, final Role role) {
return new AccessRightsMatcher(dtoClass, role, AccessFor::read);
}
protected static class AccessRightsMatcher {
private final Class<AssetDTO> dtoClass;
private final Role role;
private final String[] namesOfFieldsWithAccessForAnnotation;
private final String[] namesOfAccessibleFields;
AccessRightsMatcher(final Class<AssetDTO> dtoClass, final Role role, final Function<AccessFor, Role[]> access) {
this.dtoClass = dtoClass;
this.role = role;
final Set<Field> 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<Field> determineFieldsWithAccessForAnnotation(final Class<AssetDTO> dtoClass) {
final Set<Field> 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<AccessFor, Role[]> access, final Role role) {
if (field.isAnnotationPresent(AccessFor.class)) {
final AccessFor accessFor = field.getAnnotation(AccessFor.class);
return role.coversAny(access.apply(accessFor));
}
return false;
}
}
}

View File

@ -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;
}
}

View File

@ -1,214 +1,101 @@
package org.hostsharing.hsadminng.service.dto; package org.hostsharing.hsadminng.service.dto;
import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.commons.lang3.RandomStringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.RandomUtils; 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.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.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.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.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat; 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 @Test
@SpringBootTest(classes = { public void shouldHaveProperAccessForAdmin() {
AssetMapperImpl.class, initAccesFor(AssetDTO.class, Role.ADMIN).shouldBeExactlyFor(
AssetDTO.AssetJsonSerializer.class, "membershipId", "documentDate", "amount", "action", "valueDate", "remark");
AssetDTO.AssetJsonDeserializer.class, updateAccesFor(AssetDTO.class, Role.ADMIN).shouldBeExactlyFor("remark");
readAccesFor(AssetDTO.class, Role.ADMIN).shouldBeForAllFields();
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 @Test
public void shouldSerializePartiallyForFinancialCustomerContact() throws JsonProcessingException { public void shouldHaveProperAccessForContractualContact() {
initAccesFor(AssetDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeForNothing();
// given updateAccesFor(AssetDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeForNothing();
givenAuthenticatedUser(); readAccesFor(AssetDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeExactlyFor(
givenUserHavingRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.FINANCIAL_CONTACT); "id", "membershipId", "documentDate", "amount", "action", "valueDate", "membershipDisplayLabel");
final AssetDTO given = createSomeAssetDTO(SOME_ASSET_ID);
// when
final String actual = objectMapper.writeValueAsString(given);
// then
given.setRemark(null);
assertEquals(createExpectedJSon(given), actual);
} }
@Test @Test
public void shouldSerializeCompletelyForSupporter() throws JsonProcessingException { public void shouldHaveNoAccessForTechnicalContact() {
initAccesFor(AssetDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing();
// given updateAccesFor(AssetDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing();
givenAuthenticatedUser(); readAccesFor(AssetDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing();
givenUserHavingRole(Role.SUPPORTER);
final AssetDTO given = createSomeAssetDTO(SOME_ASSET_ID);
// when
final String actual = objectMapper.writeValueAsString(given);
// then
assertEquals(createExpectedJSon(given), actual);
} }
@Test @Test
public void shouldNotDeserializeForContractualCustomerContact() { public void shouldHaveNoAccessForNormalUsersWithinCustomerRealm() {
// given initAccesFor(AssetDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing();
givenAuthenticatedUser(); updateAccesFor(AssetDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing();
givenUserHavingRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.CONTRACTUAL_CONTACT); readAccesFor(AssetDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing();
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 @Test
public void shouldDeserializeForAdminIfRemarkIsChanged() throws IOException { public void shouldConvertToString() {
// given final AssetDTO dto = createDto(1234L);
givenAuthenticatedUser(); 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'}");
givenUserHavingRole(Role.ADMIN); }
final String json = new JSonBuilder()
.withFieldValue("id", SOME_ASSET_ID)
.withFieldValue("remark", "Updated Remark")
.toString();
// when @Test
final AssetDTO actual = objectMapper.readValue(json, AssetDTO.class); public void shouldImplementEqualsJustUsingClassAndId() {
final AssetDTO dto = createDto(1234L);
assertThat(dto.equals(dto)).isTrue();
// then final AssetDTO dtoWithSameId = createRandomDto(1234L);
final AssetDTO expected = new AssetDTO(); assertThat(dto.equals(dtoWithSameId)).isTrue();
expected.setId(SOME_ASSET_ID);
expected.setMembershipId(SOME_MEMBERSHIP_ID); final AssetDTO dtoWithAnotherId = createRandomDto(RandomUtils.nextLong(2000, 9999));
expected.setRemark("Updated Remark"); assertThat(dtoWithAnotherId.equals(dtoWithSameId)).isFalse();
expected.setMembershipDisplayLabel(SOME_MEMBERSHIP_DISPLAY_LABEL);
assertThat(actual).isEqualToIgnoringGivenFields(expected, "displayLabel"); 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 --- // --- only test fixture below ---
private String createExpectedJSon(AssetDTO dto) { private AssetDTO createDto(final Long id) {
return new JSonBuilder() final AssetDTO dto = new AssetDTO();
.withFieldValueIfPresent("id", dto.getId()) dto.setId(id);
.withFieldValueIfPresent("documentDate", dto.getDocumentDate().toString()) dto.setDocumentDate(LocalDate.parse("2000-12-07"));
.withFieldValueIfPresent("valueDate", dto.getValueDate().toString()) dto.setAmount(new BigDecimal("512.01"));
.withFieldValueIfPresent("action", dto.getAction().name()) dto.setAction(AssetAction.PAYMENT);
.withFieldValueIfPresent("amount", dto.getAmount().doubleValue()) dto.setRemark("Some Remark");
.withFieldValueIfPresent("remark", dto.getRemark()) dto.setValueDate(LocalDate.parse("2000-12-18"));
.withFieldValueIfPresent("membershipId", dto.getMembershipId()) dto.setMembershipId(888L);
.withFieldValue("membershipDisplayLabel", dto.getMembershipDisplayLabel()) dto.setMembershipDisplayLabel("Some Membership");
.toString(); return dto;
} }
private AssetDTO createSomeAssetDTO(final long id) { private AssetDTO createRandomDto(final Long id) {
final AssetDTO given = new AssetDTO(); final AssetDTO dto = new AssetDTO();
given.setId(id); dto.setId(id);
given.setAction(AssetAction.PAYMENT); final LocalDate randomDate = LocalDate.parse("2000-12-07").plusDays(RandomUtils.nextInt(1, 999));
given.setAmount(new BigDecimal("512.01")); dto.setDocumentDate(randomDate);
given.setDocumentDate(LocalDate.parse("2019-04-27")); dto.setAmount(new BigDecimal("512.01"));
given.setValueDate(LocalDate.parse("2019-04-28")); dto.setAction(AssetAction.PAYMENT);
given.setMembershipId(SOME_MEMBERSHIP_ID); dto.setRemark("Some Remark");
given.setRemark("Some Remark"); dto.setValueDate(randomDate.plusDays(RandomUtils.nextInt(1, 99)));
given.setMembershipDisplayLabel("Display Label for Membership #" + SOME_MEMBERSHIP_ID); dto.setMembershipId(RandomUtils.nextLong());
return given; dto.setMembershipDisplayLabel(RandomStringUtils.randomAlphabetic(20));
return dto;
} }
} }

View File

@ -73,7 +73,6 @@ public class ShareDTOUnitTest {
public void init() { public void init() {
given(jsonParser.getCodec()).willReturn(codec); given(jsonParser.getCodec()).willReturn(codec);
given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory);
given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory); given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory);
given(autowireCapableBeanFactory.createBean(CustomerService.class)).willReturn(customerService); given(autowireCapableBeanFactory.createBean(CustomerService.class)).willReturn(customerService);
given(autowireCapableBeanFactory.createBean(MembershipService.class)).willReturn(membershipService); given(autowireCapableBeanFactory.createBean(MembershipService.class)).willReturn(membershipService);

View File

@ -162,6 +162,7 @@ public class AssetResourceIntTest {
// Create the Asset // Create the Asset
AssetDTO assetDTO = assetMapper.toDto(asset); AssetDTO assetDTO = assetMapper.toDto(asset);
assetDTO.setMembershipDisplayLabel(null);
restAssetMockMvc.perform(post("/api/assets") restAssetMockMvc.perform(post("/api/assets")
.contentType(TestUtil.APPLICATION_JSON_UTF8) .contentType(TestUtil.APPLICATION_JSON_UTF8)
.content(TestUtil.convertObjectToJsonBytes(assetDTO))) .content(TestUtil.convertObjectToJsonBytes(assetDTO)))