Files
MyGoNavi/internal/db/database_i18n_test.go
tianqijiuyun-latiao d13c153f5e feat(i18n): 收口数据库驱动多语言代码
- 提交 internal/db 多驱动用户可见错误与状态文案多语言化

- 补齐数据库驱动多语言测试与六语言 catalog

- 修复 frontend i18n catalog 的 4 个失效 guard
2026-06-22 10:09:45 +08:00

678 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package db
import (
"context"
"database/sql"
"errors"
"os"
"strings"
"testing"
"GoNavi-Wails/shared/i18n"
)
type fakeRowsAffectedResult struct {
affected int64
err error
}
const (
rawTransactionAlreadyFinishedText = "\u4e8b\u52a1\u5df2\u7ed3\u675f"
rawTransactionNotOpenText = "\u4e8b\u52a1\u672a\u6253\u5f00"
)
func (r fakeRowsAffectedResult) LastInsertId() (int64, error) {
return 0, nil
}
func (r fakeRowsAffectedResult) RowsAffected() (int64, error) {
if r.err != nil {
return 0, r.err
}
return r.affected, nil
}
func databaseFunctionSource(t *testing.T, source string, signature string) string {
t.Helper()
start := strings.Index(source, signature)
if start < 0 {
t.Fatalf("database.go missing function signature %q", signature)
}
rest := source[start+len(signature):]
end := strings.Index(rest, "\nfunc ")
if end < 0 {
return source[start:]
}
return source[start : start+len(signature)+end]
}
func TestRequireSingleRowAffectedUsesLocalizedText(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
cases := []struct {
name string
result fakeRowsAffectedResult
action rowMutationAction
want string
}{
{
name: "delete rows affected unavailable",
result: fakeRowsAffectedResult{err: errors.New("rows affected unsupported")},
action: rowMutationActionDelete,
want: "Delete did not take effect: could not determine affected rows: rows affected unsupported",
},
{
name: "delete no rows matched",
result: fakeRowsAffectedResult{affected: 0},
action: rowMutationActionDelete,
want: "Delete did not take effect: no rows matched",
},
{
name: "update multiple rows affected",
result: fakeRowsAffectedResult{affected: 2},
action: rowMutationActionUpdate,
want: "Update did not take effect: affected 2 rows; expected exactly 1",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := requireSingleRowAffected(tc.result, tc.action)
if err == nil {
t.Fatal("expected row affected validation error")
}
if err.Error() != tc.want {
t.Fatalf("expected %q, got %q", tc.want, err.Error())
}
})
}
}
func TestRequireSingleRowAffectedSourceUsesI18nKeys(t *testing.T) {
sourceBytes, err := os.ReadFile("database.go")
if err != nil {
t.Fatalf("read database.go: %v", err)
}
source := string(sourceBytes)
functionSource := databaseFunctionSource(t, source, "func requireSingleRowAffected(result sql.Result, action rowMutationAction) error")
actionSource := databaseFunctionSource(t, source, "func localizedRowMutationAction(action rowMutationAction) string")
for _, rawMessage := range []string{
`fmt.Errorf("%s未生效无法确认影响行数%v", action, err)`,
`fmt.Errorf("%s未生效未匹配到任何行", action)`,
`fmt.Errorf("%s未生效影响了 %d 行,期望只影响 1 行", action, affected)`,
} {
if strings.Contains(functionSource, rawMessage) {
t.Fatalf("requireSingleRowAffected still contains raw row affected text %q", rawMessage)
}
}
for _, key := range []string{
"db.backend.error.row_action_not_effective_rows_affected_unknown",
"db.backend.error.row_action_not_effective_no_rows_matched",
"db.backend.error.row_action_not_effective_multiple_rows",
} {
if !strings.Contains(functionSource, key) {
t.Fatalf("requireSingleRowAffected does not reference i18n key %q", key)
}
}
for _, key := range []string{
"db.backend.action.delete",
"db.backend.action.update",
} {
if !strings.Contains(actionSource, key) {
t.Fatalf("localizedRowMutationAction does not reference i18n key %q", key)
}
}
}
func TestDatabaseRowAffectedCatalogKeysExist(t *testing.T) {
catalogs, err := i18n.LoadCatalogs()
if err != nil {
t.Fatalf("LoadCatalogs() error = %v", err)
}
keys := []string{
"db.backend.action.delete",
"db.backend.action.update",
"db.backend.error.row_action_not_effective_rows_affected_unknown",
"db.backend.error.row_action_not_effective_no_rows_matched",
"db.backend.error.row_action_not_effective_multiple_rows",
}
for _, language := range i18n.SupportedLanguages() {
catalog := catalogs[language]
for _, key := range keys {
if strings.TrimSpace(catalog[key]) == "" {
t.Fatalf("%s catalog missing database row-affected key %q", language, key)
}
}
}
}
func TestSQLConnStatementExecerUsesCurrentLanguageForConnectionNotOpen(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
execer := &sqlConnStatementExecer{}
cases := []struct {
name string
call func() error
}{
{
name: "exec",
call: func() error {
_, err := execer.ExecContext(context.Background(), "SELECT 1")
return err
},
},
{
name: "query",
call: func() error {
_, _, err := execer.QueryContext(context.Background(), "SELECT 1")
return err
},
},
{
name: "query_multi",
call: func() error {
_, err := execer.QueryMultiContext(context.Background(), "SELECT 1")
return err
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.call()
if err == nil {
t.Fatal("expected connection-not-open error")
}
if err.Error() != "Connection is not open" {
t.Fatalf("expected English connection-not-open error, got %q", err.Error())
}
if strings.Contains(err.Error(), "连接未打开") {
t.Fatalf("expected no Chinese connection-not-open text, got %q", err.Error())
}
})
}
}
func TestSQLConnTransactionExecerUsesCurrentLanguageForConnectionNotOpen(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
execer := &sqlConnTransactionExecer{}
_, err := execer.activeConn()
if err == nil {
t.Fatal("expected connection-not-open error")
}
if err.Error() != "Connection is not open" {
t.Fatalf("expected English connection-not-open error, got %q", err.Error())
}
if strings.Contains(err.Error(), "连接未打开") {
t.Fatalf("expected no Chinese connection-not-open text, got %q", err.Error())
}
}
func TestDatabaseConnectionNotOpenSourceUsesI18nKey(t *testing.T) {
sourceBytes, err := os.ReadFile("database.go")
if err != nil {
t.Fatalf("read database.go: %v", err)
}
source := string(sourceBytes)
checks := map[string]string{
"func (e *sqlConnStatementExecer) ExecContext(ctx context.Context, query string) (int64, error)": "db.backend.error.connection_not_open",
"func (e *sqlConnStatementExecer) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error)": "db.backend.error.connection_not_open",
"func (e *sqlConnStatementExecer) QueryMultiContext(ctx context.Context, query string) ([]connection.ResultSetData, error)": "db.backend.error.connection_not_open",
"func (e *sqlConnTransactionExecer) activeConn() (*sql.Conn, error)": "db.backend.error.connection_not_open",
}
for signature, key := range checks {
functionSource := databaseFunctionSource(t, source, signature)
if strings.Contains(functionSource, `fmt.Errorf("连接未打开")`) {
t.Fatalf("%s still contains raw connection-not-open text", signature)
}
if !strings.Contains(functionSource, key) {
t.Fatalf("%s does not reference i18n key %q", signature, key)
}
}
}
func TestDatabaseConnectionNotOpenCatalogKeyExists(t *testing.T) {
catalogs, err := i18n.LoadCatalogs()
if err != nil {
t.Fatalf("LoadCatalogs() error = %v", err)
}
for _, language := range i18n.SupportedLanguages() {
catalog := catalogs[language]
if strings.TrimSpace(catalog["db.backend.error.connection_not_open"]) == "" {
t.Fatalf("%s catalog missing database connection-not-open key", language)
}
}
}
func TestWrapDatabaseConnectionErrorsUseCurrentLanguage(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
baseErr := errors.New("driver unavailable")
cases := []struct {
name string
call func(error) error
want string
}{
{
name: "open",
call: wrapDatabaseConnectionOpenError,
want: "Failed to open database connection: driver unavailable",
},
{
name: "verify",
call: wrapDatabaseConnectionVerifyError,
want: "Failed to verify the established connection: driver unavailable",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.call(baseErr)
if err == nil {
t.Fatal("expected wrapped database connection error")
}
if err.Error() != tc.want {
t.Fatalf("expected %q, got %q", tc.want, err.Error())
}
if !errors.Is(err, baseErr) {
t.Fatal("expected wrapped error to preserve cause")
}
if strings.Contains(err.Error(), "打开数据库连接失败") || strings.Contains(err.Error(), "连接建立后验证失败") {
t.Fatalf("expected no raw Chinese connection wrapper text, got %q", err.Error())
}
})
}
}
func TestDatabaseConnectionWrapperHelperUsesI18nKeys(t *testing.T) {
sourceBytes, err := os.ReadFile("database.go")
if err != nil {
t.Fatalf("read database.go: %v", err)
}
source := string(sourceBytes)
checks := map[string]string{
"func wrapDatabaseConnectionOpenError(err error) error": "db.backend.error.connection_open_failed_prefix",
"func wrapDatabaseConnectionVerifyError(err error) error": "db.backend.error.connection_verify_failed_prefix",
}
for signature, key := range checks {
functionSource := databaseFunctionSource(t, source, signature)
if strings.Contains(functionSource, `fmt.Errorf("打开数据库连接失败:%w", err)`) || strings.Contains(functionSource, `fmt.Errorf("连接建立后验证失败:%w", err)`) {
t.Fatalf("%s still contains raw database connection wrapper text", signature)
}
if !strings.Contains(functionSource, key) {
t.Fatalf("%s does not reference i18n key %q", signature, key)
}
}
}
func TestDatabaseConnectionWrapperSourcesUseI18nHelpers(t *testing.T) {
type sourceCheck struct {
path string
requiredTexts []string
}
checks := []sourceCheck{
{
path: "custom_impl.go",
requiredTexts: []string{
"wrapDatabaseConnectionOpenError(err)",
"wrapDatabaseConnectionVerifyError(err)",
},
},
{
path: "mariadb_impl.go",
requiredTexts: []string{
"wrapDatabaseConnectionOpenError(err)",
"wrapDatabaseConnectionVerifyError(err)",
},
},
{
path: "sqlite_impl.go",
requiredTexts: []string{
"wrapDatabaseConnectionOpenError(err)",
"wrapDatabaseConnectionVerifyError(err)",
},
},
{
path: "sqlserver_impl.go",
requiredTexts: []string{
"wrapDatabaseConnectionOpenError(err)",
"wrapDatabaseConnectionVerifyError(err)",
},
},
{
path: "iris_impl.go",
requiredTexts: []string{
"wrapDatabaseConnectionOpenError(err)",
"wrapDatabaseConnectionVerifyError(err)",
},
},
}
for _, check := range checks {
sourceBytes, err := os.ReadFile(check.path)
if err != nil {
t.Fatalf("read %s: %v", check.path, err)
}
source := string(sourceBytes)
if strings.Contains(source, `fmt.Errorf("打开数据库连接失败:%w", err)`) {
t.Fatalf("%s still contains raw open-connection wrapper", check.path)
}
if strings.Contains(source, `fmt.Errorf("连接建立后验证失败:%w", err)`) {
t.Fatalf("%s still contains raw verify-connection wrapper", check.path)
}
for _, required := range check.requiredTexts {
if !strings.Contains(source, required) {
t.Fatalf("%s does not reference i18n helper %q", check.path, required)
}
}
}
}
func TestDatabaseConnectionWrapperCatalogKeysExist(t *testing.T) {
catalogs, err := i18n.LoadCatalogs()
if err != nil {
t.Fatalf("LoadCatalogs() error = %v", err)
}
keys := []string{
"db.backend.error.connection_open_failed_prefix",
"db.backend.error.connection_verify_failed_prefix",
}
for _, language := range i18n.SupportedLanguages() {
catalog := catalogs[language]
for _, key := range keys {
if strings.TrimSpace(catalog[key]) == "" {
t.Fatalf("%s catalog missing database connection wrapper key %q", language, key)
}
}
}
}
func TestFormatCustomDriverOpenErrorUsesCurrentLanguageForUnknownDrivers(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
cases := []struct {
name string
driver string
base error
want string
}{
{
name: "system odbc driver",
driver: "InterSystems IRIS ODBC35",
base: errors.New(`sql: unknown driver "InterSystems IRIS ODBC35" (forgotten import?)`),
want: `Failed to open database connection: custom connections do not support entering the system ODBC/JDBC driver name "InterSystems IRIS ODBC35" directly. Enter a Go database/sql driver name already registered by GoNavi. The current build does not register a generic ODBC driver, so connecting to InterSystems IRIS through "InterSystems IRIS ODBC35" is not supported yet: sql: unknown driver "InterSystems IRIS ODBC35" (forgotten import?)`,
},
{
name: "unregistered go driver",
driver: "not-a-registered-go-driver",
base: errors.New(`sql: unknown driver "not-a-registered-go-driver" (forgotten import?)`),
want: `Failed to open database connection: the custom connection driver "not-a-registered-go-driver" is not registered in GoNavi. Enter a registered Go database/sql driver name instead of a system ODBC/JDBC driver name: sql: unknown driver "not-a-registered-go-driver" (forgotten import?)`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := formatCustomDriverOpenError(tc.driver, tc.base)
if err == nil {
t.Fatal("expected wrapped custom driver open error")
}
if err.Error() != tc.want {
t.Fatalf("expected %q, got %q", tc.want, err.Error())
}
if !errors.Is(err, tc.base) {
t.Fatal("expected wrapped custom driver error to preserve cause")
}
if strings.Contains(err.Error(), "自定义连接") || strings.Contains(err.Error(), "未注册通用 ODBC 驱动") {
t.Fatalf("expected no raw Chinese custom driver guidance, got %q", err.Error())
}
})
}
}
func TestFormatCustomDriverOpenErrorSourceUsesI18nKeys(t *testing.T) {
sourceBytes, err := os.ReadFile("custom_impl.go")
if err != nil {
t.Fatalf("read custom_impl.go: %v", err)
}
source := string(sourceBytes)
functionSource := databaseFunctionSource(t, source, "func formatCustomDriverOpenError(driver string, err error) error")
for _, rawMessage := range []string{
`fmt.Errorf("打开数据库连接失败:自定义连接不支持直接填写系统 ODBC/JDBC 驱动名 %q请填写 GoNavi 已注册的 Go database/sql 驱动名。当前版本未注册通用 ODBC 驱动,因此暂不支持通过 %q 连接 InterSystems IRIS%w", driver, driver, err)`,
`fmt.Errorf("打开数据库连接失败:自定义连接驱动 %q 未在 GoNavi 中注册;请填写已注册的 Go database/sql 驱动名,不能填写系统 ODBC/JDBC 驱动名:%w", driver, err)`,
} {
if strings.Contains(functionSource, rawMessage) {
t.Fatalf("formatCustomDriverOpenError still contains raw custom driver guidance %q", rawMessage)
}
}
for _, key := range []string{
"db.backend.error.custom_driver_system_odbc_unsupported_prefix",
"db.backend.error.custom_driver_unregistered_prefix",
} {
if !strings.Contains(functionSource, key) {
t.Fatalf("formatCustomDriverOpenError does not reference i18n key %q", key)
}
}
}
func TestCustomDriverOpenErrorCatalogKeysExist(t *testing.T) {
catalogs, err := i18n.LoadCatalogs()
if err != nil {
t.Fatalf("LoadCatalogs() error = %v", err)
}
keys := []string{
"db.backend.error.custom_driver_system_odbc_unsupported_prefix",
"db.backend.error.custom_driver_unregistered_prefix",
}
for _, language := range i18n.SupportedLanguages() {
catalog := catalogs[language]
for _, key := range keys {
if strings.TrimSpace(catalog[key]) == "" {
t.Fatalf("%s catalog missing custom driver open error key %q", language, key)
}
}
}
}
func TestNewDatabaseUnsupportedTypeUsesCurrentLanguage(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
dbType := "mystery-driver"
_, err := NewDatabase(dbType)
if err == nil {
t.Fatal("expected unsupported database type error")
}
want := "Unsupported database type: mystery-driver"
if err.Error() != want {
t.Fatalf("expected localized unsupported database type error %q, got %q", want, err.Error())
}
rawUnsupportedDatabaseTypeText := "\u4e0d\u652f\u6301\u7684\u6570\u636e\u5e93\u7c7b\u578b"
if strings.Contains(err.Error(), rawUnsupportedDatabaseTypeText) {
t.Fatalf("expected no Chinese unsupported database type text, got %q", err.Error())
}
}
func TestNewDatabaseUnsupportedTypeSourceUsesI18nKey(t *testing.T) {
sourceBytes, err := os.ReadFile("database.go")
if err != nil {
t.Fatalf("read database.go: %v", err)
}
source := string(sourceBytes)
functionSource := databaseFunctionSource(t, source, "func NewDatabase(dbType string) (Database, error)")
rawUnsupportedDatabaseTypeText := "\u4e0d\u652f\u6301\u7684\u6570\u636e\u5e93\u7c7b\u578b"
rawUnsupportedDatabaseTypeSnippet := `fmt.Errorf("` + rawUnsupportedDatabaseTypeText + `%s", dbType)`
if strings.Contains(functionSource, rawUnsupportedDatabaseTypeSnippet) {
t.Fatal("NewDatabase still contains raw unsupported database type text")
}
if !strings.Contains(functionSource, "db.backend.error.unsupported_database_type") {
t.Fatal("NewDatabase does not reference db.backend.error.unsupported_database_type")
}
}
func TestNewDatabaseUnsupportedTypeCatalogKeyExists(t *testing.T) {
catalogs, err := i18n.LoadCatalogs()
if err != nil {
t.Fatalf("LoadCatalogs() error = %v", err)
}
for _, language := range i18n.SupportedLanguages() {
catalog := catalogs[language]
if strings.TrimSpace(catalog["db.backend.error.unsupported_database_type"]) == "" {
t.Fatalf("%s catalog missing unsupported database type key", language)
}
}
}
func TestTransactionExecerStateErrorsUseCurrentLanguage(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
cases := []struct {
name string
call func() error
want string
unexpected string
}{
{
name: "sql conn transaction already finished",
call: func() error {
_, err := (&sqlConnTransactionExecer{
conn: new(sql.Conn),
done: true,
}).activeConn()
return err
},
want: "Transaction has already finished",
unexpected: rawTransactionAlreadyFinishedText,
},
{
name: "sql tx transaction not open",
call: func() error {
_, err := (&sqlTxStatementExecer{}).activeTx()
return err
},
want: "Transaction is not open",
unexpected: rawTransactionNotOpenText,
},
{
name: "sql tx transaction already finished",
call: func() error {
_, err := (&sqlTxStatementExecer{
tx: new(sql.Tx),
done: true,
}).activeTx()
return err
},
want: "Transaction has already finished",
unexpected: rawTransactionAlreadyFinishedText,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.call()
if err == nil {
t.Fatal("expected localized transaction state error")
}
if err.Error() != tc.want {
t.Fatalf("expected %q, got %q", tc.want, err.Error())
}
if strings.Contains(err.Error(), tc.unexpected) {
t.Fatalf("expected no raw Chinese transaction state text, got %q", err.Error())
}
})
}
}
func TestDatabaseTransactionStateSourcesUseI18nKeys(t *testing.T) {
sourceBytes, err := os.ReadFile("database.go")
if err != nil {
t.Fatalf("read database.go: %v", err)
}
source := string(sourceBytes)
checks := map[string][]string{
"func (e *sqlConnTransactionExecer) activeConn() (*sql.Conn, error)": {
"db.backend.error.connection_not_open",
"db.backend.error.transaction_already_finished",
},
"func (e *sqlTxStatementExecer) activeTx() (*sql.Tx, error)": {
"db.backend.error.transaction_not_open",
"db.backend.error.transaction_already_finished",
},
}
for signature, keys := range checks {
functionSource := databaseFunctionSource(t, source, signature)
for _, rawMessage := range []string{
`fmt.Errorf("` + rawTransactionNotOpenText + `")`,
`fmt.Errorf("` + rawTransactionAlreadyFinishedText + `")`,
} {
if strings.Contains(functionSource, rawMessage) {
t.Fatalf("%s still contains raw transaction state text %q", signature, rawMessage)
}
}
for _, key := range keys {
if !strings.Contains(functionSource, key) {
t.Fatalf("%s does not reference i18n key %q", signature, key)
}
}
}
}
func TestDatabaseTransactionStateCatalogKeysExist(t *testing.T) {
catalogs, err := i18n.LoadCatalogs()
if err != nil {
t.Fatalf("LoadCatalogs() error = %v", err)
}
keys := []string{
"db.backend.error.transaction_not_open",
"db.backend.error.transaction_already_finished",
}
for _, language := range i18n.SupportedLanguages() {
catalog := catalogs[language]
for _, key := range keys {
if strings.TrimSpace(catalog[key]) == "" {
t.Fatalf("%s catalog missing database transaction state key %q", language, key)
}
}
}
}