Рыба проекта. Минимальная функциональность

This commit is contained in:
2026-08-03 22:22:24 +03:00
commit 8c8631ac9c
80 changed files with 10618 additions and 0 deletions
+374
View File
@@ -0,0 +1,374 @@
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)
}
}