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(_, _)
+ }
+}