Browse Source

DATACMNS-1231 - Add reactive auditing infrastructure.

We now provide a reactive variant for auditing with ReactiveAuditingHandler and ReactiveIsNewAwareAuditingHandler.

Extracted common auditing functionality into AuditingHandlerSupport which serves as base class for AuditingHandler and ReactiveAuditingHandler.

Original Pull Request: #458
pull/460/head
Mark Paluch 6 years ago committed by Christoph Strobl
parent
commit
d3af4a79e4
  1. 40
      src/main/asciidoc/auditing.adoc
  2. 130
      src/main/java/org/springframework/data/auditing/AuditingHandler.java
  3. 203
      src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java
  4. 84
      src/main/java/org/springframework/data/auditing/ReactiveAuditingHandler.java
  5. 70
      src/main/java/org/springframework/data/auditing/ReactiveIsNewAwareAuditingHandler.java
  6. 4
      src/main/java/org/springframework/data/domain/AuditorAware.java
  7. 35
      src/main/java/org/springframework/data/domain/ReactiveAuditorAware.java
  8. 108
      src/test/java/org/springframework/data/auditing/ReactiveAuditingHandlerUnitTests.java

40
src/main/asciidoc/auditing.adoc

@ -41,19 +41,49 @@ In case you use either `@CreatedBy` or `@LastModifiedBy`, the auditing infrastru @@ -41,19 +41,49 @@ In case you use either `@CreatedBy` or `@LastModifiedBy`, the auditing infrastru
The following example shows an implementation of the interface that uses Spring Security's `Authentication` object:
.Implementation of AuditorAware based on Spring Security
.Implementation of `AuditorAware` based on Spring Security
====
[source, java]
----
class SpringSecurityAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
----
====
The implementation accesses the `Authentication` object provided by Spring Security and looks up the custom `UserDetails` instance that you have created in your `UserDetailsService` implementation. We assume here that you are exposing the domain user through the `UserDetails` implementation but that, based on the `Authentication` found, you could also look it up from anywhere.
[[auditing.reactive-auditor-aware]]
=== `ReactiveAuditorAware`
When using reactive infrastructure you might want to make use of contextual information to provide `@CreatedBy` or `@LastModifiedBy` information.
We provide an `ReactiveAuditorAware<T>` SPI interface that you have to implement to tell the infrastructure who the current user or system interacting with the application is. The generic type `T` defines what type the properties annotated with `@CreatedBy` or `@LastModifiedBy` have to be.
The following example shows an implementation of the interface that uses reactive Spring Security's `Authentication` object:
.Implementation of `ReactiveAuditorAware` based on Spring Security
====
[source, java]
----
class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {
@Override
public Mono<User> getCurrentAuditor() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
----

130
src/main/java/org/springframework/data/auditing/AuditingHandler.java

@ -15,16 +15,12 @@ @@ -15,16 +15,12 @@
*/
package org.springframework.data.auditing;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.log.LogMessage;
import org.springframework.data.domain.Auditable;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
@ -39,16 +35,11 @@ import org.springframework.util.Assert; @@ -39,16 +35,11 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @since 1.5
*/
public class AuditingHandler implements InitializingBean {
public class AuditingHandler extends AuditingHandlerSupport implements InitializingBean {
private static final Log logger = LogFactory.getLog(AuditingHandler.class);
private final DefaultAuditableBeanWrapperFactory factory;
private DateTimeProvider dateTimeProvider = CurrentDateTimeProvider.INSTANCE;
private Optional<AuditorAware<?>> auditorAware;
private boolean dateTimeForNow = true;
private boolean modifyOnCreation = true;
/**
* Creates a new {@link AuditableBeanWrapper} using the given {@link MappingContext} when looking up auditing metadata
@ -73,9 +64,9 @@ public class AuditingHandler implements InitializingBean { @@ -73,9 +64,9 @@ public class AuditingHandler implements InitializingBean {
*/
public AuditingHandler(PersistentEntities entities) {
super(entities);
Assert.notNull(entities, "PersistentEntities must not be null!");
this.factory = new MappingAuditableBeanWrapperFactory(entities);
this.auditorAware = Optional.empty();
}
@ -90,36 +81,6 @@ public class AuditingHandler implements InitializingBean { @@ -90,36 +81,6 @@ public class AuditingHandler implements InitializingBean {
this.auditorAware = Optional.of(auditorAware);
}
/**
* Setter do determine if {@link Auditable#setCreatedDate(DateTime)} and
* {@link Auditable#setLastModifiedDate(DateTime)} shall be filled with the current Java time. Defaults to
* {@code true}. One might set this to {@code false} to use database features to set entity time.
*
* @param dateTimeForNow the dateTimeForNow to set
*/
public void setDateTimeForNow(boolean dateTimeForNow) {
this.dateTimeForNow = dateTimeForNow;
}
/**
* Set this to true if you want to treat entity creation as modification and thus setting the current date as
* modification date during creation, too. Defaults to {@code true}.
*
* @param modifyOnCreation if modification information shall be set on creation, too
*/
public void setModifyOnCreation(boolean modifyOnCreation) {
this.modifyOnCreation = modifyOnCreation;
}
/**
* Sets the {@link DateTimeProvider} to be used to determine the dates to be set.
*
* @param dateTimeProvider
*/
public void setDateTimeProvider(DateTimeProvider dateTimeProvider) {
this.dateTimeProvider = dateTimeProvider == null ? CurrentDateTimeProvider.INSTANCE : dateTimeProvider;
}
/**
* Marks the given object as created.
*
@ -129,7 +90,7 @@ public class AuditingHandler implements InitializingBean { @@ -129,7 +90,7 @@ public class AuditingHandler implements InitializingBean {
Assert.notNull(source, "Entity must not be null!");
return touch(source, true);
return markCreated(auditorAware.flatMap(AuditorAware::getCurrentAuditor).orElse(null), source);
}
/**
@ -141,86 +102,7 @@ public class AuditingHandler implements InitializingBean { @@ -141,86 +102,7 @@ public class AuditingHandler implements InitializingBean {
Assert.notNull(source, "Entity must not be null!");
return touch(source, false);
}
/**
* Returns whether the given source is considered to be auditable in the first place
*
* @param source must not be {@literal null}.
* @return
*/
protected final boolean isAuditable(Object source) {
Assert.notNull(source, "Source must not be null!");
return factory.getBeanWrapperFor(source).isPresent();
}
private <T> T touch(T target, boolean isNew) {
Optional<AuditableBeanWrapper<T>> wrapper = factory.getBeanWrapperFor(target);
return wrapper.map(it -> {
Optional<Object> auditor = touchAuditor(it, isNew);
Optional<TemporalAccessor> now = dateTimeForNow ? touchDate(it, isNew) : Optional.empty();
if (logger.isDebugEnabled()) {
Object defaultedNow = now.map(Object::toString).orElse("not set");
Object defaultedAuditor = auditor.map(Object::toString).orElse("unknown");
logger.debug(LogMessage.format("Touched %s - Last modification at %s by %s", target, defaultedNow, defaultedAuditor));
}
return it.getBean();
}).orElse(target);
}
/**
* Sets modifying and creating auditor. Creating auditor is only set on new auditables.
*
* @param auditable
* @return
*/
private Optional<Object> touchAuditor(AuditableBeanWrapper<?> wrapper, boolean isNew) {
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
return auditorAware.map(it -> {
Optional<?> auditor = it.getCurrentAuditor();
Assert.notNull(auditor,
() -> String.format("Auditor must not be null! Returned by: %s!", AopUtils.getTargetClass(it)));
auditor.filter(__ -> isNew).ifPresent(wrapper::setCreatedBy);
auditor.filter(__ -> !isNew || modifyOnCreation).ifPresent(wrapper::setLastModifiedBy);
return auditor;
});
}
/**
* Touches the auditable regarding modification and creation date. Creation date is only set on new auditables.
*
* @param wrapper
* @return
*/
private Optional<TemporalAccessor> touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) {
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
Optional<TemporalAccessor> now = dateTimeProvider.getNow();
Assert.notNull(now, () -> String.format("Now must not be null! Returned by: %s!", dateTimeProvider.getClass()));
now.filter(__ -> isNew).ifPresent(wrapper::setCreatedDate);
now.filter(__ -> !isNew || modifyOnCreation).ifPresent(wrapper::setLastModifiedDate);
return now;
return markModified(auditorAware.flatMap(AuditorAware::getCurrentAuditor).orElse(null), source);
}
/*
@ -229,6 +111,8 @@ public class AuditingHandler implements InitializingBean { @@ -229,6 +111,8 @@ public class AuditingHandler implements InitializingBean {
*/
public void afterPropertiesSet() {
super.afterPropertiesSet();
if (!auditorAware.isPresent()) {
logger.debug("No AuditorAware set! Auditing will not be applied!");
}

203
src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java

@ -0,0 +1,203 @@ @@ -0,0 +1,203 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.auditing;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.log.LogMessage;
import org.springframework.data.domain.Auditable;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Support class to implement auditing handlers.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.4
*/
public abstract class AuditingHandlerSupport implements InitializingBean {
private static final Log logger = LogFactory.getLog(AuditingHandlerSupport.class);
private final DefaultAuditableBeanWrapperFactory factory;
private DateTimeProvider dateTimeProvider = CurrentDateTimeProvider.INSTANCE;
private boolean dateTimeForNow = true;
private boolean modifyOnCreation = true;
/**
* Creates a new {@link AuditableBeanWrapper} using the given {@link PersistentEntities} when looking up auditing
* metadata via reflection.
*
* @param entities must not be {@literal null}.
*/
public AuditingHandlerSupport(PersistentEntities entities) {
Assert.notNull(entities, "PersistentEntities must not be null!");
this.factory = new MappingAuditableBeanWrapperFactory(entities);
}
/**
* Setter do determine if {@link Auditable#setCreatedDate(TemporalAccessor)}} and
* {@link Auditable#setLastModifiedDate(TemporalAccessor)} shall be filled with the current Java time. Defaults to
* {@code true}. One might set this to {@code false} to use database features to set entity time.
*
* @param dateTimeForNow the dateTimeForNow to set
*/
public void setDateTimeForNow(boolean dateTimeForNow) {
this.dateTimeForNow = dateTimeForNow;
}
/**
* Set this to true if you want to treat entity creation as modification and thus setting the current date as
* modification date during creation, too. Defaults to {@code true}.
*
* @param modifyOnCreation if modification information shall be set on creation, too
*/
public void setModifyOnCreation(boolean modifyOnCreation) {
this.modifyOnCreation = modifyOnCreation;
}
/**
* Sets the {@link DateTimeProvider} to be used to determine the dates to be set.
*
* @param dateTimeProvider
*/
public void setDateTimeProvider(@Nullable DateTimeProvider dateTimeProvider) {
this.dateTimeProvider = dateTimeProvider == null ? CurrentDateTimeProvider.INSTANCE : dateTimeProvider;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() {}
/**
* Returns whether the given source is considered to be auditable in the first place
*
* @param source must not be {@literal null}.
* @return
*/
protected final boolean isAuditable(Object source) {
Assert.notNull(source, "Source must not be null!");
return factory.getBeanWrapperFor(source).isPresent();
}
/**
* Marks the given object as created.
*
* @param auditor
* @param source
*/
<T> T markCreated(@Nullable Object auditor, T source) {
Assert.notNull(source, "Entity must not be null!");
return touch(auditor, source, true);
}
/**
* Marks the given object as modified.
*
* @param auditor
* @param source
*/
<T> T markModified(@Nullable Object auditor, T source) {
Assert.notNull(source, "Entity must not be null!");
return touch(auditor, source, false);
}
private <T> T touch(@Nullable Object auditor, T target, boolean isNew) {
Optional<AuditableBeanWrapper<T>> wrapper = factory.getBeanWrapperFor(target);
return wrapper.map(it -> {
if (auditor != null) {
touchAuditor(auditor, it, isNew);
}
Optional<TemporalAccessor> now = dateTimeForNow ? touchDate(it, isNew) : Optional.empty();
if (logger.isDebugEnabled()) {
Object defaultedNow = now.map(Object::toString).orElse("not set");
Object defaultedAuditor = auditor != null ? auditor.toString() : "unknown";
logger.debug(
LogMessage.format("Touched %s - Last modification at %s by %s", target, defaultedNow, defaultedAuditor));
}
return it.getBean();
}).orElse(target);
}
/**
* Sets modifying and creating auditor. Creating auditor is only set on new auditables.
*
* @param auditor
* @param wrapper
* @param isNew
* @return
*/
private void touchAuditor(Object auditor, AuditableBeanWrapper<?> wrapper, boolean isNew) {
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
Assert.notNull(auditor, "Auditor must not be null!");
if (isNew) {
wrapper.setCreatedBy(auditor);
}
if (!isNew || modifyOnCreation) {
wrapper.setLastModifiedBy(auditor);
}
}
/**
* Touches the auditable regarding modification and creation date. Creation date is only set on new auditables.
*
* @param wrapper
* @param isNew
* @return
*/
private Optional<TemporalAccessor> touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) {
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
Optional<TemporalAccessor> now = dateTimeProvider.getNow();
Assert.notNull(now, () -> String.format("Now must not be null! Returned by: %s!", dateTimeProvider.getClass()));
now.filter(__ -> isNew).ifPresent(wrapper::setCreatedDate);
now.filter(__ -> !isNew || modifyOnCreation).ifPresent(wrapper::setLastModifiedDate);
return now;
}
}

84
src/main/java/org/springframework/data/auditing/ReactiveAuditingHandler.java

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.auditing;
import reactor.core.publisher.Mono;
import java.util.Optional;
import org.springframework.data.domain.ReactiveAuditorAware;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.util.Assert;
/**
* Auditing handler to mark entity objects created and modified.
*
* @author Mark Paluch
* @since 2.4
*/
public class ReactiveAuditingHandler extends AuditingHandlerSupport {
private ReactiveAuditorAware<?> auditorAware = Mono::empty;
/**
* Creates a new {@link AuditableBeanWrapper} using the given {@link PersistentEntities} when looking up auditing
* metadata via reflection.
*
* @param entities must not be {@literal null}.
*/
public ReactiveAuditingHandler(PersistentEntities entities) {
super(entities);
}
/**
* Setter to inject a {@link ReactiveAuditorAware} component to retrieve the current auditor.
*
* @param auditorAware must not be {@literal null}.
*/
public void setAuditorAware(ReactiveAuditorAware<?> auditorAware) {
Assert.notNull(auditorAware, "AuditorAware must not be null!");
this.auditorAware = auditorAware;
}
/**
* Marks the given object as created.
*
* @param source must not be {@literal null}.
*/
public <T> Mono<T> markCreated(T source) {
Assert.notNull(source, "Entity must not be null!");
return auditorAware.getCurrentAuditor().map(Optional::of) //
.defaultIfEmpty(Optional.empty()) //
.map(auditor -> markCreated(auditor.orElse(null), source));
}
/**
* Marks the given object as modified.
*
* @param source must not be {@literal null}.
*/
public <T> Mono<T> markModified(T source) {
Assert.notNull(source, "Entity must not be null!");
return auditorAware.getCurrentAuditor().map(Optional::of) //
.defaultIfEmpty(Optional.empty()) //
.map(auditor -> markModified(auditor.orElse(null), source));
}
}

70
src/main/java/org/springframework/data/auditing/ReactiveIsNewAwareAuditingHandler.java

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.auditing;
import reactor.core.publisher.Mono;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.util.Assert;
/**
* {@link AuditingHandler} extension that uses {@link PersistentEntity#isNew(Object)} to expose a generic
* {@link #markAudited(Object)} method that will route calls to {@link #markCreated(Object)} or
* {@link #markModified(Object)} based on the {@link IsNewStrategy} determined from the factory.
*
* @author Mark Paluch
* @since 2.4
*/
public class ReactiveIsNewAwareAuditingHandler extends ReactiveAuditingHandler {
private final PersistentEntities entities;
/**
* Creates a new {@link ReactiveIsNewAwareAuditingHandler} for the given {@link MappingContext}.
*
* @param entities must not be {@literal null}.
*/
public ReactiveIsNewAwareAuditingHandler(PersistentEntities entities) {
super(entities);
this.entities = entities;
}
/**
* Marks the given object created or modified based on {@link PersistentEntity#isNew(Object)}. Will route the calls to
* {@link #markCreated(Object)} and {@link #markModified(Object)} accordingly.
*
* @param object must not be {@literal null}.
*/
public Mono<Object> markAudited(Object object) {
Assert.notNull(object, "Source object must not be null!");
if (!isAuditable(object)) {
return Mono.just(object);
}
PersistentEntity<?, ? extends PersistentProperty<?>> entity = entities
.getRequiredPersistentEntity(object.getClass());
return entity.isNew(object) ? markCreated(object) : markModified(object);
}
}

4
src/main/java/org/springframework/data/domain/AuditorAware.java

@ -20,7 +20,7 @@ import java.util.Optional; @@ -20,7 +20,7 @@ import java.util.Optional;
/**
* Interface for components that are aware of the application's current auditor. This will be some kind of user mostly.
*
* @param <T> the type of the auditing instance
* @param <T> the type of the auditing instance.
* @author Oliver Gierke
*/
public interface AuditorAware<T> {
@ -28,7 +28,7 @@ public interface AuditorAware<T> { @@ -28,7 +28,7 @@ public interface AuditorAware<T> {
/**
* Returns the current auditor of the application.
*
* @return the current auditor
* @return the current auditor.
*/
Optional<T> getCurrentAuditor();
}

35
src/main/java/org/springframework/data/domain/ReactiveAuditorAware.java

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
/*
* Copyright 2008-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.domain;
import reactor.core.publisher.Mono;
/**
* Interface for components that are aware of the application's current auditor. This will be some kind of user mostly.
*
* @param <T> the type of the auditing instance.
* @author Mark Paluch
* @since 2.4
*/
public interface ReactiveAuditorAware<T> {
/**
* Returns the current auditor of the application.
*
* @return the current auditor. If the mono does not emit a value, the auditor is considered to be unknown.
*/
Mono<T> getCurrentAuditor();
}

108
src/test/java/org/springframework/data/auditing/ReactiveAuditingHandlerUnitTests.java

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
/*
* Copyright 2008-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.auditing;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import lombok.Value;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Instant;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.domain.ReactiveAuditorAware;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.mapping.context.SampleMappingContext;
/**
* Unit test for {@link ReactiveAuditingHandler}.
*
* @author Mark Paluch
*/
@SuppressWarnings("unchecked")
class ReactiveAuditingHandlerUnitTests {
ReactiveAuditingHandler handler;
ReactiveAuditorAware<AuditedUser> auditorAware;
AuditedUser user;
@BeforeEach
void setUp() {
SampleMappingContext sampleMappingContext = new SampleMappingContext();
sampleMappingContext.getRequiredPersistentEntity(Immutable.class); // initialize to ensure we're using mapping
// metadata instead of plain reflection
handler = new ReactiveAuditingHandler(PersistentEntities.of(sampleMappingContext));
user = new AuditedUser();
auditorAware = mock(ReactiveAuditorAware.class);
when(auditorAware.getCurrentAuditor()).thenReturn(Mono.just(user));
}
@Test
void markCreatedShouldSetDatesIfAuditorNotSet() {
Immutable immutable = new Immutable(null, null, null, null);
handler.markCreated(immutable).as(StepVerifier::create).consumeNextWith(actual -> {
assertThat(actual.getCreatedDate()).isNotNull();
assertThat(actual.getModifiedDate()).isNotNull();
assertThat(actual.getCreatedBy()).isNull();
assertThat(actual.getModifiedBy()).isNull();
}).verifyComplete();
assertThat(immutable.getCreatedDate()).isNull();
}
@Test
void markModifiedSetsModifiedFields() {
AuditedUser audited = new AuditedUser();
audited.id = 1L;
handler.setAuditorAware(auditorAware);
handler.markModified(audited).as(StepVerifier::create).expectNext(audited).verifyComplete();
assertThat(audited.getCreatedBy()).isNotPresent();
assertThat(audited.getCreatedDate()).isNotPresent();
assertThat(audited.getLastModifiedBy()).isPresent();
assertThat(audited.getLastModifiedDate()).isPresent();
verify(auditorAware).getCurrentAuditor();
}
@Value
static class Immutable {
@CreatedDate Instant createdDate;
@CreatedBy String createdBy;
@LastModifiedDate Instant modifiedDate;
@LastModifiedBy String modifiedBy;
}
}
Loading…
Cancel
Save