aglinxinyuan commented on code in PR #5698: URL: https://github.com/apache/texera/pull/5698#discussion_r3409070150
########## common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala: ########## @@ -0,0 +1,252 @@ +/* + * 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 + * + * http://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.apache.texera.amber.operator.source.sql.postgresql + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.BeforeAndAfterAll + +import java.lang.reflect.{InvocationHandler, Method, Proxy} +import java.sql.{Connection, Driver, DriverManager, DriverPropertyInfo, SQLException} +import java.util.Properties +import java.util.logging.Logger +import scala.collection.mutable.ArrayBuffer +import scala.jdk.CollectionConverters._ + +class PostgreSQLConnUtilSpec extends AnyFlatSpec with BeforeAndAfterAll { + + // --------------------------------------------------------------------------- + // Strategy — pin the JDBC URL composition (the only application-logic in + // this util) without a real DB. + // + // The workflow-operator test classpath DOES include the real PostgreSQL + // driver (transitively), and that driver eats `jdbc:postgresql:` URLs + // before returning a generic "The connection attempt failed." exception. + // So we can't rely on `DriverManager.getConnection`'s default + // "No suitable driver" message. + // + // Instead, we deregister every driver claiming `jdbc:postgresql:`, + // register a capturing driver that records each URL it is asked to open + // (and returns a Proxy-backed Connection so the production code can call + // `setReadOnly`), run the assertions, then restore the real drivers + // in afterAll. + // --------------------------------------------------------------------------- + + private object CapturingPGDriver extends Driver { + val seenUrls: ArrayBuffer[String] = ArrayBuffer.empty + val seenProps: ArrayBuffer[Properties] = ArrayBuffer.empty + val readOnlyCalls: ArrayBuffer[Boolean] = ArrayBuffer.empty + + override def connect(url: String, info: Properties): Connection = { + if (!acceptsURL(url)) return null + seenUrls += url + seenProps += info + Proxy + .newProxyInstance( + getClass.getClassLoader, + Array(classOf[Connection]), + new InvocationHandler { + override def invoke(p: Any, m: Method, args: Array[AnyRef]): AnyRef = + m.getName match { + case "setReadOnly" => + readOnlyCalls += args(0).asInstanceOf[java.lang.Boolean].booleanValue() + null + // Object methods — required so `conn != null`, `conn.toString`, + // and identity HashMap-keying work without NPE on auto-unboxing. + case "equals" => java.lang.Boolean.valueOf(p eq args(0)) + case "hashCode" => java.lang.Integer.valueOf(System.identityHashCode(p)) + case "toString" => "CapturingPGDriver.StubConnection@" + System.identityHashCode(p) + case "isWrapperFor" => java.lang.Boolean.FALSE + case "close" => null + case _ => null + } + } + ) + .asInstanceOf[Connection] + } + override def acceptsURL(url: String): Boolean = + url != null && url.startsWith("jdbc:postgresql:") + override def getPropertyInfo(url: String, info: Properties): Array[DriverPropertyInfo] = + Array.empty + override def getMajorVersion: Int = 1 + override def getMinorVersion: Int = 0 + override def jdbcCompliant(): Boolean = false + override def getParentLogger: Logger = Logger.getLogger("test-pg-capturing") + } + + // Snapshot of real PG drivers temporarily deregistered in beforeAll. + // Restored in afterAll so other suites are not left with a broken + // JDBC driver registry. + private val savedRealDrivers: ArrayBuffer[Driver] = ArrayBuffer.empty + + /** `acceptsURL` is declared `throws SQLException`; treat any throw as + * "this driver doesn't claim our scheme" so a flaky third-party driver + * cannot abort the whole suite. + */ + private def safeAcceptsURL(d: Driver, url: String): Boolean = + try d.acceptsURL(url) + catch { case _: Throwable => false } + + override protected def beforeAll(): Unit = { + super.beforeAll() + // Remove every other driver that claims jdbc:postgresql: so our + // capturing driver is the only one DriverManager.getConnection sees. + // Wrapped in try/catch so that if any deregistration / registration + // step throws, we restore whatever we already deregistered before + // failing the suite — the alternative leaves the JVM's JDBC registry + // in an inconsistent state for the rest of the test run. + try { + val others = DriverManager.getDrivers.asScala.toList.filter { d => + d != CapturingPGDriver && safeAcceptsURL(d, "jdbc:postgresql://h/d") + } Review Comment: Fixed in 458389ed15 — PG probe URL now mirrors the production shape: `jdbc:postgresql://probe-host:5432/probe-db`. A permissive driver that returns `false` on a stripped probe but `true` on the real URL can no longer slip past the deregistration sweep. ########## common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala: ########## @@ -0,0 +1,252 @@ +/* + * 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 + * + * http://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.apache.texera.amber.operator.source.sql.postgresql + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.BeforeAndAfterAll + +import java.lang.reflect.{InvocationHandler, Method, Proxy} +import java.sql.{Connection, Driver, DriverManager, DriverPropertyInfo, SQLException} +import java.util.Properties +import java.util.logging.Logger +import scala.collection.mutable.ArrayBuffer +import scala.jdk.CollectionConverters._ + +class PostgreSQLConnUtilSpec extends AnyFlatSpec with BeforeAndAfterAll { + + // --------------------------------------------------------------------------- + // Strategy — pin the JDBC URL composition (the only application-logic in + // this util) without a real DB. + // + // The workflow-operator test classpath DOES include the real PostgreSQL + // driver (transitively), and that driver eats `jdbc:postgresql:` URLs + // before returning a generic "The connection attempt failed." exception. + // So we can't rely on `DriverManager.getConnection`'s default + // "No suitable driver" message. + // + // Instead, we deregister every driver claiming `jdbc:postgresql:`, + // register a capturing driver that records each URL it is asked to open + // (and returns a Proxy-backed Connection so the production code can call + // `setReadOnly`), run the assertions, then restore the real drivers + // in afterAll. + // --------------------------------------------------------------------------- + + private object CapturingPGDriver extends Driver { + val seenUrls: ArrayBuffer[String] = ArrayBuffer.empty + val seenProps: ArrayBuffer[Properties] = ArrayBuffer.empty + val readOnlyCalls: ArrayBuffer[Boolean] = ArrayBuffer.empty + + override def connect(url: String, info: Properties): Connection = { + if (!acceptsURL(url)) return null + seenUrls += url + seenProps += info + Proxy + .newProxyInstance( + getClass.getClassLoader, + Array(classOf[Connection]), + new InvocationHandler { + override def invoke(p: Any, m: Method, args: Array[AnyRef]): AnyRef = + m.getName match { + case "setReadOnly" => + readOnlyCalls += args(0).asInstanceOf[java.lang.Boolean].booleanValue() + null + // Object methods — required so `conn != null`, `conn.toString`, + // and identity HashMap-keying work without NPE on auto-unboxing. + case "equals" => java.lang.Boolean.valueOf(p eq args(0)) + case "hashCode" => java.lang.Integer.valueOf(System.identityHashCode(p)) + case "toString" => "CapturingPGDriver.StubConnection@" + System.identityHashCode(p) + case "isWrapperFor" => java.lang.Boolean.FALSE + case "close" => null + case _ => null + } + } + ) + .asInstanceOf[Connection] + } + override def acceptsURL(url: String): Boolean = + url != null && url.startsWith("jdbc:postgresql:") + override def getPropertyInfo(url: String, info: Properties): Array[DriverPropertyInfo] = + Array.empty + override def getMajorVersion: Int = 1 + override def getMinorVersion: Int = 0 + override def jdbcCompliant(): Boolean = false + override def getParentLogger: Logger = Logger.getLogger("test-pg-capturing") + } + + // Snapshot of real PG drivers temporarily deregistered in beforeAll. + // Restored in afterAll so other suites are not left with a broken + // JDBC driver registry. + private val savedRealDrivers: ArrayBuffer[Driver] = ArrayBuffer.empty + + /** `acceptsURL` is declared `throws SQLException`; treat any throw as + * "this driver doesn't claim our scheme" so a flaky third-party driver + * cannot abort the whole suite. + */ + private def safeAcceptsURL(d: Driver, url: String): Boolean = + try d.acceptsURL(url) + catch { case _: Throwable => false } + + override protected def beforeAll(): Unit = { + super.beforeAll() + // Remove every other driver that claims jdbc:postgresql: so our + // capturing driver is the only one DriverManager.getConnection sees. + // Wrapped in try/catch so that if any deregistration / registration + // step throws, we restore whatever we already deregistered before + // failing the suite — the alternative leaves the JVM's JDBC registry + // in an inconsistent state for the rest of the test run. + try { + val others = DriverManager.getDrivers.asScala.toList.filter { d => + d != CapturingPGDriver && safeAcceptsURL(d, "jdbc:postgresql://h/d") + } + others.foreach { d => + savedRealDrivers += d + DriverManager.deregisterDriver(d) + } + DriverManager.registerDriver(CapturingPGDriver) + } catch { + case t: Throwable => + // Best-effort restore before re-throwing. + savedRealDrivers.foreach { d => + try DriverManager.registerDriver(d) + catch { case _: Throwable => () } + } + throw t + } + } + + override protected def afterAll(): Unit = { + try { + try DriverManager.deregisterDriver(CapturingPGDriver) + catch { case _: Throwable => () } + savedRealDrivers.foreach { d => + try DriverManager.registerDriver(d) + catch { case _: Throwable => () } + } + } finally { + super.afterAll() + } + } + + private def clearCapture(): Unit = { + CapturingPGDriver.seenUrls.clear() + CapturingPGDriver.seenProps.clear() + CapturingPGDriver.readOnlyCalls.clear() + } + + // --------------------------------------------------------------------------- + // URL composition — pin the exact JDBC URL the driver receives + // --------------------------------------------------------------------------- + + "PostgreSQLConnUtil.connect" should + "build a JDBC URL of the form jdbc:postgresql://{host}:{port}/{database}" in { + clearCapture() + val conn = PostgreSQLConnUtil.connect("host-a", "5432", "db-a", "u", "p") + assert(conn != null) + assert(CapturingPGDriver.seenUrls.size == 1) + assert(CapturingPGDriver.seenUrls.head == "jdbc:postgresql://host-a:5432/db-a") + } + + it should "interpolate distinct host/port/database values into the URL" in { + clearCapture() + PostgreSQLConnUtil.connect("h-1", "1234", "d-1", "u", "p") + assert(CapturingPGDriver.seenUrls.head == "jdbc:postgresql://h-1:1234/d-1") + clearCapture() + PostgreSQLConnUtil.connect("h-2", "9999", "d-2", "u", "p") + assert(CapturingPGDriver.seenUrls.head == "jdbc:postgresql://h-2:9999/d-2") + } + + it should "place host BEFORE port (host-then-port, not port-then-host)" in { + clearCapture() + PostgreSQLConnUtil.connect("a", "1", "x", "u", "p") + val url = CapturingPGDriver.seenUrls.head + assert(url.contains("//a:1/"), s"expected //a:1/ ordering, got: $url") + assert(!url.contains("//1:a/"), s"port-then-host ordering must NOT appear, got: $url") + } + + it should "use the `postgresql` JDBC subprotocol (not e.g. `mysql`)" in { + clearCapture() + PostgreSQLConnUtil.connect("h", "5432", "db", "u", "p") + val url = CapturingPGDriver.seenUrls.head + assert(url.startsWith("jdbc:postgresql://")) + assert(!url.contains("jdbc:mysql:")) + } + + it should "accept an empty database name and still produce a well-formed URL" in { + clearCapture() + PostgreSQLConnUtil.connect("h", "5432", "", "u", "p") + // The resulting `jdbc:postgresql://h:5432/` is well-formed even if a + // real driver would reject it. + assert(CapturingPGDriver.seenUrls.head == "jdbc:postgresql://h:5432/") + } + + // --------------------------------------------------------------------------- + // Credentials propagation + // --------------------------------------------------------------------------- + + it should "pass username and password through DriverManager properties" in { + clearCapture() + PostgreSQLConnUtil.connect("h", "5432", "db", "the-user", "the-pass") + val props = CapturingPGDriver.seenProps.head + assert(props.getProperty("user") == "the-user") + assert(props.getProperty("password") == "the-pass") + } + + // --------------------------------------------------------------------------- + // setReadOnly(true) — pinned via the captured proxy + // --------------------------------------------------------------------------- + + it should "flip the returned Connection to read-only (query-efficiency contract)" in { + clearCapture() + PostgreSQLConnUtil.connect("h", "5432", "db", "u", "p") + assert(CapturingPGDriver.readOnlyCalls == ArrayBuffer(true)) + } + + // --------------------------------------------------------------------------- + // SQLException propagation when the driver throws — pin the @throws contract + // --------------------------------------------------------------------------- + + it should "propagate SQLException when the driver throws" in { + // Swap in a one-shot throwing override of `connect`. We can't mutate + // CapturingPGDriver in-place, so register a higher-priority throwing + // driver and remove it after. + val throwingDriver = new Driver { + override def connect(url: String, info: Properties): Connection = + throw new SQLException(s"forced-fail-for-test") + override def acceptsURL(url: String): Boolean = + url != null && url.startsWith("jdbc:postgresql:") Review Comment: Fixed in 458389ed15 — the PG throwing-driver helper now follows the JDBC contract: returns `null` when the URL is not `jdbc:postgresql:` and throws only on a matching URL. Future code that calls `DriverManager.getConnection` with another scheme while this helper is registered will no longer see a spurious throw. ########## common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala: ########## @@ -0,0 +1,246 @@ +/* + * 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 + * + * http://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.apache.texera.amber.operator.source.sql.mysql + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.BeforeAndAfterAll + +import java.lang.reflect.{InvocationHandler, Method, Proxy} +import java.sql.{Connection, Driver, DriverManager, DriverPropertyInfo, SQLException} +import java.util.Properties +import java.util.logging.Logger +import scala.collection.mutable.ArrayBuffer +import scala.jdk.CollectionConverters._ + +class MySQLConnUtilSpec extends AnyFlatSpec with BeforeAndAfterAll { + + // --------------------------------------------------------------------------- + // Strategy — same capturing-driver pattern as PostgreSQLConnUtilSpec. + // The MySQL driver may or may not be present transitively, so we + // proactively deregister anything that claims jdbc:mysql: and swap in a + // capturing driver that records each URL and returns a Proxy-backed + // Connection so the production code can call `setReadOnly(true)`. + // --------------------------------------------------------------------------- + + private object CapturingMySQLDriver extends Driver { + val seenUrls: ArrayBuffer[String] = ArrayBuffer.empty + val seenProps: ArrayBuffer[Properties] = ArrayBuffer.empty + val readOnlyCalls: ArrayBuffer[Boolean] = ArrayBuffer.empty + + override def connect(url: String, info: Properties): Connection = { + if (!acceptsURL(url)) return null + seenUrls += url + seenProps += info + Proxy + .newProxyInstance( + getClass.getClassLoader, + Array(classOf[Connection]), + new InvocationHandler { + override def invoke(p: Any, m: Method, args: Array[AnyRef]): AnyRef = + m.getName match { + case "setReadOnly" => + readOnlyCalls += args(0).asInstanceOf[java.lang.Boolean].booleanValue() + null + case "equals" => java.lang.Boolean.valueOf(p eq args(0)) + case "hashCode" => java.lang.Integer.valueOf(System.identityHashCode(p)) + case "toString" => + "CapturingMySQLDriver.StubConnection@" + System.identityHashCode(p) + case "isWrapperFor" => java.lang.Boolean.FALSE + case "close" => null + case _ => null + } + } + ) + .asInstanceOf[Connection] + } + override def acceptsURL(url: String): Boolean = + url != null && url.startsWith("jdbc:mysql:") + override def getPropertyInfo(url: String, info: Properties): Array[DriverPropertyInfo] = + Array.empty + override def getMajorVersion: Int = 1 + override def getMinorVersion: Int = 0 + override def jdbcCompliant(): Boolean = false + override def getParentLogger: Logger = Logger.getLogger("test-mysql-capturing") + } + + private val savedRealDrivers: ArrayBuffer[Driver] = ArrayBuffer.empty + + private def safeAcceptsURL(d: Driver, url: String): Boolean = + try d.acceptsURL(url) + catch { case _: Throwable => false } + + override protected def beforeAll(): Unit = { + super.beforeAll() + try { + val others = DriverManager.getDrivers.asScala.toList.filter { d => + d != CapturingMySQLDriver && safeAcceptsURL(d, "jdbc:mysql://h/d") + } Review Comment: Fixed in 458389ed15 — MySQL probe URL now mirrors the production shape including the canonical query parameters: `jdbc:mysql://probe-host:3306/probe-db?autoReconnect=true&useSSL=true`. Same flakiness defense as the PG fix. ########## common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala: ########## @@ -0,0 +1,246 @@ +/* + * 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 + * + * http://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.apache.texera.amber.operator.source.sql.mysql + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.BeforeAndAfterAll + +import java.lang.reflect.{InvocationHandler, Method, Proxy} +import java.sql.{Connection, Driver, DriverManager, DriverPropertyInfo, SQLException} +import java.util.Properties +import java.util.logging.Logger +import scala.collection.mutable.ArrayBuffer +import scala.jdk.CollectionConverters._ + +class MySQLConnUtilSpec extends AnyFlatSpec with BeforeAndAfterAll { + + // --------------------------------------------------------------------------- + // Strategy — same capturing-driver pattern as PostgreSQLConnUtilSpec. + // The MySQL driver may or may not be present transitively, so we + // proactively deregister anything that claims jdbc:mysql: and swap in a + // capturing driver that records each URL and returns a Proxy-backed + // Connection so the production code can call `setReadOnly(true)`. + // --------------------------------------------------------------------------- + + private object CapturingMySQLDriver extends Driver { + val seenUrls: ArrayBuffer[String] = ArrayBuffer.empty + val seenProps: ArrayBuffer[Properties] = ArrayBuffer.empty + val readOnlyCalls: ArrayBuffer[Boolean] = ArrayBuffer.empty + + override def connect(url: String, info: Properties): Connection = { + if (!acceptsURL(url)) return null + seenUrls += url + seenProps += info + Proxy + .newProxyInstance( + getClass.getClassLoader, + Array(classOf[Connection]), + new InvocationHandler { + override def invoke(p: Any, m: Method, args: Array[AnyRef]): AnyRef = + m.getName match { + case "setReadOnly" => + readOnlyCalls += args(0).asInstanceOf[java.lang.Boolean].booleanValue() + null + case "equals" => java.lang.Boolean.valueOf(p eq args(0)) + case "hashCode" => java.lang.Integer.valueOf(System.identityHashCode(p)) + case "toString" => + "CapturingMySQLDriver.StubConnection@" + System.identityHashCode(p) + case "isWrapperFor" => java.lang.Boolean.FALSE + case "close" => null + case _ => null + } + } + ) + .asInstanceOf[Connection] + } + override def acceptsURL(url: String): Boolean = + url != null && url.startsWith("jdbc:mysql:") + override def getPropertyInfo(url: String, info: Properties): Array[DriverPropertyInfo] = + Array.empty + override def getMajorVersion: Int = 1 + override def getMinorVersion: Int = 0 + override def jdbcCompliant(): Boolean = false + override def getParentLogger: Logger = Logger.getLogger("test-mysql-capturing") + } + + private val savedRealDrivers: ArrayBuffer[Driver] = ArrayBuffer.empty + + private def safeAcceptsURL(d: Driver, url: String): Boolean = + try d.acceptsURL(url) + catch { case _: Throwable => false } + + override protected def beforeAll(): Unit = { + super.beforeAll() + try { + val others = DriverManager.getDrivers.asScala.toList.filter { d => + d != CapturingMySQLDriver && safeAcceptsURL(d, "jdbc:mysql://h/d") + } + others.foreach { d => + savedRealDrivers += d + DriverManager.deregisterDriver(d) + } + DriverManager.registerDriver(CapturingMySQLDriver) + } catch { + case t: Throwable => + savedRealDrivers.foreach { d => + try DriverManager.registerDriver(d) + catch { case _: Throwable => () } + } + throw t + } + } + + override protected def afterAll(): Unit = { + try { + try DriverManager.deregisterDriver(CapturingMySQLDriver) + catch { case _: Throwable => () } + savedRealDrivers.foreach { d => + try DriverManager.registerDriver(d) + catch { case _: Throwable => () } + } + } finally { + super.afterAll() + } + } + + private def clearCapture(): Unit = { + CapturingMySQLDriver.seenUrls.clear() + CapturingMySQLDriver.seenProps.clear() + CapturingMySQLDriver.readOnlyCalls.clear() + } + + // --------------------------------------------------------------------------- + // URL composition — host/port/database + // --------------------------------------------------------------------------- + + "MySQLConnUtil.connect" should + "build a JDBC URL of the form jdbc:mysql://{host}:{port}/{database}?…" in { + clearCapture() + val conn = MySQLConnUtil.connect("host-m", "3306", "db-m", "u", "p") + assert(conn != null) + assert(CapturingMySQLDriver.seenUrls.size == 1) + assert(CapturingMySQLDriver.seenUrls.head.startsWith("jdbc:mysql://host-m:3306/db-m")) + } + + it should "interpolate distinct host/port/database values into the URL" in { + clearCapture() + MySQLConnUtil.connect("host-1", "3306", "db-1", "u", "p") + assert(CapturingMySQLDriver.seenUrls.head.startsWith("jdbc:mysql://host-1:3306/db-1")) + clearCapture() + MySQLConnUtil.connect("host-2", "33060", "db-2", "u", "p") + assert(CapturingMySQLDriver.seenUrls.head.startsWith("jdbc:mysql://host-2:33060/db-2")) + } + + it should "place host BEFORE port" in { + clearCapture() + MySQLConnUtil.connect("a", "1", "x", "u", "p") + val url = CapturingMySQLDriver.seenUrls.head + assert(url.contains("//a:1/")) + assert(!url.contains("//1:a/")) + } + + // --------------------------------------------------------------------------- + // Query parameters — autoReconnect=true and useSSL=true must be present + // --------------------------------------------------------------------------- + + it should "include the `autoReconnect=true` query parameter" in { + clearCapture() + MySQLConnUtil.connect("h", "3306", "db", "u", "p") + val url = CapturingMySQLDriver.seenUrls.head + assert(url.contains("autoReconnect=true"), s"URL must include autoReconnect=true, got: $url") + } + + it should "include the `useSSL=true` query parameter (TLS contract)" in { + clearCapture() + MySQLConnUtil.connect("h", "3306", "db", "u", "p") + val url = CapturingMySQLDriver.seenUrls.head + assert(url.contains("useSSL=true"), s"URL must include useSSL=true (TLS), got: $url") + } + + it should "use the canonical `?…&…` separator pattern" in { + clearCapture() + MySQLConnUtil.connect("h", "3306", "db", "u", "p") + val url = CapturingMySQLDriver.seenUrls.head + assert( + url == "jdbc:mysql://h:3306/db?autoReconnect=true&useSSL=true", + s"URL must match canonical pattern, got: $url" + ) + } + + it should "use the `mysql` JDBC subprotocol (not e.g. `postgresql`)" in { + clearCapture() + MySQLConnUtil.connect("h", "3306", "db", "u", "p") + val url = CapturingMySQLDriver.seenUrls.head + assert(url.startsWith("jdbc:mysql://")) + assert(!url.contains("jdbc:postgresql:")) + } + + // --------------------------------------------------------------------------- + // Credentials propagation + // --------------------------------------------------------------------------- + + it should "pass username and password through DriverManager properties" in { + clearCapture() + MySQLConnUtil.connect("h", "3306", "db", "the-user", "the-pass") + val props = CapturingMySQLDriver.seenProps.head + assert(props.getProperty("user") == "the-user") + assert(props.getProperty("password") == "the-pass") + } + + // --------------------------------------------------------------------------- + // setReadOnly(true) — pinned via the captured proxy (parity with PG spec) + // --------------------------------------------------------------------------- + + it should "flip the returned Connection to read-only (query-efficiency contract)" in { + clearCapture() + MySQLConnUtil.connect("h", "3306", "db", "u", "p") + assert(CapturingMySQLDriver.readOnlyCalls == ArrayBuffer(true)) + } + + // --------------------------------------------------------------------------- + // SQLException propagation when the driver throws + // --------------------------------------------------------------------------- + + it should "propagate SQLException when the driver throws" in { + val throwingDriver = new Driver { + override def connect(url: String, info: Properties): Connection = + throw new SQLException(s"forced-fail-for-test") + override def acceptsURL(url: String): Boolean = + url != null && url.startsWith("jdbc:mysql:") + override def getPropertyInfo(url: String, info: Properties) = Review Comment: Fixed in 458389ed15 — MySQL throwing-driver helper now follows the JDBC contract: returns `null` on a non-matching URL, throws only on `jdbc:mysql:*`. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
