This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch test/document-datamapping-events in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit da3a9fe60a6e8b16c704a41dd2cb61170f9cc48e Author: Walter Duque de Estrada <[email protected]> AuthorDate: Fri Aug 14 10:25:09 2026 -0500 Add construction/dispatch test coverage for AutoTimestampEventListener The existing AutoTimestampEventListenerSpec only drove the suppression logic and beforeInsert/beforeUpdate directly via a test subclass that bypassed initForMappingContext entirely, leaving construction, real entity/property scanning (storeDateCreatedAndLastUpdatedInfo, including @CreatedDate/@LastModifiedDate/@CreatedBy/@LastModifiedBy annotation detection), the deferred-initialization path, setApplicationContext, supportsEventType, and onApplicationEvent dispatch untested. Coverage: 84% -> 98% lines, 61% -> 83% branches, 76% -> 96% methods. --- ...toTimestampEventListenerConstructionSpec.groovy | 347 +++++++++++++++++++++ 1 file changed, 347 insertions(+) diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListenerConstructionSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListenerConstructionSpec.groovy new file mode 100644 index 0000000000..38eb5eace5 --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListenerConstructionSpec.groovy @@ -0,0 +1,347 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.grails.datastore.gorm.events + +import spock.lang.Specification + +import org.springframework.beans.factory.NoSuchBeanDefinitionException +import org.springframework.context.ApplicationContext + +import grails.gorm.annotation.CreatedBy +import grails.gorm.annotation.CreatedDate +import grails.gorm.annotation.LastModifiedBy +import grails.gorm.annotation.LastModifiedDate +import org.grails.datastore.gorm.timestamp.AuditorAware +import org.grails.datastore.gorm.timestamp.TimestampProvider +import org.grails.datastore.mapping.config.Entity +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.engine.EntityAccess +import org.grails.datastore.mapping.engine.event.PostInsertEvent +import org.grails.datastore.mapping.engine.event.PreInsertEvent +import org.grails.datastore.mapping.engine.event.PreUpdateEvent +import org.grails.datastore.mapping.model.ClassMapping +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.PropertyMapping +import org.grails.datastore.mapping.config.Property + +/** + * Covers the surface of {@link AutoTimestampEventListener} that + * {@code org.grails.datastore.gorm.timestamp.AutoTimestampEventListenerSpec} does not exercise: + * construction via a real {@link Datastore}/{@link MappingContext} (which drives the real + * {@code storeDateCreatedAndLastUpdatedInfo} scanning logic, including annotation detection), + * event dispatch via the public {@code onApplicationEvent}/{@code supportsEventType} surface, and + * {@code setApplicationContext}. The protected {@code AutoTimestampEventListener(MappingContext)} + * constructor is exercised only through {@code org.grails.gorm.rx.events.AutoTimestampEventListener}, + * its sole subclass, covered by that module's own spec. + */ +class AutoTimestampEventListenerConstructionSpec extends Specification { + + void "construction scans already-initialized entities and registers a dateCreated/lastUpdated-by-name property"() { + given: + PersistentEntity entity = entityFor(WithConventionalNames, true) + + when: + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([entity])) + + then: + listener.getDateCreatedPropertyNames(WithConventionalNames.name) == ['dateCreated'] as Set + listener.getLastUpdatedPropertyNames(WithConventionalNames.name) == ['lastUpdated'] as Set + } + + void "construction registers @CreatedDate/@LastModifiedDate annotated properties regardless of their name"() { + given: + PersistentEntity entity = entityFor(WithDateAnnotations, true) + + when: + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([entity])) + + then: + listener.getDateCreatedPropertyNames(WithDateAnnotations.name) == ['whenCreated'] as Set + listener.getLastUpdatedPropertyNames(WithDateAnnotations.name) == ['whenModified'] as Set + } + + void "construction registers @CreatedBy/@LastModifiedBy annotated properties as auditor fields"() { + given: + PersistentEntity entity = entityFor(WithAuditorAnnotations, true) + + when: + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([entity])) + + then: + listener.getCreatedByPropertyNames(WithAuditorAnnotations.name) == ['createdBy'] as Set + listener.getUpdatedByPropertyNames(WithAuditorAnnotations.name) == ['lastModifiedBy'] as Set + } + + void "construction ignores plain properties that carry no auto-timestamp or auditor annotation"() { + given: + PersistentEntity entity = entityFor(PlainDomain, true) + + when: + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([entity])) + + then: + listener.getDateCreatedPropertyNames(PlainDomain.name) == null + listener.getLastUpdatedPropertyNames(PlainDomain.name) == null + listener.getCreatedByPropertyNames(PlainDomain.name) == null + listener.getUpdatedByPropertyNames(PlainDomain.name) == null + } + + void "construction skips scanning an entity whose mapping explicitly disables autoTimestamp"() { + given: + PersistentEntity entity = entityFor(WithConventionalNames, true, false) + + when: + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([entity])) + + then: + listener.getDateCreatedPropertyNames(WithConventionalNames.name) == null + listener.getLastUpdatedPropertyNames(WithConventionalNames.name) == null + } + + void "construction scans an entity that has no mapped form at all as if autoTimestamp were enabled"() { + given: + PersistentEntity entity = Stub(PersistentEntity) { + getName() >> WithConventionalNames.name + isInitialized() >> true + getMapping() >> Stub(ClassMapping) { getMappedForm() >> null } + getPersistentProperties() >> propertiesFor(WithConventionalNames) + } + + when: + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([entity])) + + then: + listener.getDateCreatedPropertyNames(WithConventionalNames.name) == ['dateCreated'] as Set + } + + void "storeTimestampAvailability does not register a dateCreated/lastUpdated property whose type the TimestampProvider does not support"() { + given: + PersistentEntity entity = entityFor(WithConventionalNames, true) + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([])) + listener.setTimestampProvider(Stub(TimestampProvider) { + supportsCreating(_) >> false + }) + + when: + listener.persistentEntityAdded(entity) + + then: + listener.getDateCreatedPropertyNames(WithConventionalNames.name) == null + listener.getLastUpdatedPropertyNames(WithConventionalNames.name) == null + } + + void "an uninitialized entity is deferred and only scanned once beforeInsert is actually invoked for it"() { + given: 'an entity that starts out not initialized, but becomes initialized by the time its hook fires' + boolean initialized = false + PersistentEntity entity = Stub(PersistentEntity) { + getName() >> WithConventionalNames.name + isInitialized() >> { initialized } + getMapping() >> Stub(ClassMapping) { getMappedForm() >> new Entity() } + getPersistentProperties() >> propertiesFor(WithConventionalNames) + } + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([entity])) + + expect: 'nothing was scanned yet, since the entity reported itself as not initialized' + listener.getDateCreatedPropertyNames(WithConventionalNames.name) == null + + when: + initialized = true + WithConventionalNames domain = new WithConventionalNames() + EntityAccess ea = Stub(EntityAccess) { + getEntity() >> domain + getPropertyValue(_) >> null + getPropertyType(_) >> Date + } + listener.beforeInsert(entity, ea) + + then: 'the deferred scan ran, so the property is now known and was applied to the entity access' + listener.getDateCreatedPropertyNames(WithConventionalNames.name) == ['dateCreated'] as Set + } + + void "setApplicationContext wires the AuditorAware bean when one is present"() { + given: + AuditorAware auditorAware = Stub(AuditorAware) + ApplicationContext applicationContext = Stub(ApplicationContext) { + getBean(AuditorAware) >> auditorAware + } + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([])) + + when: + listener.setApplicationContext(applicationContext) + + then: + listener.auditorAware.is(auditorAware) + } + + void "setApplicationContext silently leaves auditorAware unset when no AuditorAware bean is registered"() { + given: + ApplicationContext applicationContext = Stub(ApplicationContext) { + getBean(AuditorAware) >> { throw new NoSuchBeanDefinitionException(AuditorAware) } + } + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([])) + + when: + listener.setApplicationContext(applicationContext) + + then: + noExceptionThrown() + listener.auditorAware == null + } + + void "supportsEventType accepts only PreInsertEvent and PreUpdateEvent"() { + given: + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastoreFor([])) + + expect: + listener.supportsEventType(PreInsertEvent) + listener.supportsEventType(PreUpdateEvent) + !listener.supportsEventType(PostInsertEvent) + } + + void "onApplicationEvent dispatches a PreInsertEvent to beforeInsert"() { + given: + PersistentEntity entity = entityFor(WithConventionalNames, true) + Datastore datastore = datastoreFor([entity]) + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastore) + WithConventionalNames domain = new WithConventionalNames() + EntityAccess ea = Mock(EntityAccess) { + getEntity() >> domain + getPropertyType(_) >> Date + } + + when: + listener.onApplicationEvent(new PreInsertEvent(datastore, entity, ea)) + + then: + 2 * ea.setProperty(_, _) + } + + void "onApplicationEvent dispatches a PreUpdateEvent to beforeUpdate"() { + given: + PersistentEntity entity = entityFor(WithConventionalNames, true) + Datastore datastore = datastoreFor([entity]) + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastore) + WithConventionalNames domain = new WithConventionalNames() + EntityAccess ea = Mock(EntityAccess) { + getEntity() >> domain + getPropertyType(_) >> Date + } + + when: + listener.onApplicationEvent(new PreUpdateEvent(datastore, entity, ea)) + + then: + 1 * ea.setProperty('lastUpdated', _) + } + + void "onApplicationEvent ignores an event whose entity is null"() { + given: + Datastore datastore = datastoreFor([]) + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastore) + EntityAccess ea = Mock(EntityAccess) + + when: + listener.onApplicationEvent(new PreInsertEvent(datastore, null, ea)) + + then: + noExceptionThrown() + 0 * ea.setProperty(*_) + } + + void "onApplicationEvent ignores event types other than PreInsert and PreUpdate"() { + given: + PersistentEntity entity = entityFor(WithConventionalNames, true) + Datastore datastore = datastoreFor([entity]) + AutoTimestampEventListener listener = new AutoTimestampEventListener(datastore) + EntityAccess ea = Mock(EntityAccess) { getEntity() >> new WithConventionalNames() } + + when: + listener.onApplicationEvent(new PostInsertEvent(datastore, entity, ea)) + + then: + noExceptionThrown() + 0 * ea.setProperty(*_) + } + + private Datastore datastoreFor(List<PersistentEntity> entities) { + Stub(Datastore) { + getMappingContext() >> Stub(MappingContext) { + getPersistentEntities() >> entities + } + } + } + + private PersistentEntity entityFor(Class<?> javaClass, boolean initialized, Boolean autoTimestamp = null) { + Entity mappedForm = autoTimestamp == null ? new Entity() : new Entity(autoTimestamp: autoTimestamp) + Stub(PersistentEntity) { + getName() >> javaClass.name + isInitialized() >> initialized + getMapping() >> Stub(ClassMapping) { getMappedForm() >> mappedForm } + getPersistentProperties() >> propertiesFor(javaClass) + } + } + + private List<PersistentProperty> propertiesFor(Class<?> javaClass) { + javaClass.declaredFields.findAll { !it.synthetic }.collect { field -> + Property mappedForm = new Property() + PersistentEntity owner = Stub(PersistentEntity) { + getName() >> javaClass.name + getJavaClass() >> javaClass + } + Stub(PersistentProperty) { + getName() >> field.name + getType() >> field.type + getOwner() >> owner + getMapping() >> Stub(PropertyMapping) { getMappedForm() >> mappedForm } + } + } + } +} + +class WithConventionalNames { + + Date dateCreated + Date lastUpdated +} + +class WithDateAnnotations { + + @CreatedDate + Date whenCreated + + @LastModifiedDate + Date whenModified +} + +class WithAuditorAnnotations { + + @CreatedBy + String createdBy + + @LastModifiedBy + String lastModifiedBy +} + +class PlainDomain { + + String name +}
