Files
MyGoNavi/internal/db/mysql_connection_params_test.go
Syngnat c0ae40c638 🐛 fix(mysql): 修复旧版 Windows 无法解析 Asia/Shanghai 时区
- 嵌入 Go IANA 时区数据,兼容 Windows Server 2012 等缺少 zoneinfo 的环境
- 保持 MySQL serverTimezone=GMT+8 到 loc=Asia/Shanghai 的时间语义
- 增加 MySQL DSN 时区解析回归测试
Refs #449
2026-05-10 17:29:11 +08:00

177 lines
5.1 KiB
Go

package db
import (
"database/sql"
"net/url"
"strings"
"testing"
"GoNavi-Wails/internal/connection"
)
func parseMySQLDSNQueryForTest(t *testing.T, dsn string) url.Values {
t.Helper()
parts := strings.SplitN(dsn, "?", 2)
if len(parts) != 2 {
t.Fatalf("dsn missing query: %s", dsn)
}
values, err := url.ParseQuery(parts[1])
if err != nil {
t.Fatalf("parse dsn query: %v", err)
}
return values
}
func TestMySQLDSN_MergesConnectionParamsWithDefaults(t *testing.T) {
t.Parallel()
m := &MySQLDB{}
dsn, err := m.getDSN(connection.ConnectionConfig{
Host: "db.local",
Port: 3306,
User: "root",
Password: "secret",
Database: "app",
Timeout: 30,
ConnectionParams: "charset=utf8&readTimeout=10&columnsWithAlias=true",
})
if err != nil {
t.Fatalf("getDSN failed: %v", err)
}
query := parseMySQLDSNQueryForTest(t, dsn)
if got := query.Get("charset"); got != "utf8" {
t.Fatalf("charset should be overridden by connectionParams, got=%q", got)
}
if got := query.Get("readTimeout"); got != "10s" {
t.Fatalf("numeric readTimeout should be converted to duration, got=%q", got)
}
if got := query.Get("columnsWithAlias"); got != "true" {
t.Fatalf("driver-specific parameter should be preserved, got=%q", got)
}
if got := query.Get("multiStatements"); got != "true" {
t.Fatalf("default multiStatements should remain enabled, got=%q", got)
}
}
func TestMySQLDSN_MapsCommonJDBCParamsWithoutLeakingUnsupportedKeys(t *testing.T) {
t.Parallel()
m := &MySQLDB{}
dsn, err := m.getDSN(connection.ConnectionConfig{
Host: "192.168.1.1",
Port: 3306,
User: "root",
Database: "app",
ConnectionParams: "useUnicode=true&characterEncoding=utf8&autoReconnect=true&" +
"useSSL=false&verifyServerCertificate=false&useOldAliasMetadataBehavior=true",
})
if err != nil {
t.Fatalf("getDSN failed: %v", err)
}
query := parseMySQLDSNQueryForTest(t, dsn)
if got := query.Get("charset"); got != "utf8" {
t.Fatalf("characterEncoding should map to charset, got=%q", got)
}
if got := query.Get("tls"); got != "false" {
t.Fatalf("useSSL=false should map to tls=false, got=%q", got)
}
for _, forbidden := range []string{
"useUnicode",
"characterEncoding",
"autoReconnect",
"useSSL",
"verifyServerCertificate",
"useOldAliasMetadataBehavior",
} {
if _, exists := query[forbidden]; exists {
t.Fatalf("JDBC-only parameter %s should not be passed to Go MySQL driver: %v", forbidden, query)
}
}
}
func TestMySQLDSN_MapsJDBCUTF8EncodingToMySQLCharset(t *testing.T) {
t.Parallel()
m := &MySQLDB{}
dsn, err := m.getDSN(connection.ConnectionConfig{
Host: "192.168.1.240",
Port: 3306,
User: "root",
Database: "mkefu_location_dev_local",
URI: "jdbc:mysql://192.168.1.240:3306/mkefu_location_dev_local?" +
"useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8",
})
if err != nil {
t.Fatalf("getDSN failed: %v", err)
}
query := parseMySQLDSNQueryForTest(t, dsn)
if got := query.Get("charset"); got != "utf8mb4" {
t.Fatalf("JDBC characterEncoding=UTF-8 should map to MySQL charset utf8mb4, got=%q", got)
}
if got := query.Get("characterEncoding"); got != "" {
t.Fatalf("JDBC characterEncoding should not be passed to Go MySQL driver, got=%q", got)
}
if got := query.Get("serverTimezone"); got != "" {
t.Fatalf("JDBC serverTimezone should not be passed to Go MySQL driver, got=%q", got)
}
if got := query.Get("loc"); got != "Asia%2FShanghai" && got != "Asia/Shanghai" {
t.Fatalf("serverTimezone=GMT+8 should map to loc=Asia/Shanghai, got=%q", got)
}
}
func TestMySQLDSN_AsiaShanghaiLocationAcceptedByDriver(t *testing.T) {
t.Parallel()
m := &MySQLDB{}
dsn, err := m.getDSN(connection.ConnectionConfig{
Host: "127.0.0.1",
Port: 3306,
User: "root",
Database: "app",
ConnectionParams: "serverTimezone=GMT%2B8",
})
if err != nil {
t.Fatalf("getDSN failed: %v", err)
}
db, err := sql.Open("mysql", dsn)
if err != nil {
t.Fatalf("mysql driver should accept loc=Asia/Shanghai: %v", err)
}
_ = db.Close()
}
func TestMySQLDSN_URIParamsAndExplicitParamsPrecedence(t *testing.T) {
t.Parallel()
m := &MySQLDB{}
dsn, err := m.getDSN(connection.ConnectionConfig{
Host: "db.local",
Port: 3306,
User: "root",
Database: "app",
URI: "jdbc:mysql://db.local:3306/app?characterEncoding=utf8&timeout=15&topology=replica&useSSL=false",
ConnectionParams: "charset=utf8mb4&timeout=5s&socketTimeout=45000",
})
if err != nil {
t.Fatalf("getDSN failed: %v", err)
}
query := parseMySQLDSNQueryForTest(t, dsn)
if got := query.Get("charset"); got != "utf8mb4" {
t.Fatalf("connectionParams should override URI charset, got=%q", got)
}
if got := query.Get("timeout"); got != "5s" {
t.Fatalf("connectionParams should override URI timeout, got=%q", got)
}
if got := query.Get("readTimeout"); got != "45s" {
t.Fatalf("socketTimeout should map to readTimeout duration, got=%q", got)
}
if _, exists := query["topology"]; exists {
t.Fatalf("internal topology parameter should not be passed to driver: %v", query)
}
}