Рыба проекта. Минимальная функциональность
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/composite"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/keymanager"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519"
|
||||
adapterpgdump "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pgdump"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pipeline"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/retention"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/storage/local"
|
||||
domaincrypto "git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
||||
domainpgdump "git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
|
||||
)
|
||||
|
||||
func RunOnce(ctx context.Context, cfg *config.Config) error {
|
||||
registry := domaincrypto.NewRegistry()
|
||||
if err := registry.Register(0x0006, func() domaincrypto.KEM { return mlkem768.New() }); err != nil {
|
||||
return fmt.Errorf("register mlkem768: %w", err)
|
||||
}
|
||||
if err := registry.Register(0x0007, func() domaincrypto.KEM { return x25519.New() }); err != nil {
|
||||
return fmt.Errorf("register x25519: %w", err)
|
||||
}
|
||||
|
||||
keyMgr := keymanager.NewKeyManager(registry)
|
||||
|
||||
pqPub, err := keyMgr.LoadPub(cfg.PQPublicKeyPath, cfg.PQScheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load pq public key: %w", err)
|
||||
}
|
||||
|
||||
classicalPub, err := keyMgr.LoadPub(cfg.ClassicalPublicKeyPath, cfg.ClassicalScheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load classical public key: %w", err)
|
||||
}
|
||||
|
||||
recipients := []domaincrypto.RecipientPub{pqPub, classicalPub}
|
||||
|
||||
encryptor := composite.NewEncryptor(registry)
|
||||
dumper := adapterpgdump.New()
|
||||
sink := local.NewLocalSink(cfg.Backup.Dir)
|
||||
|
||||
runner := pipeline.NewRunner(
|
||||
pipeline.WithDumper(dumper),
|
||||
pipeline.WithEncryptor(encryptor),
|
||||
)
|
||||
|
||||
now := time.Now()
|
||||
timestamp := now.UTC().Format("20060102-150405")
|
||||
pgDumpOpts := domainpgdump.Options{
|
||||
Host: cfg.PG.Host,
|
||||
Port: cfg.PG.Port,
|
||||
Database: cfg.PG.Database,
|
||||
User: cfg.PG.User,
|
||||
Password: cfg.PG.Password,
|
||||
ExcludeTables: cfg.PG.ExcludeTables,
|
||||
Key: fmt.Sprintf("synapse-%s.dump.pqenc", timestamp),
|
||||
}
|
||||
|
||||
if err := runner.Run(ctx, pgDumpOpts, recipients, sink, rand.Reader); err != nil {
|
||||
return fmt.Errorf("backup pipeline failed: %w", err)
|
||||
}
|
||||
|
||||
if _, err := retention.PruneByAge(ctx, cfg.Backup.Dir, cfg.Backup.RetentionDays, now); err != nil {
|
||||
return fmt.Errorf("retention pruning failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/keymanager"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
||||
)
|
||||
|
||||
// TestRunOnce_MissingKeys verifies that RunOnce surfaces a configuration error
|
||||
// when the public key paths are not provided, exercising the early validation
|
||||
// path (registry build, keymanager construction, key load).
|
||||
func TestRunOnce_MissingKeys(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
// PQPublicKeyPath and ClassicalPublicKeyPath intentionally left empty.
|
||||
|
||||
err := RunOnce(context.Background(), cfg)
|
||||
if err == nil {
|
||||
t.Fatal("RunOnce returned nil error with empty config, want error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "load pq public key") {
|
||||
t.Fatalf("RunOnce error = %q, want it to mention %q", err, "load pq public key")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunOnce_PQKeyNotFound verifies the error returned when the post-quantum
|
||||
// public key path is set but the file does not exist on disk.
|
||||
func TestRunOnce_PQKeyNotFound(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
PQScheme: 0x0006,
|
||||
// Path points to a file that does not exist; use t.TempDir() to keep
|
||||
// the test hermetic regardless of the working directory.
|
||||
PQPublicKeyPath: t.TempDir() + "/does-not-exist-pq.pem",
|
||||
ClassicalPublicKeyPath: t.TempDir() + "/does-not-exist-classical.pem",
|
||||
}
|
||||
|
||||
err := RunOnce(context.Background(), cfg)
|
||||
if err == nil {
|
||||
t.Fatal("RunOnce returned nil error when keys are missing on disk, want error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "load pq public key") {
|
||||
t.Fatalf("RunOnce error = %q, want it to mention %q", err, "load pq public key")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunOnce_ValidKeys_PgDumpMissing generates real dual-KEM keys, builds a
|
||||
// fully valid config, and calls RunOnce. Since pg_dump is not installed in the
|
||||
// test environment, the pipeline fails at the dump step, giving coverage of
|
||||
// the full orchestration path (registry, keymanager, encryptor, sink, runner,
|
||||
// pgDumpOpts assembly) while still asserting the expected error.
|
||||
func TestRunOnce_ValidKeys_PgDumpMissing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
keyDir := dir + "/keys"
|
||||
backupDir := dir + "/backups"
|
||||
_ = os.MkdirAll(keyDir, 0o755)
|
||||
_ = os.MkdirAll(backupDir, 0o755)
|
||||
|
||||
reg := crypto.NewRegistry()
|
||||
_ = reg.Register(0x0006, func() crypto.KEM { return mlkem768.New() })
|
||||
_ = reg.Register(0x0007, func() crypto.KEM { return x25519.New() })
|
||||
|
||||
km := keymanager.NewKeyManager(reg)
|
||||
|
||||
pqPubFile, _ := os.Create(keyDir + "/pq.pub.pem")
|
||||
pqPrivFile, _ := os.Create(keyDir + "/pq.priv.pem")
|
||||
_ = km.Generate(0x0006, pqPubFile, pqPrivFile, nil)
|
||||
_ = pqPubFile.Close()
|
||||
_ = pqPrivFile.Close()
|
||||
|
||||
classicalPubFile, _ := os.Create(keyDir + "/classical.pub.pem")
|
||||
classicalPrivFile, _ := os.Create(keyDir + "/classical.priv.pem")
|
||||
_ = km.Generate(0x0007, classicalPubFile, classicalPrivFile, nil)
|
||||
_ = classicalPubFile.Close()
|
||||
_ = classicalPrivFile.Close()
|
||||
|
||||
cfg := &config.Config{
|
||||
PQScheme: 0x0006,
|
||||
ClassicalScheme: 0x0007,
|
||||
PQPublicKeyPath: keyDir + "/pq.pub.pem",
|
||||
ClassicalPublicKeyPath: keyDir + "/classical.pub.pem",
|
||||
Backup: struct {
|
||||
Dir string `mapstructure:"dir"`
|
||||
RetentionDays int `mapstructure:"retention_days"`
|
||||
Cron string `mapstructure:"cron"`
|
||||
}{
|
||||
Dir: backupDir,
|
||||
RetentionDays: 180,
|
||||
},
|
||||
PG: struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
Database string `mapstructure:"database"`
|
||||
SSLMode string `mapstructure:"sslmode"`
|
||||
ExcludeTables []string `mapstructure:"exclude_tables"`
|
||||
}{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "test",
|
||||
Password: "test",
|
||||
Database: "test",
|
||||
ExcludeTables: []string{"e2e_one_time_keys_json"},
|
||||
},
|
||||
}
|
||||
|
||||
err := RunOnce(context.Background(), cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error because pg_dump is not installed in the test environment")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "backup pipeline failed") {
|
||||
t.Fatalf("expected error containing 'backup pipeline failed', got: %v", err)
|
||||
}
|
||||
|
||||
entries, _ := os.ReadDir(backupDir)
|
||||
for _, e := range entries {
|
||||
if strings.HasSuffix(e.Name(), ".tmp") {
|
||||
t.Fatalf("unexpected .tmp file after failed backup: %s", e.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user