diff --git a/src/main/asciidoc/auditing.adoc b/src/main/asciidoc/auditing.adoc index fd5fc8f45..ba571e3e8 100644 --- a/src/main/asciidoc/auditing.adoc +++ b/src/main/asciidoc/auditing.adoc @@ -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 { + @Override public Optional 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` 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 { + + @Override + public Mono getCurrentAuditor() { + + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getPrincipal) + .map(User.class::cast); } } ---- diff --git a/src/main/java/org/springframework/data/auditing/AuditingHandler.java b/src/main/java/org/springframework/data/auditing/AuditingHandler.java index 11c5f65ec..44f5ad1fb 100644 --- a/src/main/java/org/springframework/data/auditing/AuditingHandler.java +++ b/src/main/java/org/springframework/data/auditing/AuditingHandler.java @@ -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; * @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; - 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 { */ 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 { 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 { 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 { 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 touch(T target, boolean isNew) { - - Optional> wrapper = factory.getBeanWrapperFor(target); - - return wrapper.map(it -> { - - Optional auditor = touchAuditor(it, isNew); - Optional 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 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 touchDate(AuditableBeanWrapper wrapper, boolean isNew) { - - Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!"); - - Optional 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 { */ public void afterPropertiesSet() { + super.afterPropertiesSet(); + if (!auditorAware.isPresent()) { logger.debug("No AuditorAware set! Auditing will not be applied!"); } diff --git a/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java b/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java new file mode 100644 index 000000000..ad9846455 --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java @@ -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 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 markModified(@Nullable Object auditor, T source) { + + Assert.notNull(source, "Entity must not be null!"); + + return touch(auditor, source, false); + } + + private T touch(@Nullable Object auditor, T target, boolean isNew) { + + Optional> wrapper = factory.getBeanWrapperFor(target); + + return wrapper.map(it -> { + + if (auditor != null) { + touchAuditor(auditor, it, isNew); + } + Optional 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 touchDate(AuditableBeanWrapper wrapper, boolean isNew) { + + Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!"); + + Optional 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; + } +} diff --git a/src/main/java/org/springframework/data/auditing/ReactiveAuditingHandler.java b/src/main/java/org/springframework/data/auditing/ReactiveAuditingHandler.java new file mode 100644 index 000000000..42e48b95e --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/ReactiveAuditingHandler.java @@ -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 Mono 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 Mono 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)); + } +} diff --git a/src/main/java/org/springframework/data/auditing/ReactiveIsNewAwareAuditingHandler.java b/src/main/java/org/springframework/data/auditing/ReactiveIsNewAwareAuditingHandler.java new file mode 100644 index 000000000..18e950e44 --- /dev/null +++ b/src/main/java/org/springframework/data/auditing/ReactiveIsNewAwareAuditingHandler.java @@ -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 markAudited(Object object) { + + Assert.notNull(object, "Source object must not be null!"); + + if (!isAuditable(object)) { + return Mono.just(object); + } + + PersistentEntity> entity = entities + .getRequiredPersistentEntity(object.getClass()); + + return entity.isNew(object) ? markCreated(object) : markModified(object); + } +} diff --git a/src/main/java/org/springframework/data/domain/AuditorAware.java b/src/main/java/org/springframework/data/domain/AuditorAware.java index 8da971d95..86501e73d 100644 --- a/src/main/java/org/springframework/data/domain/AuditorAware.java +++ b/src/main/java/org/springframework/data/domain/AuditorAware.java @@ -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 the type of the auditing instance + * @param the type of the auditing instance. * @author Oliver Gierke */ public interface AuditorAware { @@ -28,7 +28,7 @@ public interface AuditorAware { /** * Returns the current auditor of the application. * - * @return the current auditor + * @return the current auditor. */ Optional getCurrentAuditor(); } diff --git a/src/main/java/org/springframework/data/domain/ReactiveAuditorAware.java b/src/main/java/org/springframework/data/domain/ReactiveAuditorAware.java new file mode 100644 index 000000000..c6f1cff69 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/ReactiveAuditorAware.java @@ -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 the type of the auditing instance. + * @author Mark Paluch + * @since 2.4 + */ +public interface ReactiveAuditorAware { + + /** + * 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 getCurrentAuditor(); +} diff --git a/src/test/java/org/springframework/data/auditing/ReactiveAuditingHandlerUnitTests.java b/src/test/java/org/springframework/data/auditing/ReactiveAuditingHandlerUnitTests.java new file mode 100755 index 000000000..e7bbebc6f --- /dev/null +++ b/src/test/java/org/springframework/data/auditing/ReactiveAuditingHandlerUnitTests.java @@ -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 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; + } +}