Files
ThothII/tools/tht/internal/backup/preflight_unix_test.go
T

158 lines
4.9 KiB
Go

//go:build !windows
package backup
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
)
func TestStageArchiveRejectsSymlinkedInstallationAncestor(t *testing.T) {
root, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
realProject := filepath.Join(root, "real-project")
linkedProject := filepath.Join(root, "linked-project")
if err := os.Mkdir(realProject, 0o700); err != nil {
t.Fatal(err)
}
if err := os.Symlink(realProject, linkedProject); err != nil {
t.Fatal(err)
}
installation := config.Installation{
Path: filepath.Join(linkedProject, "deploy", "local-dev", "thothii-installation.yaml"),
ProjectDirectory: linkedProject,
}
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
archive := filepath.Join(root, "valid.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
if _, err := result.StageArchive(context.Background()); err == nil || !strings.Contains(err.Error(), "staging root") {
t.Fatalf("StageArchive() error = %v, want unsafe symlinked staging-root rejection", err)
}
}
func TestStageArchiveCreatesUnixPrivateRegularFile(t *testing.T) {
installation := preflightTestInstallation(t)
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "valid.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
staged, err := result.StageArchive(context.Background())
if err != nil {
t.Fatal(err)
}
defer staged.Close()
if err := safeio.ValidatePrivateRegular(staged.path); err != nil {
t.Fatalf("staged archive privacy = %v, want owner-private regular file", err)
}
info, err := os.Lstat(staged.path)
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != 0o600 {
t.Fatalf("staged archive mode = %#o, want 0600", got)
}
}
func TestStageArchiveCloseUsesPinnedRootAfterAncestorSwap(t *testing.T) {
installation := preflightTestInstallation(t)
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "valid.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
outsideParent, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
outside := filepath.Join(outsideParent, "outside")
if err := os.Mkdir(outside, 0o700); err != nil {
t.Fatal(err)
}
if err := safeio.ProtectPrivateDirectory(outside); err != nil {
t.Fatal(err)
}
sentinelPath := filepath.Join(outside, "sentinel")
sentinel := []byte("outside sentinel")
if err := os.WriteFile(sentinelPath, sentinel, 0o600); err != nil {
t.Fatal(err)
}
swapped := false
restoreHook := safeio.SetPrivateDirectoryTestHookForTest(func(stage string) {
if stage != "before-stage-archive-remove" || swapped {
return
}
swapped = true
movedRoot := result.stagingRoot + "-original"
if err := os.Rename(result.stagingRoot, movedRoot); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, result.stagingRoot); err != nil {
t.Fatal(err)
}
})
defer restoreHook()
staged, err := result.StageArchive(context.Background())
if err != nil {
t.Fatal(err)
}
stagedName := staged.name
movedRoot := result.stagingRoot + "-original"
if filepath.Dir(staged.path) != result.stagingRoot {
t.Fatalf("staged archive directory = %q, want %q", filepath.Dir(staged.path), result.stagingRoot)
}
if err := staged.Close(); err != nil {
t.Fatal(err)
}
if !swapped {
t.Fatal("stage archive cleanup hook did not run")
}
if _, err := os.Stat(filepath.Join(movedRoot, stagedName)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("moved staging archive = %v, want os.ErrNotExist", err)
}
gotSentinel, err := os.ReadFile(sentinelPath)
if err != nil {
t.Fatal(err)
}
if string(gotSentinel) != string(sentinel) {
t.Fatalf("outside sentinel = %q, want %q", gotSentinel, sentinel)
}
}