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 <[email protected]> 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(_, _) + } +}
