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