mirror of
https://github.com/TronoSfera/backupy-agent.git
synced 2026-05-18 18:13:30 +03:00
The Alpine sqlite3 binary refuses to open /dev/stdout when running as
a non-root uid in a container ('Error: cannot open "/dev/stdout"'),
which breaks every backup attempt. Switch the dump path to stage the
snapshot in a temp file, then stream that file through gzip into the
pipeline. Adds streamSideEffect to the test mockRunner so the existing
gzip-wrap test can simulate the sqlite3 process writing to its
destination path.
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
backupv1 "github.com/backupy/backupy/packages/proto/gen/go/backupv1"
|
|
)
|
|
|
|
// mockRunner is a deterministic cmdRunner for the pg_dump / mysqldump
|
|
// drivers. It records the (name, args, env) tuples and replays the
|
|
// canned stdout / stderr the test sets up.
|
|
type mockRunner struct {
|
|
outputResp map[string][]byte // key = first arg (e.g. "--version")
|
|
streamResp []byte
|
|
streamErr error
|
|
// streamSideEffect, when non-nil, runs before streamResp is written
|
|
// to the supplied writer. Lets sqlite-style drivers that write to a
|
|
// real file-system path simulate the file write.
|
|
streamSideEffect func(args []string) error
|
|
calls []mockCall
|
|
}
|
|
|
|
type mockCall struct {
|
|
Args []string
|
|
Env []string
|
|
}
|
|
|
|
func (m *mockRunner) Output(_ context.Context, _ string, args []string, env []string) ([]byte, error) {
|
|
m.calls = append(m.calls, mockCall{Args: append([]string(nil), args...), Env: append([]string(nil), env...)})
|
|
if len(args) == 0 {
|
|
return nil, nil
|
|
}
|
|
if v, ok := m.outputResp[args[0]]; ok {
|
|
return v, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockRunner) RunStream(_ context.Context, _ string, args []string, env []string, out io.Writer) error {
|
|
m.calls = append(m.calls, mockCall{Args: append([]string(nil), args...), Env: append([]string(nil), env...)})
|
|
if m.streamErr != nil {
|
|
return m.streamErr
|
|
}
|
|
if m.streamSideEffect != nil {
|
|
if err := m.streamSideEffect(args); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(m.streamResp) > 0 {
|
|
_, _ = out.Write(m.streamResp)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestParsePgDumpVersion(t *testing.T) {
|
|
got := parsePgDumpVersion("pg_dump (PostgreSQL) 16.2")
|
|
require.Equal(t, "PostgreSQL 16.2", got)
|
|
|
|
got = parsePgDumpVersion("pg_dump (PostgreSQL) 16.2 (Debian 16.2-1.pgdg120+1)")
|
|
require.Contains(t, got, "PostgreSQL 16.2")
|
|
}
|
|
|
|
func TestIsPgDumpMagic(t *testing.T) {
|
|
require.True(t, IsPgDumpMagic([]byte("PGDMP\x00")))
|
|
require.False(t, IsPgDumpMagic([]byte("NOTHING")))
|
|
require.False(t, IsPgDumpMagic(nil))
|
|
}
|
|
|
|
func TestPgDump_DumpWritesPayload(t *testing.T) {
|
|
mock := &mockRunner{
|
|
outputResp: map[string][]byte{"--version": []byte("pg_dump (PostgreSQL) 16.2")},
|
|
streamResp: []byte("PGDMP\x01\x02\x03"),
|
|
}
|
|
d := &pgDump{binary: "pg_dump", runner: mock}
|
|
target := &backupv1.Target{
|
|
Type: backupv1.DbType_POSTGRESQL,
|
|
Connection: &backupv1.ConnectionConfig{
|
|
Host: "h", Port: 5432, Database: "db", Username: "u", PasswordSecretRef: "p",
|
|
},
|
|
}
|
|
var buf testWriter
|
|
info, err := d.Dump(context.Background(), target, &buf)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "PostgreSQL 16.2", info.EngineVersion)
|
|
require.Equal(t, []byte("PGDMP\x01\x02\x03"), buf.Bytes())
|
|
|
|
// Confirm PGPASSWORD env is propagated.
|
|
require.NotEmpty(t, mock.calls)
|
|
streamCall := mock.calls[0]
|
|
require.Contains(t, streamCall.Env, "PGPASSWORD=p")
|
|
}
|
|
|
|
// testWriter is a tiny bytes.Buffer alternative that exposes Bytes() and
|
|
// satisfies io.Writer without pulling bytes into the file's imports.
|
|
type testWriter struct{ b []byte }
|
|
|
|
func (t *testWriter) Write(p []byte) (int, error) {
|
|
t.b = append(t.b, p...)
|
|
return len(p), nil
|
|
}
|
|
func (t *testWriter) Bytes() []byte { return t.b }
|