You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/02/25 03:03:09 UTC

(grails-core) 16/18: more tests

This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 8.0.x-hibernate7
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 23066a68c664c618017a7671b65a19407eaf412f
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Tue Feb 24 18:49:44 2026 -0600

    more tests
---
 .../access/TraitPropertyAccessStrategySpec.groovy  | 262 +++++++++++++++++++++
 .../MultiTenantEventListenerSpec.groovy            | 229 ++++++++++++++++++
 2 files changed, 491 insertions(+)

diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/access/TraitPropertyAccessStrategySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/access/TraitPropertyAccessStrategySpec.groovy
new file mode 100644
index 0000000000..540338b416
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/access/TraitPropertyAccessStrategySpec.groovy
@@ -0,0 +1,262 @@
+/*
+ *  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.orm.hibernate.access
+
+import org.hibernate.property.access.spi.GetterFieldImpl
+import org.hibernate.property.access.spi.GetterMethodImpl
+import org.hibernate.property.access.spi.SetterFieldImpl
+import org.hibernate.property.access.spi.SetterMethodImpl
+import spock.lang.Specification
+import spock.lang.Unroll
+
+// ─── Test fixtures ────────────────────────────────────────────────────────────
+
+trait HasName {
+    String name
+}
+
+trait HasActive {
+    boolean active
+}
+
+trait HasFlag {
+    Boolean flag
+}
+
+/** Plain Groovy class — no trait involvement. */
+class PlainPerson {
+    String plain
+}
+
+/** Groovy class implementing a String trait. */
+class NamedEntity implements HasName {}
+
+/** Groovy class implementing a primitive-boolean trait. */
+class ActiveEntity implements HasActive {}
+
+/** Groovy class implementing a boxed-Boolean trait. */
+class FlaggedEntity implements HasFlag {}
+
+// ─── Spec ─────────────────────────────────────────────────────────────────────
+
+class TraitPropertyAccessStrategySpec extends Specification {
+
+    TraitPropertyAccessStrategy strategy = new TraitPropertyAccessStrategy()
+
+    // ─── getTraitFieldName ────────────────────────────────────────────────────
+
+    void "getTraitFieldName encodes dots as underscores with double-underscore separator"() {
+        expect:
+        strategy.getTraitFieldName(HasName, 'name') ==
+            'org_grails_orm_hibernate_access_HasName__name'
+    }
+
+    void "getTraitFieldName encodes different trait class correctly"() {
+        expect:
+        strategy.getTraitFieldName(HasActive, 'active') ==
+            'org_grails_orm_hibernate_access_HasActive__active'
+    }
+
+    void "getTraitFieldName replaces every dot in the package name"() {
+        given:
+        def fieldName = strategy.getTraitFieldName(HasName, 'name')
+
+        expect:
+        !fieldName.contains('.')
+        fieldName.contains('__')
+        fieldName.endsWith('__name')
+    }
+
+    // ─── buildPropertyAccess: String trait property ───────────────────────────
+
+    void "buildPropertyAccess returns non-null PropertyAccess for String trait property"() {
+        when:
+        def access = strategy.buildPropertyAccess(NamedEntity, 'name')
+
+        then:
+        access != null
+        access.getter != null
+        access.setter != null
+    }
+
+    void "PropertyAccess.getPropertyAccessStrategy returns the originating strategy"() {
+        given:
+        def access = strategy.buildPropertyAccess(NamedEntity, 'name')
+
+        expect:
+        access.propertyAccessStrategy.is(strategy)
+    }
+
+    void "getter and setter for String trait property are field-based"() {
+        given:
+        def access = strategy.buildPropertyAccess(NamedEntity, 'name')
+
+        expect:
+        access.getter instanceof GetterFieldImpl
+        access.setter instanceof SetterFieldImpl
+    }
+
+    void "getter.getReturnTypeClass returns String for String trait property"() {
+        given:
+        def access = strategy.buildPropertyAccess(NamedEntity, 'name')
+
+        expect:
+        access.getter.returnTypeClass == String
+    }
+
+    void "getter.getMember returns the backing trait Field for String property"() {
+        given:
+        def access = strategy.buildPropertyAccess(NamedEntity, 'name')
+
+        expect:
+        access.getter.getMember() instanceof java.lang.reflect.Field
+        (access.getter.getMember() as java.lang.reflect.Field).name ==
+            'org_grails_orm_hibernate_access_HasName__name'
+    }
+
+    // ─── buildPropertyAccess: primitive boolean trait property ───────────────
+
+    void "buildPropertyAccess resolves primitive boolean trait property via isXxx getter"() {
+        when:
+        def access = strategy.buildPropertyAccess(ActiveEntity, 'active')
+
+        then:
+        access != null
+        access.getter instanceof GetterFieldImpl
+        access.setter instanceof SetterFieldImpl
+    }
+
+    void "getter.getReturnTypeClass returns boolean for boolean trait property"() {
+        given:
+        def access = strategy.buildPropertyAccess(ActiveEntity, 'active')
+
+        expect:
+        access.getter.returnTypeClass == boolean
+    }
+
+    void "getter.getMember returns the backing trait Field for boolean property"() {
+        given:
+        def access = strategy.buildPropertyAccess(ActiveEntity, 'active')
+
+        expect:
+        (access.getter.getMember() as java.lang.reflect.Field).name ==
+            'org_grails_orm_hibernate_access_HasActive__active'
+    }
+
+    // ─── buildPropertyAccess: boxed Boolean trait property ───────────────────
+
+    void "buildPropertyAccess resolves boxed Boolean trait property via isXxx getter"() {
+        when:
+        def access = strategy.buildPropertyAccess(FlaggedEntity, 'flag')
+
+        then:
+        access != null
+        access.getter instanceof GetterFieldImpl
+        access.setter instanceof SetterFieldImpl
+    }
+
+    void "getter.getMember returns the backing trait Field for Boolean property"() {
+        given:
+        def access = strategy.buildPropertyAccess(FlaggedEntity, 'flag')
+
+        expect:
+        (access.getter.getMember() as java.lang.reflect.Field).name ==
+            'org_grails_orm_hibernate_access_HasFlag__flag'
+    }
+
+    // ─── buildPropertyAccess: error paths ────────────────────────────────────
+
+    void "buildPropertyAccess throws IllegalStateException for non-trait property"() {
+        when:
+        strategy.buildPropertyAccess(PlainPerson, 'plain')
+
+        then:
+        def e = thrown(IllegalStateException)
+        e.message.contains('plain')
+        e.message.contains('PlainPerson')
+        e.message.contains('not provided by a trait')
+    }
+
+    void "buildPropertyAccess throws IllegalStateException for non-existent property"() {
+        when:
+        strategy.buildPropertyAccess(NamedEntity, 'nonExistent')
+
+        then:
+        def e = thrown(IllegalStateException)
+        e.message.contains('nonExistent')
+        e.message.contains('not provided by a trait')
+    }
+
+    void "buildPropertyAccess error message includes class name"() {
+        when:
+        strategy.buildPropertyAccess(NamedEntity, 'missing')
+
+        then:
+        def e = thrown(IllegalStateException)
+        e.message.contains('NamedEntity')
+    }
+
+    // ─── 3-arg overload ───────────────────────────────────────────────────────
+
+    void "3-arg buildPropertyAccess delegates to 2-arg version"() {
+        given:
+        def access2 = strategy.buildPropertyAccess(NamedEntity, 'name')
+        def access3 = strategy.buildPropertyAccess(NamedEntity, 'name', true)
+
+        expect:
+        access2.getter.class     == access3.getter.class
+        access2.setter.class     == access3.setter.class
+        access3.propertyAccessStrategy.is(strategy)
+    }
+
+    @Unroll
+    void "3-arg overload with setterRequired=#req still resolves correctly"() {
+        when:
+        def access = strategy.buildPropertyAccess(NamedEntity, 'name', req)
+
+        then:
+        access.getter instanceof GetterFieldImpl
+
+        where:
+        req << [true, false]
+    }
+
+    // ─── multiple independent buildPropertyAccess calls ───────────────────────
+
+    void "two buildPropertyAccess calls for same class return independent instances"() {
+        given:
+        def access1 = strategy.buildPropertyAccess(NamedEntity, 'name')
+        def access2 = strategy.buildPropertyAccess(NamedEntity, 'name')
+
+        expect:
+        !access1.is(access2)
+        access1.getter.returnTypeClass == access2.getter.returnTypeClass
+    }
+
+    void "buildPropertyAccess works on two different trait-implementing classes"() {
+        given:
+        def nameAccess   = strategy.buildPropertyAccess(NamedEntity, 'name')
+        def activeAccess = strategy.buildPropertyAccess(ActiveEntity, 'active')
+
+        expect:
+        nameAccess.getter.returnTypeClass   == String
+        activeAccess.getter.returnTypeClass == boolean
+    }
+}
+
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListenerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListenerSpec.groovy
new file mode 100644
index 0000000000..4a0ef0b324
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListenerSpec.groovy
@@ -0,0 +1,229 @@
+/*
+ *  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.orm.hibernate.multitenancy
+
+import org.grails.datastore.mapping.engine.event.PreInsertEvent
+import org.grails.datastore.mapping.engine.event.PreUpdateEvent
+import org.grails.datastore.mapping.engine.event.ValidationEvent
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.types.TenantId
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.query.Query
+import org.grails.datastore.mapping.query.event.PreQueryEvent
+import org.grails.orm.hibernate.AbstractHibernateDatastore
+import org.springframework.context.ApplicationEvent
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class MultiTenantEventListenerSpec extends Specification {
+
+    MultiTenantEventListener listener = new MultiTenantEventListener()
+
+    // ─── supportsEventType ────────────────────────────────────────────────────
+
+    @Unroll
+    void "supportsEventType returns true for #type.simpleName"() {
+        expect:
+        listener.supportsEventType(type)
+
+        where:
+        type << [PreQueryEvent, ValidationEvent, PreInsertEvent, PreUpdateEvent]
+    }
+
+    void "supportsEventType returns false for generic ApplicationEvent"() {
+        expect:
+        !listener.supportsEventType(ApplicationEvent)
+    }
+
+    void "supportsEventType returns false for unrelated event type"() {
+        expect:
+        !listener.supportsEventType(Object)
+    }
+
+    // ─── supportsSourceType ───────────────────────────────────────────────────
+
+    void "supportsSourceType returns true for AbstractHibernateDatastore itself"() {
+        expect:
+        listener.supportsSourceType(AbstractHibernateDatastore)
+    }
+
+    void "supportsSourceType returns true for a subclass of AbstractHibernateDatastore"() {
+        given:
+        // anonymous subclass simulates a concrete HibernateDatastore
+        def subclass = Mock(AbstractHibernateDatastore).class
+
+        expect:
+        listener.supportsSourceType(AbstractHibernateDatastore)
+    }
+
+    void "supportsSourceType returns false for plain Datastore"() {
+        expect:
+        !listener.supportsSourceType(Object)
+    }
+
+    void "supportsSourceType returns false for String"() {
+        expect:
+        !listener.supportsSourceType(String)
+    }
+
+    // ─── getOrder ─────────────────────────────────────────────────────────────
+
+    void "getOrder returns DEFAULT_ORDER from PersistenceEventListener"() {
+        expect:
+        listener.getOrder() == org.grails.datastore.mapping.engine.event.PersistenceEventListener.DEFAULT_ORDER
+    }
+
+    // ─── onApplicationEvent: unsupported event type is silently ignored ───────
+
+    void "onApplicationEvent with unsupported event type does nothing"() {
+        given:
+        def unsupportedEvent = new ApplicationEvent("source") {}
+
+        when:
+        listener.onApplicationEvent(unsupportedEvent)
+
+        then:
+        noExceptionThrown()
+    }
+
+    // ─── onApplicationEvent: PreQueryEvent — non-multi-tenant entity ──────────
+
+    void "onApplicationEvent PreQueryEvent on non-multi-tenant entity does not call enableMultiTenancyFilter"() {
+        given:
+        def datastore = Mock(AbstractHibernateDatastore)
+        def entity    = Mock(PersistentEntity) { isMultiTenant() >> false }
+        def query     = Mock(Query) { getEntity() >> entity }
+        def event     = new PreQueryEvent(datastore, query)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        0 * datastore.enableMultiTenancyFilter()
+    }
+
+    // ─── onApplicationEvent: PreQueryEvent — multi-tenant entity ─────────────
+
+    void "onApplicationEvent PreQueryEvent on multi-tenant entity calls enableMultiTenancyFilter"() {
+        given:
+        def datastore = Mock(AbstractHibernateDatastore)
+        def entity    = Mock(PersistentEntity) { isMultiTenant() >> true }
+        def query     = Mock(Query) { getEntity() >> entity }
+        def event     = new PreQueryEvent(datastore, query)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        1 * datastore.enableMultiTenancyFilter()
+    }
+
+    void "onApplicationEvent PreQueryEvent with non-Hibernate source does not call enableMultiTenancyFilter"() {
+        given: "source is not an AbstractHibernateDatastore"
+        def nonHibernateDatastore = Mock(org.grails.datastore.mapping.core.Datastore)
+        def entity = Mock(PersistentEntity) { isMultiTenant() >> true }
+        def query  = Mock(Query) { getEntity() >> entity }
+        def event  = new PreQueryEvent(nonHibernateDatastore, query)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        noExceptionThrown()
+    }
+
+    // ─── onApplicationEvent: PreInsertEvent — non-multi-tenant entity ─────────
+
+    void "onApplicationEvent PreInsertEvent on non-multi-tenant entity sets no tenant"() {
+        given:
+        def datastore    = Mock(AbstractHibernateDatastore)
+        def entity       = Mock(PersistentEntity) { isMultiTenant() >> false }
+        def entityAccess = Mock(org.grails.datastore.mapping.engine.EntityAccess)
+        def event        = new PreInsertEvent(datastore, entity, entityAccess)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        0 * entityAccess.setProperty(_, _)
+    }
+
+    // ─── onApplicationEvent: PreInsertEvent — multi-tenant, no resolver → no-op ─
+
+    void "onApplicationEvent PreInsertEvent on multi-tenant entity does not set tenantId when resolver returns null"() {
+        given: "resolver returns null tenant — no-op path"
+        def resolver     = Mock(org.grails.datastore.mapping.multitenancy.TenantResolver) { resolveTenantIdentifier() >> null }
+        def tenantId     = Mock(TenantId) { getName() >> "tenantId" }
+        def entity       = Mock(PersistentEntity) {
+            isMultiTenant() >> true
+            getTenantId()   >> tenantId
+        }
+        def entityAccess = Mock(org.grails.datastore.mapping.engine.EntityAccess)
+        def datastore    = Mock(AbstractHibernateDatastore) { getTenantResolver() >> resolver }
+        def event        = new PreInsertEvent(datastore, entity, entityAccess)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then: "setProperty never called because currentId is null"
+        0 * entityAccess.setProperty(_, _)
+    }
+
+    // ─── onApplicationEvent: PreUpdateEvent — multi-tenant, no resolver → no-op ─
+
+    void "onApplicationEvent PreUpdateEvent on multi-tenant entity does not set tenantId when resolver returns null"() {
+        given:
+        def resolver     = Mock(org.grails.datastore.mapping.multitenancy.TenantResolver) { resolveTenantIdentifier() >> null }
+        def tenantId     = Mock(TenantId) { getName() >> "tenantId" }
+        def entity       = Mock(PersistentEntity) {
+            isMultiTenant() >> true
+            getTenantId()   >> tenantId
+        }
+        def entityAccess = Mock(org.grails.datastore.mapping.engine.EntityAccess)
+        def datastore    = Mock(AbstractHibernateDatastore) { getTenantResolver() >> resolver }
+        def event        = new PreUpdateEvent(datastore, entity, entityAccess)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        0 * entityAccess.setProperty(_, _)
+    }
+
+    // ─── onApplicationEvent: ValidationEvent — multi-tenant, no resolver → no-op ─
+
+    void "onApplicationEvent ValidationEvent on multi-tenant entity does not set tenantId when resolver returns null"() {
+        given:
+        def resolver     = Mock(org.grails.datastore.mapping.multitenancy.TenantResolver) { resolveTenantIdentifier() >> null }
+        def tenantId     = Mock(TenantId) { getName() >> "tenantId" }
+        def entity       = Mock(PersistentEntity) {
+            isMultiTenant() >> true
+            getTenantId()   >> tenantId
+        }
+        def entityAccess = Mock(org.grails.datastore.mapping.engine.EntityAccess)
+        def datastore    = Mock(AbstractHibernateDatastore) { getTenantResolver() >> resolver }
+        def event        = new ValidationEvent(datastore, entity, entityAccess)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        0 * entityAccess.setProperty(_, _)
+    }
+}