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/08/15 18:57:32 UTC
(grails-core) 01/03: Add unit test coverage for the jdbc package in grails-datamapping-core
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch chore/jdbc-package-cleanup
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 830f1992c8c1a33b5f1c139c7aec1f7287668815
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Aug 15 11:35:35 2026 -0500
Add unit test coverage for the jdbc package in grails-datamapping-core
Every previously-untested class under org.grails.datastore.gorm.jdbc
(DatabaseDriver, MultiTenantConnection, MultiTenantDataSource,
PropertyOrigin, OriginCapablePropertyValue, RelaxedNames,
RelaxedConversionService, RelaxedDataBinder) and its connections
subpackage (DataSourceSettings, DataSourceSettingsBuilder,
DataSourceConnectionSource, DataSourceConnectionSourceFactory,
CachedDataSourceConnectionSourceFactory,
SpringDataSourceConnectionSourceFactory) now has a Spock spec.
Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
---
.../datastore/gorm/jdbc/DatabaseDriverSpec.groovy | 89 +++++++++++++++
.../gorm/jdbc/MultiTenantConnectionSpec.groovy | 104 +++++++++++++++++
.../gorm/jdbc/MultiTenantDataSourceSpec.groovy | 70 ++++++++++++
.../jdbc/OriginCapablePropertyValueSpec.groovy | 90 +++++++++++++++
.../datastore/gorm/jdbc/PropertyOriginSpec.groovy | 49 ++++++++
.../gorm/jdbc/RelaxedConversionServiceSpec.groovy | 116 +++++++++++++++++++
.../gorm/jdbc/RelaxedDataBinderSpec.groovy | 123 +++++++++++++++++++++
.../datastore/gorm/jdbc/RelaxedNamesSpec.groovy | 77 +++++++++++++
...hedDataSourceConnectionSourceFactorySpec.groovy | 58 ++++++++++
.../DataSourceConnectionSourceFactorySpec.groovy | 115 +++++++++++++++++++
.../DataSourceConnectionSourceSpec.groovy | 96 ++++++++++++++++
.../DataSourceSettingsBuilderSpec.groovy | 75 +++++++++++++
.../jdbc/connections/DataSourceSettingsSpec.groovy | 123 +++++++++++++++++++++
...ingDataSourceConnectionSourceFactorySpec.groovy | 82 ++++++++++++++
14 files changed, 1267 insertions(+)
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DatabaseDriverSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DatabaseDriverSpec.groovy
new file mode 100644
index 0000000000..a120f32380
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/DatabaseDriverSpec.groovy
@@ -0,0 +1,89 @@
+/*
+ * 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.jdbc
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class DatabaseDriverSpec extends Specification {
+
+ @Unroll
+ void "fromJdbcUrl resolves '#url' to #expected"() {
+ expect:
+ DatabaseDriver.fromJdbcUrl(url) == expected
+
+ where:
+ url | expected
+ 'jdbc:h2:mem:testDb' | DatabaseDriver.H2
+ 'jdbc:mysql://localhost:3306/test' | DatabaseDriver.MYSQL
+ 'jdbc:mariadb://localhost:3306/test' | DatabaseDriver.MARIADB
+ 'jdbc:postgresql://localhost:5432/test' | DatabaseDriver.POSTGRESQL
+ 'jdbc:oracle:thin:@localhost:1521:orcl' | DatabaseDriver.ORACLE
+ 'jdbc:sqlserver://localhost:1433;databaseName=x' | DatabaseDriver.SQLSERVER
+ 'jdbc:hsqldb:mem:testDb' | DatabaseDriver.HSQLDB
+ 'jdbc:sqlite:test.db' | DatabaseDriver.SQLITE
+ 'jdbc:derby:memory:testDb' | DatabaseDriver.DERBY
+ 'jdbc:unknowndb:foo' | DatabaseDriver.UNKNOWN
+ null | DatabaseDriver.UNKNOWN
+ '' | DatabaseDriver.UNKNOWN
+ }
+
+ void "fromJdbcUrl throws IllegalArgumentException for a URL not starting with jdbc"() {
+ when:
+ DatabaseDriver.fromJdbcUrl('mysql://localhost:3306/test')
+
+ then:
+ thrown(IllegalArgumentException)
+ }
+
+ @Unroll
+ void "fromProductName resolves '#productName' to #expected"() {
+ expect:
+ DatabaseDriver.fromProductName(productName) == expected
+
+ where:
+ productName | expected
+ 'H2' | DatabaseDriver.H2
+ 'h2' | DatabaseDriver.H2
+ 'MySQL' | DatabaseDriver.MYSQL
+ 'PostgreSQL' | DatabaseDriver.POSTGRESQL
+ 'Oracle' | DatabaseDriver.ORACLE
+ 'Firebird 3.0' | DatabaseDriver.FIREBIRD
+ 'DB2/LINUXX8664' | DatabaseDriver.DB2
+ 'DB2 UDB for AS/400' | DatabaseDriver.DB2_AS400
+ 'Something running on AS/400' | DatabaseDriver.DB2_AS400
+ 'Completely unknown product' | DatabaseDriver.UNKNOWN
+ null | DatabaseDriver.UNKNOWN
+ '' | DatabaseDriver.UNKNOWN
+ }
+
+ void "getDriverClassName and getValidationQuery and getXaDataSourceClassName return the expected values for H2"() {
+ expect:
+ DatabaseDriver.H2.driverClassName == 'org.h2.Driver'
+ DatabaseDriver.H2.xaDataSourceClassName == 'org.h2.jdbcx.JdbcDataSource'
+ DatabaseDriver.H2.validationQuery == 'SELECT 1'
+ }
+
+ void "UNKNOWN driver has no driver class name, xa data source or validation query"() {
+ expect:
+ DatabaseDriver.UNKNOWN.driverClassName == null
+ DatabaseDriver.UNKNOWN.xaDataSourceClassName == null
+ DatabaseDriver.UNKNOWN.validationQuery == null
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnectionSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnectionSpec.groovy
new file mode 100644
index 0000000000..491f2daf1f
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantConnectionSpec.groovy
@@ -0,0 +1,104 @@
+/*
+ * 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.jdbc
+
+import java.sql.Connection
+
+import org.grails.datastore.gorm.jdbc.schema.SchemaHandler
+import spock.lang.Specification
+
+class MultiTenantConnectionSpec extends Specification {
+
+ void "close restores the default schema before delegating to the target connection when open"() {
+ given:
+ def target = Mock(Connection) { isClosed() >> false }
+ def schemaHandler = Mock(SchemaHandler)
+ def connection = new MultiTenantConnection(target, schemaHandler)
+
+ when:
+ connection.close()
+
+ then:
+ 1 * schemaHandler.useDefaultSchema(connection)
+ 1 * target.close()
+ }
+
+ void "close does not restore the default schema when the connection is already closed"() {
+ given:
+ def target = Mock(Connection) { isClosed() >> true }
+ def schemaHandler = Mock(SchemaHandler)
+ def connection = new MultiTenantConnection(target, schemaHandler)
+
+ when:
+ connection.close()
+
+ then:
+ 0 * schemaHandler.useDefaultSchema(_)
+ 1 * target.close()
+ }
+
+ void "close still closes the target connection when restoring the schema throws"() {
+ given:
+ def target = Mock(Connection) { isClosed() >> false }
+ def schemaHandler = Mock(SchemaHandler) {
+ useDefaultSchema(_) >> { throw new RuntimeException('boom') }
+ }
+ def connection = new MultiTenantConnection(target, schemaHandler)
+
+ when:
+ connection.close()
+
+ then:
+ thrown(RuntimeException)
+ 1 * target.close()
+ }
+
+ void "other Connection methods delegate to the target connection"() {
+ given:
+ def target = Mock(Connection)
+ def schemaHandler = Mock(SchemaHandler)
+ def connection = new MultiTenantConnection(target, schemaHandler)
+
+ when:
+ connection.isReadOnly()
+
+ then:
+ 1 * target.isReadOnly() >> true
+
+ when:
+ def autoCommit = connection.getAutoCommit()
+
+ then:
+ 1 * target.getAutoCommit() >> false
+ !autoCommit
+ }
+
+ void "target and schemaHandler are exposed"() {
+ given:
+ def target = Mock(Connection)
+ def schemaHandler = Mock(SchemaHandler)
+
+ when:
+ def connection = new MultiTenantConnection(target, schemaHandler)
+
+ then:
+ connection.target.is(target)
+ connection.schemaHandler.is(schemaHandler)
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantDataSourceSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantDataSourceSpec.groovy
new file mode 100644
index 0000000000..0a55213cf5
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/MultiTenantDataSourceSpec.groovy
@@ -0,0 +1,70 @@
+/*
+ * 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.jdbc
+
+import javax.sql.DataSource
+
+import spock.lang.Specification
+
+class MultiTenantDataSourceSpec extends Specification {
+
+ void "target and tenantId are exposed"() {
+ given:
+ def target = Mock(DataSource)
+
+ when:
+ def dataSource = new MultiTenantDataSource(target, 'tenantA')
+
+ then:
+ dataSource.target.is(target)
+ dataSource.tenantId == 'tenantA'
+ }
+
+ void "getConnection delegates to the target data source"() {
+ given:
+ def target = Mock(DataSource)
+ def connection = Mock(java.sql.Connection)
+ def dataSource = new MultiTenantDataSource(target, 'tenantA')
+
+ when:
+ def result = dataSource.getConnection()
+
+ then:
+ 1 * target.getConnection() >> connection
+ result.is(connection)
+ }
+
+ void "two instances with the same tenantId are equal regardless of target"() {
+ given:
+ def targetA = Mock(DataSource)
+ def targetB = Mock(DataSource)
+
+ expect:
+ new MultiTenantDataSource(targetA, 'tenantA') == new MultiTenantDataSource(targetB, 'tenantA')
+ new MultiTenantDataSource(targetA, 'tenantA').hashCode() == new MultiTenantDataSource(targetB, 'tenantA').hashCode()
+ }
+
+ void "two instances with different tenantIds are not equal"() {
+ given:
+ def target = Mock(DataSource)
+
+ expect:
+ new MultiTenantDataSource(target, 'tenantA') != new MultiTenantDataSource(target, 'tenantB')
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/OriginCapablePropertyValueSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/OriginCapablePropertyValueSpec.groovy
new file mode 100644
index 0000000000..6e71bcbbf3
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/OriginCapablePropertyValueSpec.groovy
@@ -0,0 +1,90 @@
+/*
+ * 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.jdbc
+
+import org.springframework.beans.PropertyValue
+import org.springframework.core.env.PropertySource
+import spock.lang.Specification
+
+class OriginCapablePropertyValueSpec extends Specification {
+
+ private static PropertySource<String> namedSource(String name) {
+ new PropertySource<String>(name, 'value') {
+ @Override
+ Object getProperty(String propertyName) { null }
+ }
+ }
+
+ void "getName and getValue expose the values passed to the constructor"() {
+ given:
+ def origin = new PropertyOrigin(namedSource('mySource'), 'original.name')
+
+ when:
+ def propertyValue = new OriginCapablePropertyValue('bound.name', 'boundValue', origin)
+
+ then:
+ propertyValue.name == 'bound.name'
+ propertyValue.value == 'boundValue'
+ }
+
+ void "toString includes the origin name and the source name"() {
+ given:
+ def origin = new PropertyOrigin(namedSource('myPropertySource'), 'original.name')
+ def propertyValue = new OriginCapablePropertyValue('bound.name', 'boundValue', origin)
+
+ expect:
+ propertyValue.toString() == "'original.name' from 'myPropertySource'"
+ }
+
+ void "toString reports an unknown source when the origin has no source"() {
+ given:
+ def origin = new PropertyOrigin(null, 'original.name')
+ def propertyValue = new OriginCapablePropertyValue('bound.name', 'boundValue', origin)
+
+ expect:
+ propertyValue.toString() == "'original.name' from 'unknown'"
+ }
+
+ void "static getOrigin returns the origin of an OriginCapablePropertyValue directly"() {
+ given:
+ def origin = new PropertyOrigin(namedSource('myPropertySource'), 'original.name')
+ def propertyValue = new OriginCapablePropertyValue('bound.name', 'boundValue', origin)
+
+ expect:
+ OriginCapablePropertyValue.getOrigin(propertyValue).is(origin)
+ }
+
+ void "static getOrigin returns the propertyOrigin attribute for a plain PropertyValue"() {
+ given:
+ def origin = new PropertyOrigin(namedSource('myPropertySource'), 'original.name')
+ def propertyValue = new PropertyValue('bound.name', 'boundValue')
+ propertyValue.setAttribute('propertyOrigin', origin)
+
+ expect:
+ OriginCapablePropertyValue.getOrigin(propertyValue).is(origin)
+ }
+
+ void "static getOrigin returns null for a plain PropertyValue with no propertyOrigin attribute"() {
+ given:
+ def propertyValue = new PropertyValue('bound.name', 'boundValue')
+
+ expect:
+ OriginCapablePropertyValue.getOrigin(propertyValue) == null
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/PropertyOriginSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/PropertyOriginSpec.groovy
new file mode 100644
index 0000000000..c4a25a5acc
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/PropertyOriginSpec.groovy
@@ -0,0 +1,49 @@
+/*
+ * 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.jdbc
+
+import org.springframework.core.env.PropertySource
+import spock.lang.Specification
+
+class PropertyOriginSpec extends Specification {
+
+ void "getSource and getName return the values passed to the constructor"() {
+ given:
+ def source = new PropertySource<String>('mySource', 'value') {
+ @Override
+ Object getProperty(String name) { null }
+ }
+
+ when:
+ def origin = new PropertyOrigin(source, 'my.original.name')
+
+ then:
+ origin.source.is(source)
+ origin.name == 'my.original.name'
+ }
+
+ void "source may be null"() {
+ when:
+ def origin = new PropertyOrigin(null, 'my.original.name')
+
+ then:
+ origin.source == null
+ origin.name == 'my.original.name'
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedConversionServiceSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedConversionServiceSpec.groovy
new file mode 100644
index 0000000000..248d70abee
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedConversionServiceSpec.groovy
@@ -0,0 +1,116 @@
+/*
+ * 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.jdbc
+
+import org.springframework.core.convert.ConversionFailedException
+import org.springframework.core.convert.TypeDescriptor
+import org.springframework.core.convert.support.DefaultConversionService
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class RelaxedConversionServiceSpec extends Specification {
+
+ static enum Color {
+ RED, GREEN_BLUE
+ }
+
+ void "convert delegates to the provided root conversionService when it can handle the conversion"() {
+ given:
+ def service = new RelaxedConversionService(new DefaultConversionService())
+
+ expect:
+ service.convert('42', Integer) == 42
+ service.canConvert(String, Integer)
+ }
+
+ void "canConvert(TypeDescriptor, TypeDescriptor) delegates to the provided root conversionService"() {
+ given:
+ def service = new RelaxedConversionService(new DefaultConversionService())
+
+ expect:
+ service.canConvert(TypeDescriptor.valueOf(String), TypeDescriptor.valueOf(Integer))
+ }
+
+ void "convert falls back to the additional converters for String to char[] which the root does not support"() {
+ given:
+ def service = new RelaxedConversionService(new DefaultConversionService())
+
+ expect:
+ service.convert('abcde', char[].class) == 'abcde'.toCharArray()
+ }
+
+ void "convert works with a null root conversionService using only the additional converters"() {
+ given:
+ def service = new RelaxedConversionService(null)
+
+ expect:
+ service.convert('abcde', char[].class) == 'abcde'.toCharArray()
+ service.canConvert(String, char[].class)
+ }
+
+ static class Unconvertible {
+ private Unconvertible() {}
+ }
+
+ void "canConvert returns false for an unsupported conversion"() {
+ given:
+ def service = new RelaxedConversionService(new DefaultConversionService())
+
+ expect:
+ !service.canConvert(String, Unconvertible)
+ }
+
+ @Unroll
+ void "convert resolves '#source' to enum #expected ignoring case and separator style"() {
+ given:
+ def service = new RelaxedConversionService(new DefaultConversionService())
+
+ expect:
+ service.convert(source, Color) == expected
+
+ where:
+ source | expected
+ 'RED' | Color.RED
+ 'red' | Color.RED
+ 'Red' | Color.RED
+ 'GREEN_BLUE' | Color.GREEN_BLUE
+ 'green-blue' | Color.GREEN_BLUE
+ 'green_blue' | Color.GREEN_BLUE
+ }
+
+ void "convert returns null for an empty string when the target is an enum"() {
+ given:
+ def service = new RelaxedConversionService(new DefaultConversionService())
+
+ expect:
+ service.convert('', Color) == null
+ }
+
+ void "convert throws a ConversionFailedException for an unrecognized enum constant"() {
+ given:
+ def service = new RelaxedConversionService(new DefaultConversionService())
+
+ when:
+ service.convert('not-a-color', Color)
+
+ then:
+ def ex = thrown(ConversionFailedException)
+ ex.cause instanceof IllegalArgumentException
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedDataBinderSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedDataBinderSpec.groovy
new file mode 100644
index 0000000000..df6f25c8fc
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedDataBinderSpec.groovy
@@ -0,0 +1,123 @@
+/*
+ * 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.jdbc
+
+import org.springframework.beans.MutablePropertyValues
+import spock.lang.Specification
+
+class RelaxedDataBinderSpec extends Specification {
+
+ static class NestedBean {
+ String value
+ }
+
+ static class SimpleBean {
+ String fooBar
+ String jdbcUrl
+ Map<String, Object> options = [:]
+ NestedBean nested
+ }
+
+ void "a dash separated property name is bound to the matching camelCase property"() {
+ given:
+ def target = new SimpleBean()
+ def binder = new RelaxedDataBinder(target)
+ def values = new MutablePropertyValues()
+ values.add('foo-bar', 'baz')
+
+ when:
+ binder.bind(values)
+
+ then:
+ target.fooBar == 'baz'
+ }
+
+ void "an underscore separated property name is bound to the matching camelCase property"() {
+ given:
+ def target = new SimpleBean()
+ def binder = new RelaxedDataBinder(target)
+ def values = new MutablePropertyValues()
+ values.add('foo_bar', 'baz')
+
+ when:
+ binder.bind(values)
+
+ then:
+ target.fooBar == 'baz'
+ }
+
+ void "withAlias resolves an alternate incoming property name to the target property"() {
+ given:
+ def target = new SimpleBean()
+ def binder = new RelaxedDataBinder(target).withAlias('url', 'jdbcUrl')
+ def values = new MutablePropertyValues()
+ values.add('url', 'jdbc:h2:mem:test')
+
+ when:
+ binder.bind(values)
+
+ then:
+ target.jdbcUrl == 'jdbc:h2:mem:test'
+ }
+
+ void "binding to a Map target populates the map with the given keys"() {
+ given:
+ def map = [:]
+ def binder = new RelaxedDataBinder(map)
+ def values = new MutablePropertyValues()
+ values.add('key', 'value')
+
+ when:
+ binder.bind(values)
+
+ then:
+ map.key == 'value'
+ }
+
+ void "a null nested bean property is auto-instantiated and populated"() {
+ given:
+ def target = new SimpleBean()
+ def binder = new RelaxedDataBinder(target)
+ def values = new MutablePropertyValues()
+ values.add('nested.value', 'hello')
+
+ when:
+ binder.bind(values)
+
+ then:
+ target.nested != null
+ target.nested.value == 'hello'
+ }
+
+ void "period separated keys are bound into an existing map property"() {
+ given:
+ def target = new SimpleBean()
+ def binder = new RelaxedDataBinder(target)
+ def values = new MutablePropertyValues()
+ values.add('options.timeout', '30')
+ values.add('options.retries', '3')
+
+ when:
+ binder.bind(values)
+
+ then:
+ target.options.timeout == '30'
+ target.options.retries == '3'
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedNamesSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedNamesSpec.groovy
new file mode 100644
index 0000000000..19b4ccc156
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/RelaxedNamesSpec.groovy
@@ -0,0 +1,77 @@
+/*
+ * 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.jdbc
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class RelaxedNamesSpec extends Specification {
+
+ private static Set<String> namesFor(String source) {
+ new RelaxedNames(source).iterator().toList() as Set
+ }
+
+ void "dash separated names produce underscore, camelCase and case variations"() {
+ expect:
+ namesFor('foo-bar') == ['foo-bar', 'foo_bar', 'fooBar', 'foobar', 'FOO-BAR', 'FOO_BAR', 'FOOBAR'] as Set
+ }
+
+ void "camelCase names produce dash, underscore and lowercase/uppercase variations"() {
+ expect:
+ namesFor('fooBar') == ['fooBar', 'foo_bar', 'foo-bar', 'foobar', 'FOOBAR', 'FOO_BAR', 'FOO-BAR'] as Set
+ }
+
+ void "a single lowercase word only produces case variations"() {
+ expect:
+ namesFor('foo') == ['foo', 'FOO'] as Set
+ }
+
+ void "null name is treated as an empty string"() {
+ expect:
+ namesFor(null) == [''] as Set
+ }
+
+ void "empty name only produces itself"() {
+ expect:
+ namesFor('') == [''] as Set
+ }
+
+ @Unroll
+ void "'#source' variations include '#expected'"() {
+ expect:
+ namesFor(source).contains(expected)
+
+ where:
+ source | expected
+ 'db-create' | 'dbCreate'
+ 'db-create' | 'db_create'
+ 'db-create' | 'DB-CREATE'
+ 'dbCreate' | 'db-create'
+ 'dbCreate' | 'db_create'
+ 'DB_CREATE' | 'dbCreate'
+ }
+
+ void "iterating twice returns the same values"() {
+ given:
+ def names = new RelaxedNames('foo-bar')
+
+ expect:
+ names.iterator().toList() == names.iterator().toList()
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/CachedDataSourceConnectionSourceFactorySpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/CachedDataSourceConnectionSourceFactorySpec.groovy
new file mode 100644
index 0000000000..363ecf58f8
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/CachedDataSourceConnectionSourceFactorySpec.groovy
@@ -0,0 +1,58 @@
+/*
+ * 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.jdbc.connections
+
+import spock.lang.Specification
+
+class CachedDataSourceConnectionSourceFactorySpec extends Specification {
+
+ void "create(name, settings) returns the same cached instance for the same name"() {
+ given:
+ def factory = new CachedDataSourceConnectionSourceFactory()
+ def settings = new DataSourceSettings(url: 'jdbc:h2:mem:cachedFactoryTest1;DB_CLOSE_DELAY=-1')
+
+ when:
+ def first = factory.create('default', settings)
+ def second = factory.create('default', settings)
+
+ then:
+ first.is(second)
+
+ cleanup:
+ first?.close()
+ }
+
+ void "create(name, settings) returns a different instance for a different name"() {
+ given:
+ def factory = new CachedDataSourceConnectionSourceFactory()
+ def settingsA = new DataSourceSettings(url: 'jdbc:h2:mem:cachedFactoryTest2a;DB_CLOSE_DELAY=-1')
+ def settingsB = new DataSourceSettings(url: 'jdbc:h2:mem:cachedFactoryTest2b;DB_CLOSE_DELAY=-1')
+
+ when:
+ def a = factory.create('sourceA', settingsA)
+ def b = factory.create('sourceB', settingsB)
+
+ then:
+ !a.is(b)
+
+ cleanup:
+ a?.close()
+ b?.close()
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceFactorySpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceFactorySpec.groovy
new file mode 100644
index 0000000000..0a9730c393
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceFactorySpec.groovy
@@ -0,0 +1,115 @@
+/*
+ * 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.jdbc.connections
+
+import org.grails.datastore.mapping.core.DatastoreUtils
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy
+import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
+import spock.lang.Specification
+
+class DataSourceConnectionSourceFactorySpec extends Specification {
+
+ void "create resolves settings for the default data source from the 'dataSource' prefix"() {
+ given:
+ def factory = new DataSourceConnectionSourceFactory()
+ def config = DatastoreUtils.createPropertyResolver([
+ 'dataSource.url' : 'jdbc:h2:mem:factoryDefaultTest;DB_CLOSE_DELAY=-1',
+ 'dataSource.lazy' : 'false',
+ 'dataSource.transactionAware': 'false'
+ ])
+
+ when:
+ def connectionSource = factory.create(ConnectionSource.DEFAULT, config)
+
+ then:
+ connectionSource.name == ConnectionSource.DEFAULT
+ connectionSource.settings.url == 'jdbc:h2:mem:factoryDefaultTest;DB_CLOSE_DELAY=-1'
+ connectionSource.source != null
+
+ cleanup:
+ connectionSource?.close()
+ }
+
+ void "create resolves settings for a named data source from the 'dataSources.<name>' prefix"() {
+ given:
+ def factory = new DataSourceConnectionSourceFactory()
+ def config = DatastoreUtils.createPropertyResolver([
+ 'dataSources.secondary.url' : 'jdbc:h2:mem:factoryNamedTest;DB_CLOSE_DELAY=-1',
+ 'dataSources.secondary.lazy' : 'false',
+ 'dataSources.secondary.transactionAware': 'false'
+ ])
+
+ when:
+ def connectionSource = factory.create('secondary', config)
+
+ then:
+ connectionSource.name == 'secondary'
+ connectionSource.settings.url == 'jdbc:h2:mem:factoryNamedTest;DB_CLOSE_DELAY=-1'
+
+ cleanup:
+ connectionSource?.close()
+ }
+
+ void "create(name, settings) wraps the built DataSource with lazy and transaction-aware proxies when configured"() {
+ given:
+ def factory = new DataSourceConnectionSourceFactory()
+ def settings = new DataSourceSettings(
+ url: 'jdbc:h2:mem:factoryProxyTest;DB_CLOSE_DELAY=-1',
+ lazy: true,
+ transactionAware: true)
+
+ when:
+ def connectionSource = factory.create('default', settings)
+
+ then:
+ connectionSource.source instanceof TransactionAwareDataSourceProxy
+ ((TransactionAwareDataSourceProxy) connectionSource.source).targetDataSource instanceof LazyConnectionDataSourceProxy
+
+ cleanup:
+ connectionSource?.close()
+ }
+
+ void "create(name, settings) does not wrap the DataSource when lazy and transactionAware are disabled"() {
+ given:
+ def factory = new DataSourceConnectionSourceFactory()
+ def settings = new DataSourceSettings(
+ url: 'jdbc:h2:mem:factoryNoProxyTest;DB_CLOSE_DELAY=-1',
+ lazy: false,
+ transactionAware: false)
+
+ when:
+ def connectionSource = factory.create('default', settings)
+
+ then:
+ !(connectionSource.source instanceof TransactionAwareDataSourceProxy)
+ !(connectionSource.source instanceof LazyConnectionDataSourceProxy)
+
+ cleanup:
+ connectionSource?.close()
+ }
+
+ void "getConnectionSourcesConfigurationKey returns the dataSources settings key"() {
+ given:
+ def factory = new DataSourceConnectionSourceFactory()
+
+ expect:
+ factory.connectionSourcesConfigurationKey == 'dataSources'
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceSpec.groovy
new file mode 100644
index 0000000000..9b1c127ef3
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceConnectionSourceSpec.groovy
@@ -0,0 +1,96 @@
+/*
+ * 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.jdbc.connections
+
+import javax.sql.DataSource
+
+import org.springframework.jdbc.datasource.DelegatingDataSource
+import spock.lang.Specification
+
+class DataSourceConnectionSourceSpec extends Specification {
+
+ static interface CloseableDataSource extends DataSource {
+ void close()
+ }
+
+ void "close invokes a public close() method on the underlying DataSource when present"() {
+ given:
+ def dataSource = Mock(CloseableDataSource)
+ def source = new DataSourceConnectionSource('test', dataSource, new DataSourceSettings())
+
+ when:
+ source.close()
+
+ then:
+ 1 * dataSource.close()
+ }
+
+ void "close unwraps a DelegatingDataSource to find a close() method on the target"() {
+ given:
+ def closeable = Mock(CloseableDataSource)
+ def delegating = new DelegatingDataSource(closeable)
+ def source = new DataSourceConnectionSource('test', delegating, new DataSourceSettings())
+
+ when:
+ source.close()
+
+ then:
+ 1 * closeable.close()
+ }
+
+ void "close does not fail when the DataSource has no close() method"() {
+ given:
+ def dataSource = Mock(DataSource)
+ def source = new DataSourceConnectionSource('test', dataSource, new DataSourceSettings())
+
+ when:
+ source.close()
+
+ then:
+ noExceptionThrown()
+ }
+
+ void "close swallows exceptions thrown by the underlying close() method"() {
+ given:
+ def dataSource = Mock(CloseableDataSource) {
+ close() >> { throw new RuntimeException('boom') }
+ }
+ def source = new DataSourceConnectionSource('test', dataSource, new DataSourceSettings())
+
+ when:
+ source.close()
+
+ then:
+ noExceptionThrown()
+ }
+
+ void "getName, getSource and getSettings expose the constructor arguments"() {
+ given:
+ def dataSource = Mock(DataSource)
+ def settings = new DataSourceSettings()
+
+ when:
+ def source = new DataSourceConnectionSource('test', dataSource, settings)
+
+ then:
+ source.name == 'test'
+ source.source.is(dataSource)
+ source.settings.is(settings)
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsBuilderSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsBuilderSpec.groovy
new file mode 100644
index 0000000000..24a35ba707
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsBuilderSpec.groovy
@@ -0,0 +1,75 @@
+/*
+ * 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.jdbc.connections
+
+import org.grails.datastore.mapping.core.DatastoreUtils
+import spock.lang.Specification
+
+class DataSourceSettingsBuilderSpec extends Specification {
+
+ void "build resolves settings from the default 'dataSource' configuration prefix"() {
+ given:
+ def config = DatastoreUtils.createPropertyResolver([
+ 'dataSource.url' : 'jdbc:h2:mem:builderDefaultTest',
+ 'dataSource.username': 'sa',
+ 'dataSource.password': 'secret',
+ 'dataSource.pooled' : 'false'
+ ])
+
+ when:
+ def settings = new DataSourceSettingsBuilder(config).build()
+
+ then:
+ settings.url == 'jdbc:h2:mem:builderDefaultTest'
+ settings.username == 'sa'
+ settings.password == 'secret'
+ !settings.pooled
+ }
+
+ void "build resolves settings from a custom configuration prefix"() {
+ given:
+ def config = DatastoreUtils.createPropertyResolver([
+ 'dataSources.secondary.url' : 'jdbc:h2:mem:builderCustomPrefixTest',
+ 'dataSources.secondary.username' : 'other'
+ ])
+
+ when:
+ def settings = new DataSourceSettingsBuilder(config, 'dataSources.secondary').build()
+
+ then:
+ settings.url == 'jdbc:h2:mem:builderCustomPrefixTest'
+ settings.username == 'other'
+ }
+
+ void "properties not present in configuration keep their default values"() {
+ given:
+ def config = DatastoreUtils.createPropertyResolver([
+ 'dataSource.url': 'jdbc:h2:mem:builderDefaultsTest'
+ ])
+
+ when:
+ def settings = new DataSourceSettingsBuilder(config).build()
+
+ then:
+ settings.pooled
+ settings.lazy
+ settings.transactionAware
+ settings.dbCreate == 'none'
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsSpec.groovy
new file mode 100644
index 0000000000..c6a7e04032
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/DataSourceSettingsSpec.groovy
@@ -0,0 +1,123 @@
+/*
+ * 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.jdbc.connections
+
+import org.grails.datastore.gorm.jdbc.schema.DefaultSchemaHandler
+import spock.lang.Specification
+
+class DataSourceSettingsSpec extends Specification {
+
+ void "default settings use an in-memory H2 database and the default schema handler"() {
+ given:
+ def settings = new DataSourceSettings()
+
+ expect:
+ settings.url == 'jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000'
+ settings.pooled
+ settings.lazy
+ settings.transactionAware
+ !settings.readOnly
+ !settings.logSql
+ !settings.formatSql
+ settings.dbCreate == 'none'
+ settings.schemaHandler == DefaultSchemaHandler
+ settings.properties == [:]
+ }
+
+ void "toHibernateProperties maps the dbCreate, logSql and formatSql settings"() {
+ given:
+ def settings = new DataSourceSettings(dbCreate: 'update', logSql: true, formatSql: true)
+
+ when:
+ def props = settings.toHibernateProperties()
+
+ then:
+ props.getProperty('hibernate.hbm2ddl.auto') == 'update'
+ props.getProperty('hibernate.show_sql') == 'true'
+ props.getProperty('hibernate.format_sql') == 'true'
+ !props.containsKey('hibernate.dialect')
+ }
+
+ void "toHibernateProperties includes the dialect name when a dialect class is configured"() {
+ given:
+ def settings = new DataSourceSettings(dialect: String)
+
+ when:
+ def props = settings.toHibernateProperties()
+
+ then:
+ props.getProperty('hibernate.dialect') == String.name
+ }
+
+ void "toProperties includes url, driverClassName, username and password when set"() {
+ given:
+ def settings = new DataSourceSettings(
+ url: 'jdbc:h2:mem:test',
+ driverClassName: 'org.h2.Driver',
+ username: 'sa',
+ password: 'secret')
+
+ when:
+ def props = settings.toProperties()
+
+ then:
+ props.url == 'jdbc:h2:mem:test'
+ props.driverClassName == 'org.h2.Driver'
+ props.username == 'sa'
+ props.password == 'secret'
+ !props.containsKey('defaultReadOnly')
+ }
+
+ void "toProperties omits driverClassName, username and password when not set"() {
+ given:
+ def settings = new DataSourceSettings(url: 'jdbc:h2:mem:test')
+
+ when:
+ def props = settings.toProperties()
+
+ then:
+ props.url == 'jdbc:h2:mem:test'
+ !props.containsKey('driverClassName')
+ !props.containsKey('username')
+ !props.containsKey('password')
+ }
+
+ void "toProperties includes defaultReadOnly when readOnly is true"() {
+ given:
+ def settings = new DataSourceSettings(url: 'jdbc:h2:mem:test', readOnly: true)
+
+ when:
+ def props = settings.toProperties()
+
+ then:
+ props.defaultReadOnly == 'true'
+ }
+
+ void "toProperties merges in the configured additional properties"() {
+ given:
+ def settings = new DataSourceSettings(url: 'jdbc:h2:mem:test', properties: [maximumPoolSize: '10'])
+
+ when:
+ def props = settings.toProperties()
+
+ then:
+ props.maximumPoolSize == '10'
+ props.url == 'jdbc:h2:mem:test'
+ }
+}
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/SpringDataSourceConnectionSourceFactorySpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/SpringDataSourceConnectionSourceFactorySpec.groovy
new file mode 100644
index 0000000000..93c44e953f
--- /dev/null
+++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/jdbc/connections/SpringDataSourceConnectionSourceFactorySpec.groovy
@@ -0,0 +1,82 @@
+/*
+ * 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.jdbc.connections
+
+import javax.sql.DataSource
+
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.springframework.context.support.StaticApplicationContext
+import spock.lang.Specification
+
+class SpringDataSourceConnectionSourceFactorySpec extends Specification {
+
+ void "create returns the Spring-managed 'dataSource' bean for the default connection source name"() {
+ given:
+ def springDataSource = Mock(DataSource)
+ def context = new StaticApplicationContext()
+ context.beanFactory.registerSingleton('dataSource', springDataSource)
+ context.refresh()
+
+ def factory = new SpringDataSourceConnectionSourceFactory()
+ factory.applicationContext = context
+
+ when:
+ def connectionSource = factory.create(ConnectionSource.DEFAULT, new DataSourceSettings())
+
+ then:
+ connectionSource.source.is(springDataSource)
+ }
+
+ void "create returns the Spring-managed 'dataSource_<name>' bean for a named connection source"() {
+ given:
+ def springDataSource = Mock(DataSource)
+ def context = new StaticApplicationContext()
+ context.beanFactory.registerSingleton('dataSource_secondary', springDataSource)
+ context.refresh()
+
+ def factory = new SpringDataSourceConnectionSourceFactory()
+ factory.applicationContext = context
+
+ when:
+ def connectionSource = factory.create('secondary', new DataSourceSettings())
+
+ then:
+ connectionSource.source.is(springDataSource)
+ }
+
+ void "create falls back to building its own DataSource when no matching Spring bean exists"() {
+ given:
+ def context = new StaticApplicationContext()
+ context.refresh()
+
+ def factory = new SpringDataSourceConnectionSourceFactory()
+ factory.applicationContext = context
+ def settings = new DataSourceSettings(url: 'jdbc:h2:mem:springFallbackTest;DB_CLOSE_DELAY=-1')
+
+ when:
+ def connectionSource = factory.create(ConnectionSource.DEFAULT, settings)
+
+ then:
+ connectionSource.source instanceof DataSource
+ connectionSource instanceof DataSourceConnectionSource
+
+ cleanup:
+ connectionSource?.close()
+ }
+}