This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch test/document-datamapping-core-services in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit ea7581ca891a3468cd0a303a9b5624d26962c68e Author: Walter Duque de Estrada <[email protected]> AuthorDate: Sat Aug 15 18:52:30 2026 -0500 Add unit test coverage for DefaultTenantService/DefaultTransactionService Both classes had zero test coverage in org.grails.datastore.gorm.services. Mock-based Spock specs cover all public methods including the multi-tenancy-mode/datastore-capability branch checks, bringing each class to 100% line/branch/method coverage. Co-Authored-By: Claude Sonnet 5 <[email protected]> --- .../gorm/services/DefaultTenantServiceSpec.groovy | 205 +++++++++++++++++++++ .../services/DefaultTransactionServiceSpec.groovy | 186 +++++++++++++++++++ 2 files changed, 391 insertions(+) diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTenantServiceSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTenantServiceSpec.groovy new file mode 100644 index 0000000000..65f39221b2 --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTenantServiceSpec.groovy @@ -0,0 +1,205 @@ +/* + * 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.services + +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.model.DatastoreConfigurationException +import org.grails.datastore.mapping.multitenancy.AllTenantsResolver +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings +import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore +import org.grails.datastore.mapping.multitenancy.TenantResolver +import spock.lang.Specification + +class DefaultTenantServiceSpec extends Specification { + + DefaultTenantService tenantService = new DefaultTenantService() + + void 'multiTenantDatastore throws when the datastore is not multi-tenant capable'() { + given: + tenantService.datastore = Mock(Datastore) + + when: + tenantService.eachTenant {} + + then: + DatastoreConfigurationException e = thrown(DatastoreConfigurationException) + e.message.contains('not Multi-Tenant capable') + } + + void 'currentId throws when multi tenancy mode is NONE'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.NONE + tenantService.datastore = datastore + + when: + tenantService.currentId() + + then: + DatastoreConfigurationException e = thrown(DatastoreConfigurationException) + e.message.contains('not configured for Multi-Tenancy') + } + + void 'currentId resolves the tenant id from the tenant resolver'() { + given: + TenantResolver tenantResolver = Mock(TenantResolver) { + resolveTenantIdentifier() >> 'tenant1' + } + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR + datastore.tenantResolver >> tenantResolver + tenantService.datastore = datastore + + expect: + tenantService.currentId() == 'tenant1' + } + + void 'withoutId throws when multi tenancy mode is NONE'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.NONE + tenantService.datastore = datastore + + when: + tenantService.withoutId { 'result' } + + then: + thrown(DatastoreConfigurationException) + } + + void 'withoutId with a shared connection mode executes the callable without a session'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR + tenantService.datastore = datastore + + when: + String result = tenantService.withoutId { -> 'result' } + + then: + result == 'result' + 0 * datastore.withSession(_) + 0 * datastore.withNewSession(_, _) + } + + void 'withoutId with a non-shared connection mode executes within a new session'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.DATABASE + datastore.withNewSession(_, _) >> { args -> args[1].call('session') } + tenantService.datastore = datastore + + expect: + tenantService.withoutId { 'result' } == 'result' + } + + void 'withId throws when multi tenancy mode is NONE'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.NONE + tenantService.datastore = datastore + + when: + tenantService.withId('tenant1') { 'result' } + + then: + thrown(DatastoreConfigurationException) + } + + void 'withId with a shared connection mode executes the callable directly'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.SCHEMA + tenantService.datastore = datastore + + expect: + tenantService.withId('tenant1') { tenantId -> "result-$tenantId" } == 'result-tenant1' + } + + void 'withId with a non-shared connection mode executes within a new session for the tenant'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.DATABASE + datastore.withNewSession('tenant1', _) >> { args -> args[1].call('session') } + tenantService.datastore = datastore + + expect: + tenantService.withId('tenant1') { 'result' } == 'result' + } + + void 'withCurrent throws when multi tenancy mode is NONE'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.NONE + tenantService.datastore = datastore + + when: + tenantService.withCurrent { 'result' } + + then: + thrown(DatastoreConfigurationException) + } + + void 'withCurrent executes the callable with the resolved current tenant id'() { + given: + TenantResolver tenantResolver = Mock(TenantResolver) { + resolveTenantIdentifier() >> 'tenant1' + } + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.SCHEMA + datastore.tenantResolver >> tenantResolver + tenantService.datastore = datastore + + expect: + tenantService.withCurrent { tenantId -> "result-$tenantId" } == 'result-tenant1' + } + + void 'eachTenant throws when multi tenancy mode is NONE'() { + given: + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.NONE + tenantService.datastore = datastore + + when: + tenantService.eachTenant {} + + then: + thrown(UnsupportedOperationException) + } + + void 'eachTenant with a shared connection mode invokes the callable for every resolved tenant id'() { + given: + AllTenantsResolver tenantResolver = Mock(AllTenantsResolver) { + resolveTenantIds() >> ['tenant1', 'tenant2'] + } + MultiTenantCapableDatastore datastore = Mock(MultiTenantCapableDatastore) + datastore.multiTenancyMode >> MultiTenancySettings.MultiTenancyMode.SCHEMA + datastore.tenantResolver >> tenantResolver + tenantService.datastore = datastore + + and: + List<Serializable> seen = [] + + when: + tenantService.eachTenant { tenantId -> seen << tenantId } + + then: + seen == ['tenant1', 'tenant2'] + } +} diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTransactionServiceSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTransactionServiceSpec.groovy new file mode 100644 index 0000000000..99be3d643e --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/services/DefaultTransactionServiceSpec.groovy @@ -0,0 +1,186 @@ +/* + * 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.services + +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.TransactionStatus +import org.springframework.transaction.TransactionSystemException + +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.transactions.CustomizableRollbackTransactionAttribute +import org.grails.datastore.mapping.transactions.TransactionCapableDatastore +import spock.lang.Specification + +class DefaultTransactionServiceSpec extends Specification { + + DefaultTransactionService transactionService = new DefaultTransactionService() + + PlatformTransactionManager transactionManager = Mock(PlatformTransactionManager) { + getTransaction(_) >> Mock(TransactionStatus) + } + + TransactionCapableDatastore transactionCapableDatastore = Mock(TransactionCapableDatastore) { + getTransactionManager() >> transactionManager + } + + void 'withTransaction(Closure) executes the callable within a transaction'() { + given: + transactionService.datastore = transactionCapableDatastore + + when: + String result = transactionService.withTransaction { status -> 'done' } + + then: + result == 'done' + 1 * transactionManager.commit(_) + } + + void 'withTransaction(Closure) throws when the datastore does not support transactions'() { + given: + transactionService.datastore = Mock(Datastore) + + when: + transactionService.withTransaction { status -> 'done' } + + then: + TransactionSystemException e = thrown(TransactionSystemException) + e.message.contains('does not support transactions') + } + + void 'withRollback(Closure) executes the callable and rolls back'() { + given: + transactionService.datastore = transactionCapableDatastore + + expect: + transactionService.withRollback { status -> 'done' } == 'done' + } + + void 'withRollback(Closure) throws when the datastore does not support transactions'() { + given: + transactionService.datastore = Mock(Datastore) + + when: + transactionService.withRollback { status -> 'done' } + + then: + thrown(TransactionSystemException) + } + + void 'withNewTransaction(Closure) executes the callable with PROPAGATION_REQUIRES_NEW'() { + given: + transactionService.datastore = transactionCapableDatastore + + expect: + transactionService.withNewTransaction { status -> 'done' } == 'done' + } + + void 'withNewTransaction(Closure) throws when the datastore does not support transactions'() { + given: + transactionService.datastore = Mock(Datastore) + + when: + transactionService.withNewTransaction { status -> 'done' } + + then: + thrown(TransactionSystemException) + } + + void 'withTransaction(TransactionDefinition, Closure) executes the callable with the given definition'() { + given: + transactionService.datastore = transactionCapableDatastore + TransactionDefinition definition = new CustomizableRollbackTransactionAttribute() + + expect: + transactionService.withTransaction(definition) { status -> 'done' } == 'done' + } + + void 'withTransaction(TransactionDefinition, Closure) throws when the datastore does not support transactions'() { + given: + transactionService.datastore = Mock(Datastore) + TransactionDefinition definition = new CustomizableRollbackTransactionAttribute() + + when: + transactionService.withTransaction(definition) { status -> 'done' } + + then: + thrown(TransactionSystemException) + } + + void 'withTransaction(Map, Closure) builds a transaction definition from the map'() { + given: + transactionService.datastore = transactionCapableDatastore + + expect: + transactionService.withTransaction([readOnly: true]) { status -> 'done' } == 'done' + } + + void 'withTransaction(Map, Closure) throws when the datastore does not support transactions'() { + given: + transactionService.datastore = Mock(Datastore) + + when: + transactionService.withTransaction([readOnly: true]) { status -> 'done' } + + then: + thrown(TransactionSystemException) + } + + void 'withRollback(TransactionDefinition, Closure) executes the callable and rolls back'() { + given: + transactionService.datastore = transactionCapableDatastore + TransactionDefinition definition = new CustomizableRollbackTransactionAttribute() + + expect: + transactionService.withRollback(definition) { status -> 'done' } == 'done' + } + + void 'withRollback(TransactionDefinition, Closure) throws when the datastore does not support transactions'() { + given: + transactionService.datastore = Mock(Datastore) + TransactionDefinition definition = new CustomizableRollbackTransactionAttribute() + + when: + transactionService.withRollback(definition) { status -> 'done' } + + then: + thrown(TransactionSystemException) + } + + void 'withNewTransaction(TransactionDefinition, Closure) forces PROPAGATION_REQUIRES_NEW'() { + given: + transactionService.datastore = transactionCapableDatastore + TransactionDefinition definition = new CustomizableRollbackTransactionAttribute() + + expect: + transactionService.withNewTransaction(definition) { status -> 'done' } == 'done' + } + + void 'withNewTransaction(TransactionDefinition, Closure) throws when the datastore does not support transactions'() { + given: + transactionService.datastore = Mock(Datastore) + TransactionDefinition definition = new CustomizableRollbackTransactionAttribute() + + when: + transactionService.withNewTransaction(definition) { status -> 'done' } + + then: + thrown(TransactionSystemException) + } +}
