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 29f75717dfd3a43c1adc4293e26f68e3f8e232e4 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Fri Aug 14 10:12:12 2026 -0500 Add test coverage for org.grails.datastore.gorm.events (core) DomainEventListener, DefaultApplicationEventPublisher, and ConfigurableApplicationContextEventPublisher had zero test coverage. Adds mock-based Spock specs driving each class through its public constructor/method/onApplicationEvent surface only. Coverage: DomainEventListener 0% -> 92% lines / 91% branches / 96% methods; DefaultApplicationEventPublisher 0% -> 95% / 81% / 100%; ConfigurableApplicationContextEventPublisher 0% -> 100% / n/a / 100%. Three branches are documented as intentionally left uncovered in DomainEventListenerSpec: invokeEvent's ea==null path is unreachable via the public API, and its eventMethod.getParameterTypes().length==1 path is confirmed (via decompiling spring-core) structurally dead — findAndCacheEvent only ever caches zero-argument hook methods, so event-argument-accepting hooks can never be invoked. --- ...ableApplicationContextEventPublisherSpec.groovy | 71 +++ .../DefaultApplicationEventPublisherSpec.groovy | 163 ++++++ .../gorm/events/DomainEventListenerSpec.groovy | 556 +++++++++++++++++++++ 3 files changed, 790 insertions(+) diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/ConfigurableApplicationContextEventPublisherSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/ConfigurableApplicationContextEventPublisherSpec.groovy new file mode 100644 index 0000000000..f28f519144 --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/ConfigurableApplicationContextEventPublisherSpec.groovy @@ -0,0 +1,71 @@ +/* + * 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.context.ApplicationEvent +import org.springframework.context.ApplicationListener +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.PayloadApplicationEvent + +class ConfigurableApplicationContextEventPublisherSpec extends Specification { + + ConfigurableApplicationContext applicationContext = Mock(ConfigurableApplicationContext) + ConfigurableApplicationContextEventPublisher publisher = new ConfigurableApplicationContextEventPublisher(applicationContext) + + void "exposes the ApplicationContext it was constructed with"() { + expect: + publisher.applicationContext.is(applicationContext) + } + + void "addApplicationListener delegates to the wrapped ApplicationContext"() { + given: + ApplicationListener listener = Mock(ApplicationListener) + + when: + publisher.addApplicationListener(listener) + + then: + 1 * applicationContext.addApplicationListener(listener) + } + + void "publishEvent(ApplicationEvent) delegates to the wrapped ApplicationContext"() { + given: + ApplicationEvent event = new PayloadApplicationEvent<>(this, 'payload') + + when: + publisher.publishEvent(event) + + then: + 1 * applicationContext.publishEvent(event) + } + + void "publishEvent(Object) delegates to the wrapped ApplicationContext"() { + given: + Object payload = 'a plain payload' + + when: + publisher.publishEvent(payload) + + then: + 1 * applicationContext.publishEvent(payload) + } +} diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/DefaultApplicationEventPublisherSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/DefaultApplicationEventPublisherSpec.groovy new file mode 100644 index 0000000000..2c4e5fc689 --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/DefaultApplicationEventPublisherSpec.groovy @@ -0,0 +1,163 @@ +/* + * 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.context.ApplicationEvent +import org.springframework.context.ApplicationListener +import org.springframework.context.PayloadApplicationEvent +import org.springframework.context.event.SmartApplicationListener + +class DefaultApplicationEventPublisherSpec extends Specification { + + DefaultApplicationEventPublisher publisher = new DefaultApplicationEventPublisher() + + void "publishEvent(ApplicationEvent) does nothing when no listeners are registered"() { + given: + ApplicationEvent event = new PayloadApplicationEvent<>(this, 'payload') + + when: + publisher.publishEvent(event) + + then: + noExceptionThrown() + } + + void "publishEvent(Object) does nothing when no listeners are registered"() { + when: + publisher.publishEvent('a plain payload') + + then: + noExceptionThrown() + } + + void "publishEvent notifies a plain ApplicationListener regardless of event or source type"() { + given: + ApplicationListener listener = Mock(ApplicationListener) + publisher.addApplicationListener(listener) + ApplicationEvent event = new PayloadApplicationEvent<>(this, 'payload') + + when: + publisher.publishEvent(event) + + then: + 1 * listener.onApplicationEvent(event) + } + + void "publishEvent notifies every registered listener in order"() { + given: + ApplicationListener first = Mock(ApplicationListener) + ApplicationListener second = Mock(ApplicationListener) + publisher.addApplicationListener(first) + publisher.addApplicationListener(second) + ApplicationEvent event = new PayloadApplicationEvent<>(this, 'payload') + + when: + publisher.publishEvent(event) + + then: + 1 * first.onApplicationEvent(event) + 1 * second.onApplicationEvent(event) + } + + void "publishEvent notifies a SmartApplicationListener when it supports both the event type and source type"() { + given: + SmartApplicationListener listener = Mock(SmartApplicationListener) { + supportsEventType(_) >> true + supportsSourceType(_) >> true + } + publisher.addApplicationListener(listener) + ApplicationEvent event = new PayloadApplicationEvent<>(this, 'payload') + + when: + publisher.publishEvent(event) + + then: + 1 * listener.onApplicationEvent(event) + } + + void "publishEvent skips a SmartApplicationListener that does not support the event type"() { + given: + SmartApplicationListener listener = Mock(SmartApplicationListener) { + supportsEventType(_) >> false + } + publisher.addApplicationListener(listener) + ApplicationEvent event = new PayloadApplicationEvent<>(this, 'payload') + + when: + publisher.publishEvent(event) + + then: + 0 * listener.onApplicationEvent(_) + } + + void "publishEvent skips a SmartApplicationListener that supports the event type but not the source type"() { + given: + SmartApplicationListener listener = Mock(SmartApplicationListener) { + supportsEventType(_) >> true + supportsSourceType(_) >> false + } + publisher.addApplicationListener(listener) + ApplicationEvent event = new PayloadApplicationEvent<>(this, 'payload') + + when: + publisher.publishEvent(event) + + then: + 0 * listener.onApplicationEvent(_) + } + + void "publishEvent(Object) wraps the payload in a PayloadApplicationEvent sourced from the publisher itself"() { + given: + ApplicationListener listener = Mock(ApplicationListener) + publisher.addApplicationListener(listener) + + when: + publisher.publishEvent('a plain payload') + + then: + 1 * listener.onApplicationEvent({ ApplicationEvent e -> + e instanceof PayloadApplicationEvent && + e.source.is(publisher) && + e.payload == 'a plain payload' + }) + } + + void "publishEvent(Object) applies the same SmartApplicationListener filtering as publishEvent(ApplicationEvent)"() { + given: + SmartApplicationListener supported = Mock(SmartApplicationListener) { + supportsEventType(_) >> true + supportsSourceType(_) >> true + } + SmartApplicationListener unsupported = Mock(SmartApplicationListener) { + supportsEventType(_) >> false + } + publisher.addApplicationListener(supported) + publisher.addApplicationListener(unsupported) + + when: + publisher.publishEvent('a plain payload') + + then: + 1 * supported.onApplicationEvent(_) + 0 * unsupported.onApplicationEvent(_) + } +} diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/DomainEventListenerSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/DomainEventListenerSpec.groovy new file mode 100644 index 0000000000..3ddf2d0118 --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/DomainEventListenerSpec.groovy @@ -0,0 +1,556 @@ +/* + * 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 java.sql.Timestamp + +import spock.lang.Specification +import spock.lang.Unroll + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory +import org.springframework.context.ApplicationEvent +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.PayloadApplicationEvent + +import org.grails.datastore.mapping.config.Entity +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.core.connections.ConnectionSource +import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings +import org.grails.datastore.mapping.core.connections.ConnectionSources +import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider +import org.grails.datastore.mapping.dirty.checking.DirtyCheckable +import org.grails.datastore.mapping.engine.EntityAccess +import org.grails.datastore.mapping.engine.event.MergeEvent +import org.grails.datastore.mapping.engine.event.PersistEvent +import org.grails.datastore.mapping.engine.event.PostDeleteEvent +import org.grails.datastore.mapping.engine.event.PostInsertEvent +import org.grails.datastore.mapping.engine.event.PostLoadEvent +import org.grails.datastore.mapping.engine.event.PostUpdateEvent +import org.grails.datastore.mapping.engine.event.PreDeleteEvent +import org.grails.datastore.mapping.engine.event.PreInsertEvent +import org.grails.datastore.mapping.engine.event.PreLoadEvent +import org.grails.datastore.mapping.engine.event.PreUpdateEvent +import org.grails.datastore.mapping.engine.event.SaveOrUpdateEvent +import org.grails.datastore.mapping.engine.event.ValidationEvent +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.config.GormProperties + +/** + * Note on coverage gaps left deliberately untested: + * - {@code invokeEvent}'s {@code ea != null} branch is always true through every public before- + * and after-hook method, which never passes a null {@code EntityAccess}; the {@code ea == null} + * path is unreachable via the public API. + * - {@code invokeEvent}'s {@code eventMethod.getParameterTypes().length == 1} branch can never be + * taken: {@code findAndCacheEvent} caches hooks via Spring's {@code ReflectionUtils.findMethod(Class, String)}, + * which (confirmed via decompiling spring-core) only ever matches zero-argument methods, so a + * cached {@code eventMethod} can never have one parameter. Event-argument-accepting hooks appear + * to be an unreachable, effectively dead capability. + * - The protected {@code DomainEventListener(ConnectionSourcesProvider, MappingContext)} + * constructor exists solely for subclassing (e.g. {@code grails.gorm.rx.events.DomainEventListener}), + * which is covered by its own module's spec; exercising it here would duplicate that coverage. + */ +class DomainEventListenerSpec extends Specification { + + void "registers itself as a mapping context listener and creates event caches for entities present at construction time"() { + given: + RecordingDomain domain = new RecordingDomain() + PersistentEntity entity = entityFor(RecordingDomain) + MappingContext mappingContext = Mock(MappingContext) { + getPersistentEntities() >> [entity] + } + Datastore datastore = plainDatastore(mappingContext) + + when: + DomainEventListener listener = new DomainEventListener(datastore) + + then: + 1 * mappingContext.addMappingContextListener(_) + + when: 'the pre-existing entity\'s hook is invoked' + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + listener.beforeInsert(entity, ea) + + then: 'it fires immediately, proving the cache was created eagerly at construction time' + domain.invoked == ['beforeInsert'] + } + + void "persistentEntityAdded creates event caches for a newly discovered entity"() { + given: + RecordingDomain domain = new RecordingDomain() + PersistentEntity entity = entityFor(RecordingDomain) + Datastore datastore = plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }) + DomainEventListener listener = new DomainEventListener(datastore) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + expect: 'the hook is not yet wired up before the entity is added' + listener.beforeInsert(entity, ea) + domain.invoked.isEmpty() + + when: + listener.persistentEntityAdded(entity) + listener.beforeInsert(entity, ea) + + then: + domain.invoked == ['beforeInsert'] + } + + void "supportsEventType accepts AbstractPersistenceEvent subtypes and rejects unrelated ApplicationEvents"() { + given: + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + + expect: + listener.supportsEventType(PreInsertEvent) + !listener.supportsEventType(PayloadApplicationEvent) + } + + void "beforeInsert sets an initial numeric version to 0 when the entity is versioned"() { + given: + PersistentEntity entity = entityFor(NoHooksDomain, true, Long) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Mock(EntityAccess) { + getEntity() >> new NoHooksDomain() + getPersistentEntity() >> entity + } + + when: + listener.beforeInsert(entity, ea) + + then: + 1 * ea.setProperty(GormProperties.VERSION, 0) + } + + void "beforeInsert sets an initial java.sql.Timestamp version when the version type is a Timestamp"() { + given: + PersistentEntity entity = entityFor(NoHooksDomain, true, Timestamp) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Mock(EntityAccess) { + getEntity() >> new NoHooksDomain() + getPersistentEntity() >> entity + } + + when: + listener.beforeInsert(entity, ea) + + then: + 1 * ea.setProperty(GormProperties.VERSION, { it instanceof Timestamp }) + } + + void "beforeInsert sets an initial java.util.Date version when the version type is a plain Date"() { + given: + PersistentEntity entity = entityFor(NoHooksDomain, true, Date) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Mock(EntityAccess) { + getEntity() >> new NoHooksDomain() + getPersistentEntity() >> entity + } + + when: + listener.beforeInsert(entity, ea) + + then: + 1 * ea.setProperty(GormProperties.VERSION, { it.class == Date }) + } + + void "beforeInsert does not set a version when the version type is neither Number, Timestamp, nor Date"() { + given: + PersistentEntity entity = entityFor(NoHooksDomain, true, String) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Mock(EntityAccess) { + getEntity() >> new NoHooksDomain() + getPersistentEntity() >> entity + } + + when: + listener.beforeInsert(entity, ea) + + then: + 0 * ea.setProperty(*_) + } + + void "beforeInsert does not set a version when the entity is not versioned"() { + given: + PersistentEntity entity = entityFor(NoHooksDomain, false) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Mock(EntityAccess) { getEntity() >> new NoHooksDomain() } + + when: + listener.beforeInsert(entity, ea) + + then: + 0 * ea.setProperty(*_) + } + + void "beforeInsert returns true without error when the entity was never registered with the listener"() { + given: + PersistentEntity entity = entityFor(NoHooksDomain) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> new NoHooksDomain() } + + expect: + listener.beforeInsert(entity, ea) + } + + void "beforeInsert returns true without error when the domain class defines no beforeInsert hook"() { + given: + PersistentEntity entity = entityFor(NoHooksDomain) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + listener.persistentEntityAdded(entity) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> new NoHooksDomain() } + + expect: + listener.beforeInsert(entity, ea) + } + + @Unroll + void "the 2-arg #methodName(entity, ea) convenience overload dispatches to the corresponding hook"() { + given: + RecordingDomain domain = new RecordingDomain() + PersistentEntity entity = entityFor(RecordingDomain) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + listener.persistentEntityAdded(entity) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener."$methodName"(entity, ea) + + then: + domain.invoked == [hookName] + + where: + methodName | hookName + 'beforeInsert' | 'beforeInsert' + 'beforeUpdate' | 'beforeUpdate' + 'beforeDelete' | 'beforeDelete' + 'beforeLoad' | 'beforeLoad' + 'afterInsert' | 'afterInsert' + 'afterUpdate' | 'afterUpdate' + 'afterDelete' | 'afterDelete' + 'afterLoad' | 'afterLoad' + } + + void "afterInsert activates dirty checking on entities that implement DirtyCheckable"() { + given: + DirtyCheckableDomain domain = Spy(DirtyCheckableDomain) + PersistentEntity entity = entityFor(DirtyCheckableDomain) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.afterInsert(entity, ea) + + then: + 1 * domain.trackChanges() + } + + void "afterUpdate re-activates dirty checking on entities that implement DirtyCheckable"() { + given: + DirtyCheckableDomain domain = Spy(DirtyCheckableDomain) + PersistentEntity entity = entityFor(DirtyCheckableDomain) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.afterUpdate(entity, ea) + + then: + 1 * domain.trackChanges() + } + + void "afterLoad activates dirty checking on entities that implement DirtyCheckable"() { + given: + DirtyCheckableDomain domain = Spy(DirtyCheckableDomain) + PersistentEntity entity = entityFor(DirtyCheckableDomain) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.afterLoad(entity, ea) + + then: + 1 * domain.trackChanges() + } + + void "afterDelete does not activate dirty checking since the entity is no longer trackable"() { + given: + DirtyCheckableDomain domain = Spy(DirtyCheckableDomain) + PersistentEntity entity = entityFor(DirtyCheckableDomain) + DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.afterDelete(entity, ea) + + then: + 0 * domain.trackChanges() + } + + void "afterLoad autowires the entity when the datastore's default connection source is configured to autowire"() { + given: + NoHooksDomain domain = new NoHooksDomain() + PersistentEntity entity = entityFor(NoHooksDomain, false, null, false) + AutowireCapableBeanFactory beanFactory = Mock(AutowireCapableBeanFactory) + ConfigurableApplicationContext appContext = Stub(ConfigurableApplicationContext) { + getAutowireCapableBeanFactory() >> beanFactory + } + Datastore datastore = connectionAwareDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }, true, appContext) + DomainEventListener listener = new DomainEventListener(datastore) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.afterLoad(entity, ea) + + then: + 1 * beanFactory.autowireBeanProperties(domain, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false) + } + + void "afterLoad autowires the entity when the entity's own mapping requests autowire even though the datastore default does not"() { + given: + NoHooksDomain domain = new NoHooksDomain() + PersistentEntity entity = entityFor(NoHooksDomain, false, null, true) + AutowireCapableBeanFactory beanFactory = Mock(AutowireCapableBeanFactory) + ConfigurableApplicationContext appContext = Stub(ConfigurableApplicationContext) { + getAutowireCapableBeanFactory() >> beanFactory + } + Datastore datastore = plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }, appContext) + DomainEventListener listener = new DomainEventListener(datastore) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.afterLoad(entity, ea) + + then: + 1 * beanFactory.autowireBeanProperties(domain, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false) + } + + void "afterLoad does not autowire the entity when neither the datastore default nor the entity's mapping request it"() { + given: + NoHooksDomain domain = new NoHooksDomain() + PersistentEntity entity = entityFor(NoHooksDomain, false, null, false) + AutowireCapableBeanFactory beanFactory = Mock(AutowireCapableBeanFactory) + ConfigurableApplicationContext appContext = Stub(ConfigurableApplicationContext) { + getAutowireCapableBeanFactory() >> beanFactory + } + Datastore datastore = plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }, appContext) + DomainEventListener listener = new DomainEventListener(datastore) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.afterLoad(entity, ea) + + then: + 0 * beanFactory.autowireBeanProperties(*_) + } + + void "afterLoad requests autowiring without error when the datastore has no ApplicationContext to autowire through"() { + given: + NoHooksDomain domain = new NoHooksDomain() + PersistentEntity entity = entityFor(NoHooksDomain, false, null, false) + Datastore datastore = connectionAwareDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }, true, null) + DomainEventListener listener = new DomainEventListener(datastore) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.afterLoad(entity, ea) + + then: + notThrown(NullPointerException) + } + + @Unroll + void "onApplicationEvent dispatches a #eventType.simpleName to the #hookName hook"() { + given: + RecordingDomain domain = new RecordingDomain() + PersistentEntity entity = entityFor(RecordingDomain) + Datastore datastore = plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }) + DomainEventListener listener = new DomainEventListener(datastore) + listener.persistentEntityAdded(entity) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + ApplicationEvent event = eventType.newInstance(datastore, entity, ea) + + when: + listener.onApplicationEvent(event) + + then: + domain.invoked == [hookName] + + where: + eventType | hookName + PreInsertEvent | 'beforeInsert' + PostInsertEvent | 'afterInsert' + PreUpdateEvent | 'beforeUpdate' + PostUpdateEvent | 'afterUpdate' + PreDeleteEvent | 'beforeDelete' + PostDeleteEvent | 'afterDelete' + PreLoadEvent | 'beforeLoad' + PostLoadEvent | 'afterLoad' + } + + @Unroll + void "onApplicationEvent silently ignores a #eventType.simpleName since domain events define no hook for it"() { + given: + RecordingDomain domain = new RecordingDomain() + PersistentEntity entity = entityFor(RecordingDomain) + Datastore datastore = plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }) + DomainEventListener listener = new DomainEventListener(datastore) + listener.persistentEntityAdded(entity) + EntityAccess ea = Stub(EntityAccess) { getEntity() >> domain } + + when: + listener.onApplicationEvent(eventType.newInstance(datastore, entity, ea)) + + then: + noExceptionThrown() + domain.invoked.isEmpty() + + where: + eventType << [SaveOrUpdateEvent, ValidationEvent, MergeEvent, PersistEvent] + } + + @Unroll + void "onApplicationEvent cancels a #eventType.simpleName when its before-hook returns false"() { + given: + CancellingDomain domain = new CancellingDomain() + PersistentEntity entity = entityFor(CancellingDomain) + Datastore datastore = plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }) + DomainEventListener listener = new DomainEventListener(datastore) + listener.persistentEntityAdded(entity) + EntityAccess ea = Mock(EntityAccess) { getEntity() >> domain } + ApplicationEvent event = eventType.newInstance(datastore, entity, ea) + + when: + listener.onApplicationEvent(event) + + then: + event.cancelled + 0 * ea.refresh() + + where: + eventType << [PreInsertEvent, PreUpdateEvent, PreDeleteEvent] + } + + void "onApplicationEvent refreshes the entity access after a successful beforeInsert hook since beforeInsert is a refresh event"() { + given: + RecordingDomain domain = new RecordingDomain() + PersistentEntity entity = entityFor(RecordingDomain) + Datastore datastore = plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }) + DomainEventListener listener = new DomainEventListener(datastore) + listener.persistentEntityAdded(entity) + EntityAccess ea = Mock(EntityAccess) { getEntity() >> domain } + + when: + listener.onApplicationEvent(new PreInsertEvent(datastore, entity, ea)) + + then: + 1 * ea.refresh() + } + + void "onApplicationEvent does not refresh the entity access after a successful beforeLoad hook since beforeLoad is not a refresh event"() { + given: + RecordingDomain domain = new RecordingDomain() + PersistentEntity entity = entityFor(RecordingDomain) + Datastore datastore = plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] }) + DomainEventListener listener = new DomainEventListener(datastore) + listener.persistentEntityAdded(entity) + EntityAccess ea = Mock(EntityAccess) { getEntity() >> domain } + + when: + listener.onApplicationEvent(new PreLoadEvent(datastore, entity, ea)) + + then: + 0 * ea.refresh() + } + + private PersistentEntity entityFor(Class<?> javaClass, boolean versioned = false, Class<?> versionType = null, + boolean mappedAutowire = false) { + PersistentProperty version = versioned ? Stub(PersistentProperty) { getType() >> versionType } : null + ClassMapping mapping = Stub(ClassMapping) { + getMappedForm() >> new Entity(autowire: mappedAutowire) + } + Stub(PersistentEntity) { + getJavaClass() >> javaClass + isVersioned() >> versioned + getVersion() >> version + getMapping() >> mapping + } + } + + private Datastore plainDatastore(MappingContext mappingContext, ConfigurableApplicationContext appContext = null) { + Stub(Datastore) { + getMappingContext() >> mappingContext + getApplicationContext() >> appContext + } + } + + private Datastore connectionAwareDatastore(MappingContext mappingContext, boolean autowire, + ConfigurableApplicationContext appContext = null) { + ConnectionSource connectionSource = Stub(ConnectionSource) { + getSettings() >> new ConnectionSourceSettings(autowire: autowire) + } + ConnectionSources connectionSources = Stub(ConnectionSources) { + getDefaultConnectionSource() >> connectionSource + } + Stub(Datastore, additionalInterfaces: [ConnectionSourcesProvider]) { + getMappingContext() >> mappingContext + getApplicationContext() >> appContext + getConnectionSources() >> connectionSources + } + } +} + +class RecordingDomain { + + List<String> invoked = [] + + void beforeInsert() { invoked << 'beforeInsert' } + + void beforeUpdate() { invoked << 'beforeUpdate' } + + void beforeDelete() { invoked << 'beforeDelete' } + + void beforeLoad() { invoked << 'beforeLoad' } + + void afterInsert() { invoked << 'afterInsert' } + + void afterUpdate() { invoked << 'afterUpdate' } + + void afterDelete() { invoked << 'afterDelete' } + + void afterLoad() { invoked << 'afterLoad' } +} + +class CancellingDomain { + + boolean beforeInsert() { false } + + boolean beforeUpdate() { false } + + boolean beforeDelete() { false } +} + +class NoHooksDomain { + +} + +class DirtyCheckableDomain implements DirtyCheckable { + +}
