This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 83df2b5132 test(workflow-operator): add unit test coverage for
PostgreSQLConnUtil and MySQLConnUtil (#5698)
83df2b5132 is described below
commit 83df2b5132d3b9ab76f8796c890a0685edd9f1fd
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sun Jun 14 20:39:07 2026 -0700
test(workflow-operator): add unit test coverage for PostgreSQLConnUtil and
MySQLConnUtil (#5698)
### What changes were proposed in this PR?
Pin the JDBC URL composition for the two SQL-source connection helpers
in `common/workflow-operator/operator/source/sql/{postgresql,mysql}/`
without standing up a real DB. No production-code changes.
| Spec | Source class | Tests |
| --- | --- | --- |
| `PostgreSQLConnUtilSpec` | `PostgreSQLConnUtil` | 8 |
| `MySQLConnUtilSpec` | `MySQLConnUtil` | 10 |
Both spec files follow the `<srcClassName>Spec.scala` one-to-one
convention.
**Behavior pinned — `PostgreSQLConnUtil`**
| Surface | Contract |
| --- | --- |
| URL format | `jdbc:postgresql://{host}:{port}/{database}` (exact
substring) |
| Host/port/database interpolation | distinct values reach their slots;
host BEFORE port |
| Subprotocol | `jdbc:postgresql:`, never `jdbc:mysql:` |
| Empty database name | produces a well-formed
`jdbc:postgresql://{host}:{port}/` URL |
| Credentials | `user` / `password` reach the driver via `Properties` |
| `setReadOnly(true)` | called on the returned Connection
(query-efficiency contract) |
| `SQLException` | propagated when the driver throws |
**Behavior pinned — `MySQLConnUtil`**
| Surface | Contract |
| --- | --- |
| URL format | `jdbc:mysql://{host}:{port}/{database}?…` (exact
substring) |
| Host/port/database interpolation | distinct values reach their slots;
host BEFORE port |
| `autoReconnect=true` query parameter | present (retry-behavior
contract) |
| `useSSL=true` query parameter | present (TLS contract — drift here
would silently downgrade security) |
| `?` / `&` separators | canonical
`jdbc:mysql://h:3306/db?autoReconnect=true&useSSL=true` sequence pinned
end-to-end |
| Subprotocol | `jdbc:mysql:`, never `jdbc:postgresql:` |
| Credentials | `user` / `password` reach the driver via `Properties` |
| `setReadOnly(true)` | called on the returned Connection
(query-efficiency contract) |
| `SQLException` | propagated when the driver throws |
**Test strategy**
Both specs use the same capturing-driver pattern:
- Deregister every driver that claims the relevant `jdbc:postgresql:` /
`jdbc:mysql:` scheme via a `safeAcceptsURL` helper (the JDBC spec allows
`Driver.acceptsURL` to throw `SQLException`, so the probe must be
defensive).
- Register a capturing driver that records each URL + the `Properties`
it is asked to open, and returns a `java.lang.reflect.Proxy`-backed
`Connection` so the production code can call `setReadOnly(true)` against
a stand-in.
- Restore the original drivers in `afterAll`. Setup failures also
trigger best-effort re-registration so a sibling suite never sees a
half-deregistered JDBC registry.
This approach works regardless of whether a real driver happens to be on
the test classpath (the PostgreSQL driver is loaded transitively from
`org.postgresql:postgresql`; the MySQL driver is not currently on the
workflow-operator classpath, but the spec is robust if it gets added
later).
### Any related issues, documentation, discussions?
Closes #5695.
### How was this PR tested?
Pure unit-test additions; verified locally with:
- \`sbt \"WorkflowOperator/testOnly
org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLConnUtilSpec
org.apache.texera.amber.operator.source.sql.mysql.MySQLConnUtilSpec\"\`
— 18 tests, all green
- \`sbt scalafmtCheckAll\` — clean
- CI to confirm
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.7 [1M context])
---
.../source/sql/mysql/MySQLConnUtilSpec.scala | 260 ++++++++++++++++++++
.../sql/postgresql/PostgreSQLConnUtilSpec.scala | 263 +++++++++++++++++++++
2 files changed, 523 insertions(+)
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala
new file mode 100644
index 0000000000..e4bd71cb18
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala
@@ -0,0 +1,260 @@
+/*
+ * 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()
+ // The probe URL mirrors the exact shape `MySQLConnUtil.connect`
+ // constructs (`jdbc:mysql://{host}:{port}/{database}?…`), including
+ // the canonical query parameters. A permissive third-party driver
+ // that returns `false` on a stripped-down probe but `true` on the
+ // real URL would otherwise slip past us.
+ try {
+ val others = DriverManager.getDrivers.asScala.toList.filter { d =>
+ d != CapturingMySQLDriver && safeAcceptsURL(
+ d,
+
"jdbc:mysql://probe-host:3306/probe-db?autoReconnect=true&useSSL=true"
+ )
+ }
+ 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 acceptsURL(url: String): Boolean =
+ url != null && url.startsWith("jdbc:mysql:")
+ // Follow the JDBC contract: return `null` if the URL isn't ours
+ // and throw only on a matching URL — keeps the helper from
+ // interfering with `DriverManager.getConnection` calls for any
+ // other scheme that might happen during the suite.
+ override def connect(url: String, info: Properties): Connection = {
+ if (!acceptsURL(url)) return null
+ throw new SQLException("forced-fail-for-test")
+ }
+ override def getPropertyInfo(url: String, info: Properties) =
+ Array.empty[DriverPropertyInfo]
+ override def getMajorVersion: Int = 99
+ override def getMinorVersion: Int = 0
+ override def jdbcCompliant(): Boolean = false
+ override def getParentLogger: Logger =
Logger.getLogger("test-mysql-throwing")
+ }
+ DriverManager.deregisterDriver(CapturingMySQLDriver)
+ DriverManager.registerDriver(throwingDriver)
+ try {
+ val ex = intercept[SQLException] {
+ MySQLConnUtil.connect("h", "3306", "db", "u", "p")
+ }
+ assert(ex.getMessage.contains("forced-fail-for-test"))
+ } finally {
+ DriverManager.deregisterDriver(throwingDriver)
+ DriverManager.registerDriver(CapturingMySQLDriver)
+ }
+ }
+}
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala
new file mode 100644
index 0000000000..5081c11c26
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala
@@ -0,0 +1,263 @@
+/*
+ * 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.
+ // The probe URL mirrors the exact shape `PostgreSQLConnUtil.connect`
+ // constructs (`jdbc:postgresql://{host}:{port}/{database}`) so a
+ // permissive third-party driver that returns `false` on a stripped-
+ // down probe but `true` on the real URL can't slip past us.
+ //
+ // 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://probe-host:5432/probe-db")
+ }
+ 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 acceptsURL(url: String): Boolean =
+ url != null && url.startsWith("jdbc:postgresql:")
+ // Follow the JDBC contract: return `null` if the URL is not ours,
+ // then throw only on a matching URL. A future refactor that calls
+ // `DriverManager.getConnection` with a different scheme while
+ // this driver is registered would otherwise see a spurious throw.
+ override def connect(url: String, info: Properties): Connection = {
+ if (!acceptsURL(url)) return null
+ throw new SQLException("forced-fail-for-test")
+ }
+ override def getPropertyInfo(url: String, info: Properties) =
Array.empty[DriverPropertyInfo]
+ override def getMajorVersion: Int = 99
+ override def getMinorVersion: Int = 0
+ override def jdbcCompliant(): Boolean = false
+ override def getParentLogger: Logger =
Logger.getLogger("test-pg-throwing")
+ }
+ DriverManager.deregisterDriver(CapturingPGDriver)
+ DriverManager.registerDriver(throwingDriver)
+ try {
+ val ex = intercept[SQLException] {
+ PostgreSQLConnUtil.connect("h", "5432", "db", "u", "p")
+ }
+ assert(ex.getMessage.contains("forced-fail-for-test"))
+ } finally {
+ DriverManager.deregisterDriver(throwingDriver)
+ DriverManager.registerDriver(CapturingPGDriver)
+ }
+ }
+}