Files
2026-05-02 22:29:39 -07:00

236 lines
7.5 KiB
Go

package config
import (
"testing"
v1 "github.com/garethgeorge/backrest/gen/go/v1"
)
func TestCleanupOrphanedRemoteReposAndPlans(t *testing.T) {
tests := []struct {
name string
config *v1.Config
wantRepoIDs []string
wantPlanIDs []string
}{
{
name: "no remote repos, nothing removed",
config: &v1.Config{
Repos: []*v1.Repo{
{Id: "local-repo", Uri: "file:///tmp/repo", Guid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
},
Plans: []*v1.Plan{
{Id: "plan1", Repo: "local-repo", Paths: []string{"/data"}},
},
},
wantRepoIDs: []string{"local-repo"},
wantPlanIDs: []string{"plan1"},
},
{
name: "remote repo with valid peer is kept",
config: &v1.Config{
Repos: []*v1.Repo{
{Id: "local-repo", Uri: "file:///tmp/repo", Guid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
{Id: "remote-repo", Uri: "file:///tmp/remote", Guid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", OriginInstanceId: "server-a"},
},
Plans: []*v1.Plan{
{Id: "plan1", Repo: "local-repo", Paths: []string{"/data"}},
{Id: "plan2", Repo: "remote-repo", Paths: []string{"/data"}},
},
Multihost: &v1.Multihost{
KnownHosts: []*v1.Multihost_Peer{
{InstanceId: "server-a", Keyid: "key-a", InstanceUrl: "http://server-a:9898"},
},
},
},
wantRepoIDs: []string{"local-repo", "remote-repo"},
wantPlanIDs: []string{"plan1", "plan2"},
},
{
name: "remote repo orphaned when peer removed",
config: &v1.Config{
Repos: []*v1.Repo{
{Id: "local-repo", Uri: "file:///tmp/repo", Guid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
{Id: "remote-repo", Uri: "file:///tmp/remote", Guid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", OriginInstanceId: "server-a"},
},
Plans: []*v1.Plan{
{Id: "plan1", Repo: "local-repo", Paths: []string{"/data"}},
{Id: "plan2", Repo: "remote-repo", Paths: []string{"/data"}},
},
Multihost: &v1.Multihost{},
},
wantRepoIDs: []string{"local-repo"},
wantPlanIDs: []string{"plan1"},
},
{
name: "authorized client peer keeps remote repo",
config: &v1.Config{
Repos: []*v1.Repo{
{Id: "remote-repo", Uri: "file:///tmp/remote", Guid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", OriginInstanceId: "client-b"},
},
Plans: []*v1.Plan{
{Id: "plan1", Repo: "remote-repo", Paths: []string{"/data"}},
},
Multihost: &v1.Multihost{
AuthorizedClients: []*v1.Multihost_Peer{
{InstanceId: "client-b", Keyid: "key-b"},
},
},
},
wantRepoIDs: []string{"remote-repo"},
wantPlanIDs: []string{"plan1"},
},
{
name: "multiple orphaned repos and plans cleaned up",
config: &v1.Config{
Repos: []*v1.Repo{
{Id: "local", Uri: "file:///tmp/repo", Guid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
{Id: "remote-a", Uri: "file:///tmp/a", Guid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", OriginInstanceId: "gone-server"},
{Id: "remote-b", Uri: "file:///tmp/b", Guid: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", OriginInstanceId: "gone-server"},
},
Plans: []*v1.Plan{
{Id: "local-plan", Repo: "local", Paths: []string{"/data"}},
{Id: "plan-a", Repo: "remote-a", Paths: []string{"/data"}},
{Id: "plan-b", Repo: "remote-b", Paths: []string{"/data"}},
},
Multihost: &v1.Multihost{},
},
wantRepoIDs: []string{"local"},
wantPlanIDs: []string{"local-plan"},
},
{
name: "plan referencing local repo not removed even if remote repos cleaned",
config: &v1.Config{
Repos: []*v1.Repo{
{Id: "local", Uri: "file:///tmp/repo", Guid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
{Id: "remote", Uri: "file:///tmp/remote", Guid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", OriginInstanceId: "gone"},
},
Plans: []*v1.Plan{
{Id: "kept-plan", Repo: "local", Paths: []string{"/data"}},
{Id: "removed-plan", Repo: "remote", Paths: []string{"/data"}},
},
Multihost: &v1.Multihost{},
},
wantRepoIDs: []string{"local"},
wantPlanIDs: []string{"kept-plan"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cleanupOrphanedRemoteReposAndPlans(tc.config)
gotRepoIDs := make([]string, len(tc.config.Repos))
for i, r := range tc.config.Repos {
gotRepoIDs[i] = r.Id
}
gotPlanIDs := make([]string, len(tc.config.Plans))
for i, p := range tc.config.Plans {
gotPlanIDs[i] = p.Id
}
if !sliceEqual(gotRepoIDs, tc.wantRepoIDs) {
t.Errorf("repos = %v, want %v", gotRepoIDs, tc.wantRepoIDs)
}
if !sliceEqual(gotPlanIDs, tc.wantPlanIDs) {
t.Errorf("plans = %v, want %v", gotPlanIDs, tc.wantPlanIDs)
}
})
}
}
func TestValidateRepoForgetPolicy(t *testing.T) {
validGUID := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
baseConfig := func(repo *v1.Repo) *v1.Config {
return &v1.Config{Instance: "test", Repos: []*v1.Repo{repo}}
}
tests := []struct {
name string
repo *v1.Repo
wantErr bool
}{
{
name: "no forget policy is valid",
repo: &v1.Repo{Id: "repo1", Uri: "file:///tmp/repo", Guid: validGUID},
},
{
name: "valid forget policy",
repo: &v1.Repo{
Id: "repo1", Uri: "file:///tmp/repo", Guid: validGUID,
ForgetPolicy: &v1.ForgetPolicy{
Schedule: &v1.Schedule{Schedule: &v1.Schedule_MaxFrequencyDays{MaxFrequencyDays: 1}},
Retention: &v1.RetentionPolicy{Policy: &v1.RetentionPolicy_PolicyKeepLastN{PolicyKeepLastN: 5}},
},
},
},
{
name: "forget policy with nil retention",
repo: &v1.Repo{
Id: "repo1", Uri: "file:///tmp/repo", Guid: validGUID,
ForgetPolicy: &v1.ForgetPolicy{
Schedule: &v1.Schedule{Schedule: &v1.Schedule_MaxFrequencyDays{MaxFrequencyDays: 1}},
},
},
wantErr: true,
},
{
name: "forget policy with empty retention",
repo: &v1.Repo{
Id: "repo1", Uri: "file:///tmp/repo", Guid: validGUID,
ForgetPolicy: &v1.ForgetPolicy{
Schedule: &v1.Schedule{Schedule: &v1.Schedule_MaxFrequencyDays{MaxFrequencyDays: 1}},
Retention: &v1.RetentionPolicy{},
},
},
wantErr: true,
},
{
name: "forget policy with invalid schedule",
repo: &v1.Repo{
Id: "repo1", Uri: "file:///tmp/repo", Guid: validGUID,
ForgetPolicy: &v1.ForgetPolicy{
Schedule: &v1.Schedule{Schedule: &v1.Schedule_Cron{Cron: "bad cron"}},
Retention: &v1.RetentionPolicy{Policy: &v1.RetentionPolicy_PolicyKeepLastN{PolicyKeepLastN: 5}},
},
},
wantErr: true,
},
{
name: "forget policy with empty time bucketed retention",
repo: &v1.Repo{
Id: "repo1", Uri: "file:///tmp/repo", Guid: validGUID,
ForgetPolicy: &v1.ForgetPolicy{
Schedule: &v1.Schedule{Schedule: &v1.Schedule_MaxFrequencyDays{MaxFrequencyDays: 1}},
Retention: &v1.RetentionPolicy{Policy: &v1.RetentionPolicy_PolicyTimeBucketed{PolicyTimeBucketed: &v1.RetentionPolicy_TimeBucketedCounts{}}},
},
},
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := ValidateConfig(baseConfig(tc.repo))
if tc.wantErr && err == nil {
t.Error("expected error, got nil")
} else if !tc.wantErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
func sliceEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}