Рыба проекта. Минимальная функциональность
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pipeline"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
|
||||
)
|
||||
|
||||
// --- Mocks ---
|
||||
|
||||
type mockRecipientPub struct {
|
||||
schemeID uint16
|
||||
keyID []byte
|
||||
raw []byte
|
||||
}
|
||||
|
||||
func (m *mockRecipientPub) SchemeID() uint16 { return m.schemeID }
|
||||
func (m *mockRecipientPub) KeyID() []byte { return m.keyID }
|
||||
func (m *mockRecipientPub) Raw() []byte { return m.raw }
|
||||
|
||||
type mockKeyManager struct {
|
||||
loadPubCalls []loadPubCall
|
||||
pub crypto.RecipientPub
|
||||
}
|
||||
|
||||
type loadPubCall struct {
|
||||
path string
|
||||
schemeID uint16
|
||||
}
|
||||
|
||||
func (m *mockKeyManager) LoadPub(
|
||||
path string,
|
||||
schemeID uint16,
|
||||
) (
|
||||
crypto.RecipientPub,
|
||||
error,
|
||||
) {
|
||||
m.loadPubCalls = append(
|
||||
m.loadPubCalls,
|
||||
loadPubCall{path: path, schemeID: schemeID},
|
||||
)
|
||||
return m.pub, nil
|
||||
}
|
||||
|
||||
func (m *mockKeyManager) LoadPriv(
|
||||
path string,
|
||||
schemeID uint16,
|
||||
) (
|
||||
crypto.RecipientPriv,
|
||||
error,
|
||||
) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockKeyManager) Generate(
|
||||
schemeID uint16,
|
||||
pubOut io.Writer,
|
||||
privOut io.Writer,
|
||||
rand io.Reader,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type successDumper struct {
|
||||
data []byte
|
||||
receivedOpts pgdump.Options
|
||||
}
|
||||
|
||||
func (d *successDumper) Dump(
|
||||
ctx context.Context,
|
||||
opts pgdump.Options,
|
||||
sink io.Writer,
|
||||
) error {
|
||||
d.receivedOpts = opts
|
||||
if len(d.data) > 0 {
|
||||
_, err := sink.Write(d.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if closer, ok := sink.(io.Closer); ok {
|
||||
_ = closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type failDumper struct{}
|
||||
|
||||
func (d *failDumper) Dump(
|
||||
ctx context.Context,
|
||||
opts pgdump.Options,
|
||||
sink io.Writer,
|
||||
) error {
|
||||
return pgdump.ErrPgDumpFailed(1)
|
||||
}
|
||||
|
||||
type passthroughEncryptor struct{}
|
||||
|
||||
func (e *passthroughEncryptor) Encrypt(
|
||||
plaintext io.Reader,
|
||||
recipients []crypto.RecipientPub,
|
||||
sink io.Writer,
|
||||
rand io.Reader,
|
||||
) error {
|
||||
_, err := io.Copy(sink, plaintext)
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func setBackupFlags(
|
||||
cmd *cobra.Command,
|
||||
backupDir string,
|
||||
pqPath string,
|
||||
classicalPath string,
|
||||
) {
|
||||
config.RegisterFlags(cmd)
|
||||
_ = cmd.Flags().Set("backup-dir", backupDir)
|
||||
_ = cmd.Flags().Set("pq-public-key-path", pqPath)
|
||||
_ = cmd.Flags().Set("classical-public-key-path", classicalPath)
|
||||
_ = cmd.Flags().Set("pg-host", "localhost")
|
||||
_ = cmd.Flags().Set("pg-port", "5432")
|
||||
_ = cmd.Flags().Set("pg-user", "testuser")
|
||||
_ = cmd.Flags().Set("pg-password", "testpass")
|
||||
_ = cmd.Flags().Set("pg-database", "testdb")
|
||||
}
|
||||
|
||||
func restoreGlobals(t *testing.T) {
|
||||
originalNewKeyManager := newKeyManager
|
||||
originalNewRunner := newRunner
|
||||
originalOutputWriter := outputWriter
|
||||
t.Cleanup(func() {
|
||||
newKeyManager = originalNewKeyManager
|
||||
newRunner = originalNewRunner
|
||||
outputWriter = originalOutputWriter
|
||||
})
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
func TestBackupCmd_Structure(t *testing.T) {
|
||||
if backupCmd == nil {
|
||||
t.Fatal("backupCmd is nil")
|
||||
}
|
||||
if backupCmd.Use != "backup" {
|
||||
t.Fatalf("expected Use='backup', got %q", backupCmd.Use)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupCmd_Success(t *testing.T) {
|
||||
restoreGlobals(t)
|
||||
|
||||
backupDir := t.TempDir()
|
||||
pqPath := filepath.Join(backupDir, "pq.pub")
|
||||
classicalPath := filepath.Join(backupDir, "classical.pub")
|
||||
|
||||
_ = os.WriteFile(pqPath, []byte("pq"), 0o644)
|
||||
_ = os.WriteFile(classicalPath, []byte("classical"), 0o644)
|
||||
|
||||
mockKM := &mockKeyManager{
|
||||
pub: &mockRecipientPub{
|
||||
schemeID: 0x0006,
|
||||
keyID: make([]byte, 8),
|
||||
raw: make([]byte, 32),
|
||||
},
|
||||
}
|
||||
newKeyManager = func(crypto.Registry) crypto.KeyManager {
|
||||
return mockKM
|
||||
}
|
||||
|
||||
testData := []byte("test backup payload")
|
||||
dumper := &successDumper{data: testData}
|
||||
newRunner = func(...pipeline.Option) pipelineRunner {
|
||||
return pipeline.NewRunner(
|
||||
pipeline.WithDumper(dumper),
|
||||
pipeline.WithEncryptor(&passthroughEncryptor{}),
|
||||
)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
outputWriter = &buf
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
setBackupFlags(cmd, backupDir, pqPath, classicalPath)
|
||||
_ = cmd.Flags().Set("backup-retention-days", "1")
|
||||
|
||||
oldFile := filepath.Join(backupDir, "synapse-20230101-000000.dump.pqenc")
|
||||
_ = os.WriteFile(oldFile, []byte("old"), 0o644)
|
||||
oldTime := time.Now().Add(-48 * time.Hour)
|
||||
_ = os.Chtimes(oldFile, oldTime, oldTime)
|
||||
|
||||
err := backupCmd.RunE(cmd, []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(mockKM.loadPubCalls) != 2 {
|
||||
t.Fatalf("expected 2 LoadPub calls, got %d", len(mockKM.loadPubCalls))
|
||||
}
|
||||
if mockKM.loadPubCalls[0].path != pqPath ||
|
||||
mockKM.loadPubCalls[0].schemeID != 0x0006 {
|
||||
t.Fatalf("unexpected PQ LoadPub call: %+v", mockKM.loadPubCalls[0])
|
||||
}
|
||||
if mockKM.loadPubCalls[1].path != classicalPath ||
|
||||
mockKM.loadPubCalls[1].schemeID != 0x0007 {
|
||||
t.Fatalf(
|
||||
"unexpected classical LoadPub call: %+v",
|
||||
mockKM.loadPubCalls[1],
|
||||
)
|
||||
}
|
||||
|
||||
if dumper.receivedOpts.Host != "localhost" {
|
||||
t.Fatalf("unexpected host: %q", dumper.receivedOpts.Host)
|
||||
}
|
||||
if dumper.receivedOpts.Port != 5432 {
|
||||
t.Fatalf("unexpected port: %d", dumper.receivedOpts.Port)
|
||||
}
|
||||
if dumper.receivedOpts.Database != "testdb" {
|
||||
t.Fatalf("unexpected database: %q", dumper.receivedOpts.Database)
|
||||
}
|
||||
if dumper.receivedOpts.User != "testuser" {
|
||||
t.Fatalf("unexpected user: %q", dumper.receivedOpts.User)
|
||||
}
|
||||
if dumper.receivedOpts.Password != "testpass" {
|
||||
t.Fatalf("unexpected password: %q", dumper.receivedOpts.Password)
|
||||
}
|
||||
|
||||
key := dumper.receivedOpts.Key
|
||||
matched, _ := regexp.MatchString(
|
||||
`^synapse-\d{8}-\d{6}\.dump\.pqenc$`,
|
||||
key,
|
||||
)
|
||||
if !matched {
|
||||
t.Fatalf("unexpected key format: %q", key)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(backupDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var foundFinal int
|
||||
var foundTmp int
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if strings.HasSuffix(name, ".dump.pqenc") {
|
||||
foundFinal++
|
||||
}
|
||||
if strings.HasSuffix(name, ".tmp") {
|
||||
foundTmp++
|
||||
}
|
||||
}
|
||||
if foundFinal != 1 {
|
||||
t.Fatalf("expected 1 final .pqenc file, found %d", foundFinal)
|
||||
}
|
||||
if foundTmp != 0 {
|
||||
t.Fatalf("expected 0 .tmp files, found %d", foundTmp)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(oldFile); !os.IsNotExist(err) {
|
||||
t.Fatal("expected old file to be pruned by retention")
|
||||
}
|
||||
|
||||
logStr := buf.String()
|
||||
if !strings.Contains(logStr, "run_id=") {
|
||||
t.Fatal("expected log to contain run_id")
|
||||
}
|
||||
if !strings.Contains(logStr, "start_time=") {
|
||||
t.Fatal("expected log to contain start_time")
|
||||
}
|
||||
if !strings.Contains(logStr, "end_time=") {
|
||||
t.Fatal("expected log to contain end_time")
|
||||
}
|
||||
if !strings.Contains(logStr, "byte_count=") {
|
||||
t.Fatal("expected log to contain byte_count")
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`byte_count=(\d+)`)
|
||||
matches := re.FindAllStringSubmatch(logStr, -1)
|
||||
if len(matches) == 0 {
|
||||
t.Fatal("expected log to contain byte_count value")
|
||||
}
|
||||
lastMatch := matches[len(matches)-1][1]
|
||||
if lastMatch == "0" {
|
||||
t.Fatal("expected non-zero byte_count for successful backup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupCmd_PgDumpFailure(t *testing.T) {
|
||||
restoreGlobals(t)
|
||||
|
||||
backupDir := t.TempDir()
|
||||
pqPath := filepath.Join(backupDir, "pq.pub")
|
||||
classicalPath := filepath.Join(backupDir, "classical.pub")
|
||||
|
||||
_ = os.WriteFile(pqPath, []byte("pq"), 0o644)
|
||||
_ = os.WriteFile(classicalPath, []byte("classical"), 0o644)
|
||||
|
||||
mockKM := &mockKeyManager{
|
||||
pub: &mockRecipientPub{
|
||||
schemeID: 0x0006,
|
||||
keyID: make([]byte, 8),
|
||||
raw: make([]byte, 32),
|
||||
},
|
||||
}
|
||||
newKeyManager = func(crypto.Registry) crypto.KeyManager {
|
||||
return mockKM
|
||||
}
|
||||
|
||||
newRunner = func(...pipeline.Option) pipelineRunner {
|
||||
return pipeline.NewRunner(
|
||||
pipeline.WithDumper(&failDumper{}),
|
||||
pipeline.WithEncryptor(&passthroughEncryptor{}),
|
||||
)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
outputWriter = &buf
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
setBackupFlags(cmd, backupDir, pqPath, classicalPath)
|
||||
|
||||
err := backupCmd.RunE(cmd, []string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected non-nil error")
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(backupDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if strings.HasSuffix(name, ".pqenc") || strings.HasSuffix(name, ".tmp") {
|
||||
t.Fatalf("unexpected file after failure: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
logStr := buf.String()
|
||||
if !strings.Contains(logStr, "backup failed") {
|
||||
t.Fatal("expected 'backup failed' in log")
|
||||
}
|
||||
if !strings.Contains(logStr, "run_id=") {
|
||||
t.Fatal("expected log to contain run_id")
|
||||
}
|
||||
if !strings.Contains(logStr, "start_time=") {
|
||||
t.Fatal("expected log to contain start_time")
|
||||
}
|
||||
if !strings.Contains(logStr, "end_time=") {
|
||||
t.Fatal("expected log to contain end_time")
|
||||
}
|
||||
if !strings.Contains(logStr, "byte_count=0") {
|
||||
t.Fatal("expected log to contain byte_count=0 for failed backup")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user