diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLChecker.kt b/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLChecker.kt index c329b2ec4d0d..197f22b044a5 100644 --- a/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLChecker.kt +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLChecker.kt @@ -8,7 +8,8 @@ import java.util.UUID import javax.sql.DataSource @Singleton -class MSSQLChecker(private val dataSourceFactory: MSSQLDataSourceFactory) : DestinationChecker { +class MSSQLChecker(private val dataSourceFactory: MSSQLDataSourceFactory) : + DestinationChecker { override fun check(config: MSSQLConfiguration) { val dataSource: DataSource = dataSourceFactory.getDataSource(config) val testTableName = "check_test_${UUID.randomUUID()}" diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/config/DataSourceFactory.kt b/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/config/DataSourceFactory.kt index 5dc36296219f..9fac89b81870 100644 --- a/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/config/DataSourceFactory.kt +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/config/DataSourceFactory.kt @@ -30,26 +30,31 @@ class DataSourceFactory { } fun MSSQLConfiguration.toSQLServerDataSource(): SQLServerDataSource { - val connectionString = StringBuilder().apply { - append("jdbc:sqlserver://${host}:${port};databaseName=${database}") + val connectionString = + StringBuilder() + .apply { + append("jdbc:sqlserver://${host}:${port};databaseName=${database}") - when (sslMethod) { - is EncryptedVerify -> { - append(";encrypt=true") - sslMethod.trustStoreName?.let { append(";trustStoreName=$it") } - sslMethod.trustStorePassword?.let { append(";trustStorePassword=$it") } - sslMethod.hostNameInCertificate?.let { append(";hostNameInCertificate=$it") } - } - is EncryptedTrust -> { - append(";encrypt=true;trustServerCertificate=true") - } - is Unencrypted -> { - append(";encrypt=false") - } - } + when (sslMethod) { + is EncryptedVerify -> { + append(";encrypt=true") + sslMethod.trustStoreName?.let { append(";trustStoreName=$it") } + sslMethod.trustStorePassword?.let { append(";trustStorePassword=$it") } + sslMethod.hostNameInCertificate?.let { + append(";hostNameInCertificate=$it") + } + } + is EncryptedTrust -> { + append(";encrypt=true;trustServerCertificate=true") + } + is Unencrypted -> { + append(";encrypt=false") + } + } - jdbcUrlParams?.let { append(";$it") } - }.toString() + jdbcUrlParams?.let { append(";$it") } + } + .toString() return SQLServerDataSource().also { it.url = connectionString diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/config/MSSQLSpecification.kt b/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/config/MSSQLSpecification.kt index 39f5c007f0e8..80860683a665 100644 --- a/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/config/MSSQLSpecification.kt +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/main/kotlin/io/airbyte/integrations/destination/mssql/v2/config/MSSQLSpecification.kt @@ -112,31 +112,29 @@ class EncryptedTrust : EncryptionMethod { @JsonSchemaTitle("Encrypted (verify certificate)") @JsonSchemaDescription("Verify and use the certificate provided by the server.") -class EncryptedVerify : EncryptionMethod { - companion object { - const val NAME = "encrypted_verify_certificate" - } - override val name: String = NAME - +class EncryptedVerify( @get:JsonSchemaTitle("Trust Store Name") @get:JsonPropertyDescription("Specifies the name of the trust store.") @get:JsonProperty("trustStoreName") @get:JsonSchemaInject(json = """{"order":1}""") - val trustStoreName: String? = null - + val trustStoreName: String? = null, @get:JsonSchemaTitle("Trust Store Password") @get:JsonPropertyDescription("Specifies the password of the trust store.") @get:JsonProperty("trustStorePassword") @get:JsonSchemaInject(json = """{"airbyte_secret":true,"order":2}""") - val trustStorePassword: String? = null - + val trustStorePassword: String? = null, @get:JsonSchemaTitle("Host Name In Certificate") @get:JsonPropertyDescription( "Specifies the host name of the server. The value of this property must match the subject property of the certificate." ) @get:JsonProperty("hostNameInCertificate") @get:JsonSchemaInject(json = """{"order":3}""") - val hostNameInCertificate: String? = null + val hostNameInCertificate: String? = null, +) : EncryptionMethod { + companion object { + const val NAME = "encrypted_verify_certificate" + } + override val name: String = NAME } @Singleton diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLCheckTest.kt b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLCheckTest.kt index ae0f78d18596..394b1f51c58e 100644 --- a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLCheckTest.kt +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLCheckTest.kt @@ -3,11 +3,19 @@ package io.airbyte.integrations.destination.mssql.v2 import io.airbyte.cdk.load.check.CheckIntegrationTest import io.airbyte.cdk.load.check.CheckTestConfig import io.airbyte.integrations.destination.mssql.v2.config.MSSQLSpecification +import java.util.regex.Pattern -class MSSQLCheckTest : CheckIntegrationTest( - successConfigFilenames = listOf( - CheckTestConfig(MSSQLTestConfigUtil.getConfigPath("check/valid.json")), - ), - failConfigFilenamesAndFailureReasons = emptyMap() -) { -} +class MSSQLCheckTest : + CheckIntegrationTest( + successConfigFilenames = + listOf( + CheckTestConfig(MSSQLTestConfigUtil.getConfigPath("check/valid.json")), + CheckTestConfig(MSSQLTestConfigUtil.getConfigPath("check/valid-ssl-trust.json")), + ), + failConfigFilenamesAndFailureReasons = + mapOf( + CheckTestConfig( + MSSQLTestConfigUtil.getConfigPath("check/fail-internal-schema-invalid.json") + ) to Pattern.compile("\"iamnotthere\" either does not exist"), + ) + ) {} diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLSpecTest.kt b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLSpecTest.kt new file mode 100644 index 000000000000..e9d136eac35e --- /dev/null +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLSpecTest.kt @@ -0,0 +1,5 @@ +package io.airbyte.integrations.destination.mssql.v2 + +import io.airbyte.cdk.load.spec.SpecTest + +class MSSQLSpecTest : SpecTest() {} diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLTestConfigUtil.kt b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLTestConfigUtil.kt index ae0b81b0fa3d..2823bfe544e0 100644 --- a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLTestConfigUtil.kt +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/kotlin/io/airbyte/integrations/destination/mssql/v2/MSSQLTestConfigUtil.kt @@ -4,5 +4,8 @@ import java.nio.file.Path object MSSQLTestConfigUtil { fun getConfigPath(relativePath: String): Path = - Path.of(this::class.java.classLoader.getResource(relativePath)?.toURI() ?: throw IllegalArgumentException("Resource $relativePath could not be found")) + Path.of( + this::class.java.classLoader.getResource(relativePath)?.toURI() + ?: throw IllegalArgumentException("Resource $relativePath could not be found") + ) } diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/check/fail-internal-schema-invalid.json b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/check/fail-internal-schema-invalid.json new file mode 100644 index 000000000000..d985294a6e4b --- /dev/null +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/check/fail-internal-schema-invalid.json @@ -0,0 +1,10 @@ +{ + "host": "localhost", + "port": 1433, + "database": "master", + "schema": "dbo", + "raw_data_schema": "iamnotthere", + "ssl_method": {"name": "unencrypted"}, + "user": "sa", + "password": "Averycomplicatedpassword1!" +} diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/check/valid-ssl-trust.json b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/check/valid-ssl-trust.json new file mode 100644 index 000000000000..4e4d25cfcad9 --- /dev/null +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/check/valid-ssl-trust.json @@ -0,0 +1,10 @@ +{ + "host": "localhost", + "port": 1433, + "database": "master", + "schema": "dbo", + "raw_data_schema": "guest", + "ssl_method": {"name": "encrypted_trust_server_certificate"}, + "user": "sa", + "password": "Averycomplicatedpassword1!" +} diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/expected-spec-cloud.json b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/expected-spec-cloud.json new file mode 100644 index 000000000000..5cee2a681685 --- /dev/null +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/expected-spec-cloud.json @@ -0,0 +1,136 @@ +{ + "documentationUrl" : "https://docs.airbyte.com/integrations/destinations/mssql-v2", + "connectionSpecification" : { + "$schema" : "http://json-schema.org/draft-07/schema#", + "title" : "MSSQL V2 Destination Specification", + "type" : "object", + "additionalProperties" : true, + "properties" : { + "host" : { + "type" : "string", + "description" : "The host name of the MSSQL database.", + "title" : "Host", + "order" : 0 + }, + "port" : { + "type" : "integer", + "description" : "The port of the MSSQL database.", + "title" : "Port", + "minimum" : 0, + "maximum" : 65536, + "examples" : [ "1433" ], + "order" : 1 + }, + "database" : { + "type" : "string", + "description" : "The name of the MSSQL database.", + "title" : "DB Name", + "order" : 2 + }, + "schema" : { + "type" : "string", + "description" : "The default schema tables are written to if the source does not specify a namespace. The usual value for this field is \"public\".", + "title" : "Default Schema", + "examples" : [ "public" ], + "default" : "public", + "order" : 3 + }, + "user" : { + "type" : "string", + "description" : "The username which is used to access the database.", + "title" : "User", + "order" : 4 + }, + "password" : { + "type" : "string", + "description" : "The password associated with this username.", + "title" : "Password", + "airbyte_secret" : true, + "order" : 5 + }, + "jdbc_url_params" : { + "type" : "string", + "description" : "Additional properties to pass to the JDBC URL string when connecting to the database formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3).", + "title" : "JDBC URL Params", + "order" : 6 + }, + "raw_data_schema" : { + "type" : "string", + "description" : "The schema to write raw tables into (default: airbyte_internal)", + "title" : "Raw Table Schema Name", + "default" : "airbyte_internal", + "order" : 5 + }, + "ssl_method" : { + "oneOf" : [ { + "title" : "Unencrypted", + "type" : "object", + "additionalProperties" : true, + "description" : "The data transfer will not be encrypted.", + "properties" : { + "name" : { + "type" : "string", + "enum" : [ "unencrypted" ], + "default" : "unencrypted" + } + }, + "required" : [ "name" ] + }, { + "title" : "Encrypted (trust server certificate)", + "type" : "object", + "additionalProperties" : true, + "description" : "Use the certificate provided by the server without verification. (For testing purposes only!)", + "properties" : { + "name" : { + "type" : "string", + "enum" : [ "encrypted_trust_server_certificate" ], + "default" : "encrypted_trust_server_certificate" + } + }, + "required" : [ "name" ] + }, { + "title" : "Encrypted (verify certificate)", + "type" : "object", + "additionalProperties" : true, + "description" : "Verify and use the certificate provided by the server.", + "properties" : { + "name" : { + "type" : "string", + "enum" : [ "encrypted_verify_certificate" ], + "default" : "encrypted_verify_certificate" + }, + "trustStoreName" : { + "type" : "string", + "description" : "Specifies the name of the trust store.", + "title" : "Trust Store Name", + "order" : 1 + }, + "trustStorePassword" : { + "type" : "string", + "description" : "Specifies the password of the trust store.", + "title" : "Trust Store Password", + "airbyte_secret" : true, + "order" : 2 + }, + "hostNameInCertificate" : { + "type" : "string", + "description" : "Specifies the host name of the server. The value of this property must match the subject property of the certificate.", + "title" : "Host Name In Certificate", + "order" : 3 + } + }, + "required" : [ "name" ] + } ], + "description" : "The encryption method which is used to communicate with the database.", + "title" : "SSL Method", + "order" : 8, + "type" : "object" + } + }, + "required" : [ "host", "port", "database", "schema", "raw_data_schema", "ssl_method" ] + }, + "supportsIncremental" : true, + "supportsNormalization" : false, + "supportsDBT" : false, + "supported_destination_sync_modes" : [ "overwrite", "append", "append_dedup" ] +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/expected-spec-oss.json b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/expected-spec-oss.json new file mode 100644 index 000000000000..5cee2a681685 --- /dev/null +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/test-integration/resources/expected-spec-oss.json @@ -0,0 +1,136 @@ +{ + "documentationUrl" : "https://docs.airbyte.com/integrations/destinations/mssql-v2", + "connectionSpecification" : { + "$schema" : "http://json-schema.org/draft-07/schema#", + "title" : "MSSQL V2 Destination Specification", + "type" : "object", + "additionalProperties" : true, + "properties" : { + "host" : { + "type" : "string", + "description" : "The host name of the MSSQL database.", + "title" : "Host", + "order" : 0 + }, + "port" : { + "type" : "integer", + "description" : "The port of the MSSQL database.", + "title" : "Port", + "minimum" : 0, + "maximum" : 65536, + "examples" : [ "1433" ], + "order" : 1 + }, + "database" : { + "type" : "string", + "description" : "The name of the MSSQL database.", + "title" : "DB Name", + "order" : 2 + }, + "schema" : { + "type" : "string", + "description" : "The default schema tables are written to if the source does not specify a namespace. The usual value for this field is \"public\".", + "title" : "Default Schema", + "examples" : [ "public" ], + "default" : "public", + "order" : 3 + }, + "user" : { + "type" : "string", + "description" : "The username which is used to access the database.", + "title" : "User", + "order" : 4 + }, + "password" : { + "type" : "string", + "description" : "The password associated with this username.", + "title" : "Password", + "airbyte_secret" : true, + "order" : 5 + }, + "jdbc_url_params" : { + "type" : "string", + "description" : "Additional properties to pass to the JDBC URL string when connecting to the database formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3).", + "title" : "JDBC URL Params", + "order" : 6 + }, + "raw_data_schema" : { + "type" : "string", + "description" : "The schema to write raw tables into (default: airbyte_internal)", + "title" : "Raw Table Schema Name", + "default" : "airbyte_internal", + "order" : 5 + }, + "ssl_method" : { + "oneOf" : [ { + "title" : "Unencrypted", + "type" : "object", + "additionalProperties" : true, + "description" : "The data transfer will not be encrypted.", + "properties" : { + "name" : { + "type" : "string", + "enum" : [ "unencrypted" ], + "default" : "unencrypted" + } + }, + "required" : [ "name" ] + }, { + "title" : "Encrypted (trust server certificate)", + "type" : "object", + "additionalProperties" : true, + "description" : "Use the certificate provided by the server without verification. (For testing purposes only!)", + "properties" : { + "name" : { + "type" : "string", + "enum" : [ "encrypted_trust_server_certificate" ], + "default" : "encrypted_trust_server_certificate" + } + }, + "required" : [ "name" ] + }, { + "title" : "Encrypted (verify certificate)", + "type" : "object", + "additionalProperties" : true, + "description" : "Verify and use the certificate provided by the server.", + "properties" : { + "name" : { + "type" : "string", + "enum" : [ "encrypted_verify_certificate" ], + "default" : "encrypted_verify_certificate" + }, + "trustStoreName" : { + "type" : "string", + "description" : "Specifies the name of the trust store.", + "title" : "Trust Store Name", + "order" : 1 + }, + "trustStorePassword" : { + "type" : "string", + "description" : "Specifies the password of the trust store.", + "title" : "Trust Store Password", + "airbyte_secret" : true, + "order" : 2 + }, + "hostNameInCertificate" : { + "type" : "string", + "description" : "Specifies the host name of the server. The value of this property must match the subject property of the certificate.", + "title" : "Host Name In Certificate", + "order" : 3 + } + }, + "required" : [ "name" ] + } ], + "description" : "The encryption method which is used to communicate with the database.", + "title" : "SSL Method", + "order" : 8, + "type" : "object" + } + }, + "required" : [ "host", "port", "database", "schema", "raw_data_schema", "ssl_method" ] + }, + "supportsIncremental" : true, + "supportsNormalization" : false, + "supportsDBT" : false, + "supported_destination_sync_modes" : [ "overwrite", "append", "append_dedup" ] +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/destination-mssql-v2/src/test/kotlin/io/airbyte/integrations/destination/mssql/v2/config/DataSourceFactoryTest.kt b/airbyte-integrations/connectors/destination-mssql-v2/src/test/kotlin/io/airbyte/integrations/destination/mssql/v2/config/DataSourceFactoryTest.kt index 2c15f3148c1a..6c52f58a59a0 100644 --- a/airbyte-integrations/connectors/destination-mssql-v2/src/test/kotlin/io/airbyte/integrations/destination/mssql/v2/config/DataSourceFactoryTest.kt +++ b/airbyte-integrations/connectors/destination-mssql-v2/src/test/kotlin/io/airbyte/integrations/destination/mssql/v2/config/DataSourceFactoryTest.kt @@ -4,19 +4,105 @@ package io.airbyte.integrations.destination.mssql.v2.config +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue import org.junit.jupiter.api.Test internal class DataSourceFactoryTest { + // @Test + // fun test() { + // val factory = DataSourceFactory() + // val dataSource = factory.dataSource() + // + // dataSource.connection.use { connection -> + // connection.createStatement().use { statement -> + // statement.execute("SELECT * FROM Inventory") + // } + // } + // } + + @Test + fun `test data source base url conversion`() { + val config = + Fixtures.defaultConfig.copy( + host = "myhost", + port = 1234, + database = "db", + ) + val dataSource = config.toSQLServerDataSource() + assertTrue { dataSource.url.startsWith("jdbc:sqlserver://myhost:1234;databaseName=db;") } + } + + @Test + fun `test data source handles optional passwords conversion`() { + val config = + Fixtures.defaultConfig.copy( + user = "airbyte-test", + password = null, + ) + val dataSource = config.toSQLServerDataSource() + assertEquals("airbyte-test", dataSource.user) + } + + @Test + fun `test jdbc params passthrough`() { + val config = Fixtures.defaultConfig.copy(jdbcUrlParams = "custom=params") + val dataSource = config.toSQLServerDataSource() + assertTrue { dataSource.url.endsWith(";custom=params") } + } + @Test - fun test() { - val factory = DataSourceFactory() - val dataSource = factory.dataSource() - - dataSource.connection.use { connection -> - connection.createStatement().use { statement -> - statement.execute("SELECT * FROM Inventory") - } + fun `test unencrypted config`() { + val config = Fixtures.defaultConfig.copy(sslMethod = Unencrypted()) + val dataSource = config.toSQLServerDataSource() + assertTrue { dataSource.url.contains(";encrypt=false") } + assertFalse { dataSource.url.contains(";encrypt=true") } + } + + @Test + fun `test encrypted trust config`() { + val config = Fixtures.defaultConfig.copy(sslMethod = EncryptedTrust()) + val dataSource = config.toSQLServerDataSource() + assertTrue { dataSource.url.contains(";encrypt=true;trustServerCertificate=true") } + assertFalse { dataSource.url.contains(";encrypt=false") } + } + + @Test + fun `test encrypted verify config`() { + val sslMethod = + EncryptedVerify( + trustStoreName = "name", + trustStorePassword = "password", + hostNameInCertificate = "cert-host" + ) + val config = Fixtures.defaultConfig.copy(sslMethod = sslMethod) + val dataSource = config.toSQLServerDataSource() + assertTrue { dataSource.url.contains(";encrypt=true") } + assertTrue { dataSource.url.contains(";trustStoreName=${sslMethod.trustStoreName}") } + assertTrue { + dataSource.url.contains(";trustStorePassword=${sslMethod.trustStorePassword}") + } + assertTrue { + dataSource.url.contains(";hostNameInCertificate=${sslMethod.hostNameInCertificate}") } + assertFalse { dataSource.url.contains(";encrypt=false") } + assertFalse { dataSource.url.contains(";trustServerCertificate=true") } + } + + object Fixtures { + val defaultConfig = + MSSQLConfiguration( + host = "localhost", + port = 1433, + database = "master", + schema = "dbo", + user = "airbyte", + password = "super secure o//", + jdbcUrlParams = null, + rawDataSchema = "airbyte_internal", + sslMethod = Unencrypted(), + ) } }