175 lines
6.3 KiB
Go
175 lines
6.3 KiB
Go
package dbmigration
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.pcux.cn/xuans/Smail/config"
|
|
mysql "github.com/go-sql-driver/mysql"
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
func TestPortablePostgresUsesSupportedRuntimeTLSMode(t *testing.T) {
|
|
for _, name := range []string{"", "smail_next"} {
|
|
driver, dsn, err := runtimeSQLDSN(config.DatabaseConfig{
|
|
Type: config.DatabaseTypePostgreSQL, Host: "::1", Port: 5432, Username: "review",
|
|
}, "test-only-password", name)
|
|
if err != nil || driver != "postgres" {
|
|
t.Fatal("PostgreSQL connection was not built")
|
|
}
|
|
parsed, err := url.Parse(dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
query := parsed.Query()
|
|
if query.Get("sslmode") != "require" || query.Get("timezone") != "UTC" || query.Get("search_path") != "public" {
|
|
t.Fatal("migration connection differs from supported runtime TLS or time settings")
|
|
}
|
|
if _, err := pq.NewConnector(dsn); err != nil {
|
|
t.Fatal("driver rejected generated connection parameters")
|
|
}
|
|
if name == "" && parsed.Path != "/postgres" {
|
|
t.Fatal("maintenance connection did not select postgres database")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDatabaseCreationErrorClassification(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
kind config.DatabaseType
|
|
err error
|
|
want error
|
|
}{
|
|
{"postgres permission", config.DatabaseTypePostgreSQL, &pq.Error{Code: "42501"}, ErrSQLPermissionDenied},
|
|
{"postgres wrapped permission", config.DatabaseTypePostgreSQL, fmt.Errorf("private detail: %w", &pq.Error{Code: "42501"}), ErrSQLPermissionDenied},
|
|
{"postgres duplicate", config.DatabaseTypePostgreSQL, &pq.Error{Code: "42P04"}, ErrSQLTargetExists},
|
|
{"mysql database permission", config.DatabaseTypeMySQL, &mysql.MySQLError{Number: 1044}, ErrSQLPermissionDenied},
|
|
{"mysql command permission", config.DatabaseTypeMySQL, &mysql.MySQLError{Number: 1142}, ErrSQLPermissionDenied},
|
|
{"mysql privilege", config.DatabaseTypeMySQL, &mysql.MySQLError{Number: 1227}, ErrSQLPermissionDenied},
|
|
{"mysql duplicate", config.DatabaseTypeMySQL, &mysql.MySQLError{Number: 1007}, ErrSQLTargetExists},
|
|
{"authentication", config.DatabaseTypePostgreSQL, &pq.Error{Code: "28P01"}, ErrSQLConnectionUnavailable},
|
|
{"network", config.DatabaseTypePostgreSQL, errors.New("private connection detail"), ErrSQLConnectionUnavailable},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if got := classifyDatabaseCreationError(test.err, test.kind); got != test.want {
|
|
t.Fatalf("classification = %v, want %v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type permissionCatalog struct {
|
|
exists int64
|
|
allowed bool
|
|
queryErr error
|
|
createErr error
|
|
queries int
|
|
creates int
|
|
}
|
|
|
|
func (c *permissionCatalog) Connect(context.Context) (driver.Conn, error) { return c, nil }
|
|
func (c *permissionCatalog) Driver() driver.Driver { return c }
|
|
func (c *permissionCatalog) Open(string) (driver.Conn, error) { return c, nil }
|
|
func (c *permissionCatalog) Close() error { return nil }
|
|
func (c *permissionCatalog) Prepare(string) (driver.Stmt, error) {
|
|
return nil, errors.New("unexpected prepare")
|
|
}
|
|
func (c *permissionCatalog) Begin() (driver.Tx, error) {
|
|
return nil, errors.New("unexpected transaction")
|
|
}
|
|
func (c *permissionCatalog) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
|
switch query {
|
|
case "SELECT COUNT(*) FROM pg_database WHERE datname = $1":
|
|
if len(args) != 1 || args[0].Value != "smail_next" {
|
|
return nil, errors.New("unexpected target name")
|
|
}
|
|
return &permissionCatalogRow{value: c.exists}, nil
|
|
case "SELECT rolcreatedb OR rolsuper FROM pg_roles WHERE rolname = CURRENT_USER":
|
|
c.queries++
|
|
if c.queryErr != nil {
|
|
return nil, c.queryErr
|
|
}
|
|
return &permissionCatalogRow{value: c.allowed}, nil
|
|
default:
|
|
return nil, errors.New("unexpected catalog query")
|
|
}
|
|
}
|
|
func (c *permissionCatalog) ExecContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Result, error) {
|
|
if !strings.HasPrefix(query, "CREATE DATABASE ") {
|
|
return nil, errors.New("unexpected write")
|
|
}
|
|
c.creates++
|
|
return nil, c.createErr
|
|
}
|
|
|
|
type permissionCatalogRow struct {
|
|
value driver.Value
|
|
done bool
|
|
}
|
|
|
|
func (r *permissionCatalogRow) Columns() []string { return []string{"value"} }
|
|
func (r *permissionCatalogRow) Close() error { return nil }
|
|
func (r *permissionCatalogRow) Next(values []driver.Value) error {
|
|
if r.done {
|
|
return io.EOF
|
|
}
|
|
r.done = true
|
|
values[0] = r.value
|
|
return nil
|
|
}
|
|
|
|
func TestPortablePostgresPermissionPreviewIsReadOnly(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
catalog permissionCatalog
|
|
want error
|
|
}{
|
|
{"denied", permissionCatalog{}, ErrSQLPermissionDenied},
|
|
{"allowed", permissionCatalog{allowed: true}, nil},
|
|
{"existing", permissionCatalog{exists: 1}, ErrSQLTargetExists},
|
|
{"catalog unavailable", permissionCatalog{queryErr: errors.New("unavailable")}, ErrSQLConnectionUnavailable},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
root, source := portableFixture(t)
|
|
target := testDatabase(t)
|
|
target.Type = config.DatabaseTypePostgreSQL
|
|
target.Name = "smail_next"
|
|
plan, err := buildTargetPlan(source, target)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
executor := &PortableExecutor{root: root, open: func(context.Context, config.DatabaseConfig, string) (*sql.DB, error) {
|
|
return sql.OpenDB(&test.catalog), nil
|
|
}}
|
|
if err := executor.Preview(context.Background(), plan); !errors.Is(err, test.want) {
|
|
t.Fatalf("preview = %v, want %v", err, test.want)
|
|
}
|
|
if test.catalog.creates != 0 || executor.paused.Load() || executor.pending {
|
|
t.Fatal("preview wrote target or paused source")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPortablePostgresCreationHandlesPermissionRevocation(t *testing.T) {
|
|
catalog := &permissionCatalog{allowed: true, createErr: &pq.Error{Code: "42501", Message: "private database detail"}}
|
|
executor := &PortableExecutor{open: func(context.Context, config.DatabaseConfig, string) (*sql.DB, error) {
|
|
return sql.OpenDB(catalog), nil
|
|
}}
|
|
target := testDatabase(t)
|
|
target.Type = config.DatabaseTypePostgreSQL
|
|
target.Name = "smail_next"
|
|
connection, err := executor.createTarget(context.Background(), target)
|
|
if connection != nil || err != ErrSQLPermissionDenied || catalog.creates != 1 || catalog.queries != 1 {
|
|
t.Fatalf("creation did not preserve permission failure: %v", err)
|
|
}
|
|
}
|