chore: add test coverage for backup hook triggers

This commit is contained in:
garethgeorge
2024-02-03 18:25:57 -08:00
parent f5bb74bf24
commit 2cba3d581a
3 changed files with 174 additions and 0 deletions
+1
View File
@@ -15,6 +15,7 @@ require (
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.18.0
golang.org/x/net v0.20.0
golang.org/x/sync v0.6.0
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe
google.golang.org/grpc v1.61.0
google.golang.org/protobuf v1.32.0
+2
View File
@@ -52,6 +52,8 @@ golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+171
View File
@@ -3,6 +3,9 @@ package api
import (
"context"
"errors"
"fmt"
"os"
"path"
"slices"
"testing"
"time"
@@ -15,6 +18,7 @@ import (
"github.com/garethgeorge/backrest/internal/orchestrator"
"github.com/garethgeorge/backrest/internal/resticinstaller"
"github.com/garethgeorge/backrest/internal/rotatinglog"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/proto"
)
@@ -216,6 +220,172 @@ func TestMultipleBackup(t *testing.T) {
}
}
func TestHookExecution(t *testing.T) {
dir := t.TempDir()
hookOutputBefore := path.Join(dir, "before.txt")
hookOutputAfter := path.Join(dir, "after.txt")
sut := createSystemUnderTest(t, &config.MemoryStore{
Config: &v1.Config{
Modno: 1234,
Repos: []*v1.Repo{
{
Id: "local",
Uri: t.TempDir(),
Password: "test",
},
},
Plans: []*v1.Plan{
{
Id: "test",
Repo: "local",
Paths: []string{
t.TempDir(),
},
Cron: "0 0 1 1 *",
Hooks: []*v1.Hook{
{
Conditions: []v1.Hook_Condition{
v1.Hook_CONDITION_SNAPSHOT_START,
},
Action: &v1.Hook_ActionCommand{
ActionCommand: &v1.Hook_Command{
Command: "echo before > " + hookOutputBefore,
},
},
},
{
Conditions: []v1.Hook_Condition{
v1.Hook_CONDITION_SNAPSHOT_END,
},
Action: &v1.Hook_ActionCommand{
ActionCommand: &v1.Hook_Command{
Command: "echo after > " + hookOutputAfter,
},
},
},
},
},
},
},
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
sut.orch.Run(ctx)
}()
_, err := sut.handler.Backup(context.Background(), connect.NewRequest(&types.StringValue{Value: "test"}))
if err != nil {
t.Fatalf("Backup() error = %v", err)
}
// Wait for two hook operations to appear in the oplog
if err := retry(t, 10, 2*time.Second, func() error {
hookOps := slices.DeleteFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool {
_, ok := op.GetOp().(*v1.Operation_OperationRunHook)
return !ok
})
if len(hookOps) == 2 {
return nil
}
return fmt.Errorf("expected 2 hook operations, got %d", len(hookOps))
}); err != nil {
t.Fatalf("Couldn't find hooks in oplog: %v", err)
}
// expect the hook output files to exist
if _, err := os.Stat(hookOutputBefore); err != nil {
t.Fatalf("hook output file before not found")
}
if _, err := os.Stat(hookOutputAfter); err != nil {
t.Fatalf("hook output file after not found")
}
}
func TestCancelBackup(t *testing.T) {
sut := createSystemUnderTest(t, &config.MemoryStore{
Config: &v1.Config{
Modno: 1234,
Repos: []*v1.Repo{
{
Id: "local",
Uri: t.TempDir(),
Password: "test",
},
},
Plans: []*v1.Plan{
{
Id: "test",
Repo: "local",
Paths: []string{
t.TempDir(),
},
Cron: "0 0 1 1 *",
Retention: &v1.RetentionPolicy{
KeepHourly: 1,
},
},
},
},
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
sut.orch.Run(ctx)
}()
// Start a backup
var errgroup errgroup.Group
errgroup.Go(func() error {
backupReq := connect.NewRequest(&types.StringValue{Value: "test"})
_, err := sut.handler.Backup(context.Background(), backupReq)
if err != nil {
return fmt.Errorf("Backup() error = %v", err)
}
return nil
})
// Find the backup operation ID in the oplog
var backupOpId int64
if err := retry(t, 100, 50*time.Millisecond, func() error {
operations := getOperations(t, sut.oplog)
for _, op := range operations {
_, ok := op.GetOp().(*v1.Operation_OperationBackup)
if ok {
backupOpId = op.Id
return nil
}
}
return errors.New("backup operation not found")
}); err != nil {
t.Fatalf("Couldn't find backup operation in oplog")
}
errgroup.Go(func() error {
if _, err := sut.handler.Cancel(context.Background(), connect.NewRequest(&types.Int64Value{Value: backupOpId})); err != nil {
return fmt.Errorf("Cancel() error = %v, wantErr nil", err)
}
return nil
})
if err := errgroup.Wait(); err != nil {
t.Fatalf(err.Error())
}
// Assert that the backup operation was cancelled
if slices.IndexFunc(getOperations(t, sut.oplog), func(op *v1.Operation) bool {
_, ok := op.GetOp().(*v1.Operation_OperationBackup)
return op.Status == v1.OperationStatus_STATUS_USER_CANCELLED && ok
}) == -1 {
t.Fatalf("Expected a cancelled backup operation in the log")
}
}
type systemUnderTest struct {
handler *BackrestHandler
oplog *oplog.OpLog
@@ -273,6 +443,7 @@ func retry(t *testing.T, times int, backoff time.Duration, f func() error) error
}
func getOperations(t *testing.T, oplog *oplog.OpLog) []*v1.Operation {
t.Logf("Reading oplog")
operations := []*v1.Operation{}
if err := oplog.ForAll(func(op *v1.Operation) error {
operations = append(operations, op)