117 lines
5.0 KiB
Go
117 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Follow-up to the review (point 4): the one-hour look below a share must not hide a backup repository
|
|
// that appears later. Where this machine's backup seat keeps its repository is read from the bus and every
|
|
// share is judged against it on every pass.
|
|
|
|
func (f *fake) backup(holder string, paths ...string) {
|
|
raw, _ := json.Marshal(BackupFromBus{Holder: holder, Paths: paths})
|
|
f.file(BackupFile, string(raw))
|
|
}
|
|
|
|
func TestAShareHoldingTheBackupSeatsRepositoryIsRefusedOnTheNextPass(t *testing.T) {
|
|
f := newFake(t)
|
|
f.mountinfo("/")
|
|
f.dir("/srv/pool/media")
|
|
f.dir("/srv/pool/restic")
|
|
f.file(ConfigFile, grantsConfig("pool=/srv/pool media=/srv/pool/media", "pool=ws:rw media=ws:rw"))
|
|
f.roster("ws 10.0.0.3")
|
|
f.wants(map[string][]Want{"ws": {{Share: "pool", Access: "rw"}, {Share: "media", Access: "rw"}}})
|
|
m := f.machine()
|
|
now := time.Date(2026, 10, 8, 20, 0, 0, 0, time.UTC)
|
|
m.Now = func() time.Time { return now }
|
|
// The repository is not there yet: the look below finds nothing and is kept for an hour.
|
|
if s := m.Pass(context.Background(), nil); refusedWhy(s, "pool") != "" {
|
|
t.Fatalf("the pool is refused before any repository exists: %+v", s.Refused)
|
|
}
|
|
// The backup seat starts keeping its repository there; a minute later is well inside LookKept.
|
|
f.backup("restic", "/srv/pool/restic", "/var/lib/restic")
|
|
now = now.Add(time.Minute)
|
|
s := m.Pass(context.Background(), nil)
|
|
if why := refusedWhy(s, "pool"); !strings.Contains(why, "/srv/pool/restic") || !strings.Contains(why, "restic") {
|
|
t.Errorf("a share holding the backup seat's repository is exported inside the kept look: %q", why)
|
|
}
|
|
if refusedWhy(s, "media") != "" {
|
|
t.Errorf("a share beside the repository is refused: %q", refusedWhy(s, "media"))
|
|
}
|
|
if strings.Contains(f.read(ExportFile), "/srv/pool ") {
|
|
t.Errorf("the export file still names the pool:\n%s", f.read(ExportFile))
|
|
}
|
|
}
|
|
|
|
func TestAShareInsideTheBackupSeatsRepositoryIsRefused(t *testing.T) {
|
|
f := newFake(t)
|
|
f.mountinfo("/")
|
|
f.dir("/srv/backups/data")
|
|
f.file(ConfigFile, grantsConfig("inside=/srv/backups/data", "inside=ws:rw"))
|
|
f.roster("ws 10.0.0.3")
|
|
f.wants(map[string][]Want{"ws": {{Share: "inside", Access: "rw"}}})
|
|
f.backup("restic", "/srv/backups")
|
|
s := f.machine().Pass(context.Background(), nil)
|
|
if !strings.Contains(refusedWhy(s, "inside"), "/srv/backups") {
|
|
t.Errorf("a share inside the backup seat's repository is not refused: %+v", s.Refused)
|
|
}
|
|
}
|
|
|
|
// Until a list has been read, what is exported stays exported — the bus being slow is no reason to take
|
|
// the library away — and the status says the list is not known.
|
|
func TestWithoutTheBackupListTheExportsStayAndItIsSaid(t *testing.T) {
|
|
f := newFake(t)
|
|
f.mountinfo("/")
|
|
f.dir("/srv/library")
|
|
f.file(ConfigFile, grantsConfig("library=/srv/library", "library=ws:rw"))
|
|
f.roster("ws 10.0.0.3")
|
|
f.wants(map[string][]Want{"ws": {{Share: "library", Access: "rw"}}})
|
|
s := f.machine().Pass(context.Background(), nil)
|
|
if refusedWhy(s, "library") != "" || len(s.Shares) != 1 {
|
|
t.Fatalf("the library is not exported while the backup list is unknown: %+v", s)
|
|
}
|
|
if !strings.Contains(strings.Join(s.Said, "\n"), "backup seat") {
|
|
t.Errorf("the status does not say the backup seat's repository is not known: %v", s.Said)
|
|
}
|
|
}
|
|
|
|
func TestBackupFromTheSeatsAndTheData(t *testing.T) {
|
|
seats := json.RawMessage(`{"answer":{"seats":[{"seat":"node-backup","scope":"node","holders":[{"module":"restic","node":"ace"},{"module":"borg","node":"shanks"}]}]},"ok":true}`)
|
|
data := json.RawMessage(`{"answer":{"data":[{"machine":"ace","module":"restic","item":"repository","path":"/storage/restic"},{"machine":"ace","module":"restic","item":"state","path":"/var/lib/restic"},{"machine":"ace","module":"plex","item":"data","path":"/mnt/plex/data"}]},"ok":true}`)
|
|
holder, paths, err := BackupFrom("ace", seats, data)
|
|
if err != nil || holder != "restic" || strings.Join(paths, " ") != "/storage/restic /var/lib/restic" {
|
|
t.Errorf("got %q %v %v", holder, paths, err)
|
|
}
|
|
if _, _, err := BackupFrom("g14", seats, data); err == nil {
|
|
t.Errorf("a machine whose backup seat nobody holds is not said")
|
|
}
|
|
if _, _, err := BackupFrom("ace", seats, json.RawMessage(`{"ok":true}`)); err == nil {
|
|
t.Errorf("an answer with no data list reads as an empty list")
|
|
}
|
|
}
|
|
|
|
// A failed ask keeps the last paths: the file is not emptied, only the error is added.
|
|
func TestAFailedAskKeepsTheLastBackupPaths(t *testing.T) {
|
|
f := newFake(t)
|
|
m := f.machine()
|
|
m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) {
|
|
return nil, errors.New("no responders")
|
|
})
|
|
f.backup("restic", "/storage/restic")
|
|
m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) {
|
|
return nil, errors.New("timeout")
|
|
})
|
|
var b BackupFromBus
|
|
if err := json.Unmarshal([]byte(f.read(BackupFile)), &b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(b.Paths, " ") != "/storage/restic" || !strings.Contains(b.Error, "timeout") {
|
|
t.Errorf("a failed ask lost the last paths or did not say why: %+v", b)
|
|
}
|
|
}
|