375 lines
7.5 KiB
Go
375 lines
7.5 KiB
Go
package pgdump
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
|
|
)
|
|
|
|
// mockCommandContext creates a test helper that asserts the command name and args,
|
|
// and returns a shell command that produces the given stdout, stderr, and exit code.
|
|
func mockCommandContext(
|
|
t *testing.T,
|
|
wantName string,
|
|
wantArgs []string,
|
|
stdout string,
|
|
stderr string,
|
|
exitCode int,
|
|
) func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
return func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
if name != wantName {
|
|
t.Errorf("command name = %q, want %q", name, wantName)
|
|
}
|
|
if !reflect.DeepEqual(arg, wantArgs) {
|
|
t.Errorf("args = %v, want %v", arg, wantArgs)
|
|
}
|
|
script := fmt.Sprintf(
|
|
"printf '%%s' '%s'; printf '%%s' '%s' >&2; exit %d",
|
|
stdout,
|
|
stderr,
|
|
exitCode,
|
|
)
|
|
return exec.CommandContext(ctx, "sh", "-c", script)
|
|
}
|
|
}
|
|
|
|
func TestDump_Success(t *testing.T) {
|
|
wantArgs := []string{
|
|
"--format=custom",
|
|
"--exclude-table=e2e_one_time_keys_json",
|
|
}
|
|
|
|
adapter := &adapter{
|
|
commandContext: mockCommandContext(
|
|
t,
|
|
"pg_dump",
|
|
wantArgs,
|
|
"dumpdata",
|
|
"",
|
|
0,
|
|
),
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{Database: "testdb"},
|
|
&buf,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if buf.String() != "dumpdata" {
|
|
t.Errorf("output = %q, want %q", buf.String(), "dumpdata")
|
|
}
|
|
}
|
|
|
|
func TestDump_WaitAfterStdoutEOF(t *testing.T) {
|
|
// This test verifies that after io.Copy returns (stdout EOF),
|
|
// cmd.Wait() is called and the exit code is verified before returning.
|
|
wantArgs := []string{
|
|
"--format=custom",
|
|
"--exclude-table=e2e_one_time_keys_json",
|
|
}
|
|
|
|
adapter := &adapter{
|
|
commandContext: mockCommandContext(
|
|
t,
|
|
"pg_dump",
|
|
wantArgs,
|
|
"dumpdata",
|
|
"",
|
|
0,
|
|
),
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{Database: "testdb"},
|
|
&buf,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if buf.String() != "dumpdata" {
|
|
t.Errorf("output = %q, want %q", buf.String(), "dumpdata")
|
|
}
|
|
}
|
|
|
|
func TestDump_NonZeroExitCode(t *testing.T) {
|
|
wantArgs := []string{
|
|
"--format=custom",
|
|
"--exclude-table=e2e_one_time_keys_json",
|
|
}
|
|
|
|
adapter := &adapter{
|
|
commandContext: mockCommandContext(
|
|
t,
|
|
"pg_dump",
|
|
wantArgs,
|
|
"",
|
|
"stderr error message",
|
|
1,
|
|
),
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{Database: "testdb"},
|
|
&buf,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
|
|
if !errors.Is(err, pgdump.ErrPgDumpFailed(1)) {
|
|
t.Errorf("error = %v, want ErrPgDumpFailed(1)", err)
|
|
}
|
|
}
|
|
|
|
func TestDump_StderrCaptured(t *testing.T) {
|
|
wantStderr := "stderr captured"
|
|
var capturedCmd *exec.Cmd
|
|
|
|
adapter := &adapter{
|
|
commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
script := fmt.Sprintf("printf '%%s' '%s' >&2; exit 0", wantStderr)
|
|
capturedCmd = exec.CommandContext(ctx, "sh", "-c", script)
|
|
return capturedCmd
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{Database: "testdb"},
|
|
&buf,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if capturedCmd.Stderr == nil {
|
|
t.Fatal("expected cmd.Stderr to be set, got nil")
|
|
}
|
|
|
|
stderrBuilder, ok := capturedCmd.Stderr.(*strings.Builder)
|
|
if !ok {
|
|
t.Fatalf("expected cmd.Stderr to be *strings.Builder, got %T", capturedCmd.Stderr)
|
|
}
|
|
|
|
if stderrBuilder.String() != wantStderr {
|
|
t.Errorf("stderr = %q, want %q", stderrBuilder.String(), wantStderr)
|
|
}
|
|
}
|
|
|
|
func TestDump_ContextCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
|
|
adapter := &adapter{
|
|
commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
return exec.CommandContext(ctx, "sh", "-c", "while :; do :; done")
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := adapter.Dump(ctx, pgdump.Options{Database: "testdb"}, &buf)
|
|
if err == nil {
|
|
t.Fatal("expected error due to context cancellation, got nil")
|
|
}
|
|
|
|
// Accept either context deadline exceeded or signal killed.
|
|
if !errors.Is(err, context.DeadlineExceeded) && !strings.Contains(err.Error(), "signal") {
|
|
t.Logf("got error: %v (acceptable variants: context.DeadlineExceeded or signal killed)", err)
|
|
}
|
|
}
|
|
|
|
func TestDump_DefaultExcludeTables(t *testing.T) {
|
|
wantArgs := []string{
|
|
"--format=custom",
|
|
"--exclude-table=e2e_one_time_keys_json",
|
|
}
|
|
|
|
adapter := &adapter{
|
|
commandContext: mockCommandContext(
|
|
t,
|
|
"pg_dump",
|
|
wantArgs,
|
|
"",
|
|
"",
|
|
0,
|
|
),
|
|
}
|
|
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{Database: "testdb"},
|
|
io.Discard,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDump_CustomExcludeTables(t *testing.T) {
|
|
wantArgs := []string{
|
|
"--format=custom",
|
|
"--exclude-table=table_a",
|
|
"--exclude-table=table_b",
|
|
}
|
|
|
|
adapter := &adapter{
|
|
commandContext: mockCommandContext(
|
|
t,
|
|
"pg_dump",
|
|
wantArgs,
|
|
"",
|
|
"",
|
|
0,
|
|
),
|
|
}
|
|
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{
|
|
Database: "testdb",
|
|
ExcludeTables: []string{"table_a", "table_b"},
|
|
},
|
|
io.Discard,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDump_EnvVars(t *testing.T) {
|
|
var capturedCmd *exec.Cmd
|
|
|
|
adapter := &adapter{
|
|
commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
capturedCmd = exec.CommandContext(ctx, "sh", "-c", "exit 0")
|
|
return capturedCmd
|
|
},
|
|
}
|
|
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{
|
|
Host: "myhost",
|
|
Port: 5433,
|
|
User: "myuser",
|
|
Password: "mypass",
|
|
Database: "mydb",
|
|
},
|
|
io.Discard,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
envStr := strings.Join(capturedCmd.Env, "\n")
|
|
wantEnvVars := []string{
|
|
"PGHOST=myhost",
|
|
"PGPORT=5433",
|
|
"PGUSER=myuser",
|
|
"PGPASSWORD=mypass",
|
|
"PGDATABASE=mydb",
|
|
}
|
|
for _, wantEnv := range wantEnvVars {
|
|
if !strings.Contains(envStr, wantEnv) {
|
|
t.Errorf("env missing %q", wantEnv)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDump_PipeWriterClosed(t *testing.T) {
|
|
adapter := &adapter{
|
|
commandContext: mockCommandContext(
|
|
t,
|
|
"pg_dump",
|
|
[]string{
|
|
"--format=custom",
|
|
"--exclude-table=e2e_one_time_keys_json",
|
|
},
|
|
"pipe data",
|
|
"",
|
|
0,
|
|
),
|
|
}
|
|
|
|
pr, pw := io.Pipe()
|
|
readDone := make(chan struct{})
|
|
var readData []byte
|
|
var readErr error
|
|
|
|
go func() {
|
|
readData, readErr = io.ReadAll(pr)
|
|
close(readDone)
|
|
}()
|
|
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{Database: "testdb"},
|
|
pw,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
<-readDone
|
|
if readErr != nil {
|
|
t.Fatalf("read error: %v", readErr)
|
|
}
|
|
if string(readData) != "pipe data" {
|
|
t.Errorf("read data = %q, want %q", string(readData), "pipe data")
|
|
}
|
|
}
|
|
|
|
func TestDump_PipeWriterClosedWithError(t *testing.T) {
|
|
adapter := &adapter{
|
|
commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
return exec.CommandContext(ctx, "sh", "-c", "exit 1")
|
|
},
|
|
}
|
|
|
|
pr, pw := io.Pipe()
|
|
readDone := make(chan struct{})
|
|
var readErr error
|
|
|
|
go func() {
|
|
_, readErr = io.ReadAll(pr)
|
|
close(readDone)
|
|
}()
|
|
|
|
err := adapter.Dump(
|
|
context.Background(),
|
|
pgdump.Options{Database: "testdb"},
|
|
pw,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
|
|
<-readDone
|
|
if readErr == nil {
|
|
t.Fatal("expected read error due to pipe close with error, got nil")
|
|
}
|
|
if !errors.Is(readErr, pgdump.ErrPgDumpFailed(1)) {
|
|
t.Errorf("read error = %v, want ErrPgDumpFailed(1)", readErr)
|
|
}
|
|
}
|