157 lines
4.8 KiB
Go
157 lines
4.8 KiB
Go
//go:build unix
|
|
|
|
package firewall
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// noTempLeftover asserts the staging directory holds no uncommitted temp files.
|
|
func noTempLeftover(t *testing.T, dir string) {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(dir)
|
|
require.NoError(t, err)
|
|
for _, e := range entries {
|
|
require.NotContains(t, e.Name(), ".tmp.", "a staged temp file was left behind")
|
|
}
|
|
}
|
|
|
|
// TestAtomicFilePreservesMode confirms a rewrite of an existing file keeps its
|
|
// non-default permission mode rather than resetting to the caller's default.
|
|
func TestAtomicFilePreservesMode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "conf")
|
|
require.NoError(t, os.WriteFile(path, []byte("old\n"), 0600))
|
|
require.NoError(t, os.Chmod(path, 0640))
|
|
|
|
af, err := newAtomicFile(path, 0644)
|
|
require.NoError(t, err)
|
|
_, err = af.Write([]byte("new\n"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, af.Commit())
|
|
|
|
fi, err := os.Stat(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, os.FileMode(0640), fi.Mode().Perm(), "existing mode must be preserved")
|
|
|
|
got, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "new\n", string(got))
|
|
noTempLeftover(t, dir)
|
|
}
|
|
|
|
// TestAtomicFileNewFileUsesDefaultMode confirms a file that does not yet exist
|
|
// is created with the supplied default mode.
|
|
func TestAtomicFileNewFileUsesDefaultMode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "new-conf")
|
|
|
|
af, err := newAtomicFile(path, 0600)
|
|
require.NoError(t, err)
|
|
_, err = af.Write([]byte("content\n"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, af.Commit())
|
|
|
|
fi, err := os.Stat(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, os.FileMode(0600), fi.Mode().Perm())
|
|
noTempLeftover(t, dir)
|
|
}
|
|
|
|
// TestAtomicFileAbortLeavesOriginal confirms Abort discards the staged rewrite
|
|
// and leaves the original file and its content untouched.
|
|
func TestAtomicFileAbortLeavesOriginal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "conf")
|
|
require.NoError(t, os.WriteFile(path, []byte("original\n"), 0644))
|
|
|
|
af, err := newAtomicFile(path, 0644)
|
|
require.NoError(t, err)
|
|
_, err = af.Write([]byte("discarded\n"))
|
|
require.NoError(t, err)
|
|
af.Abort()
|
|
|
|
got, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "original\n", string(got), "Abort must not modify the original")
|
|
noTempLeftover(t, dir)
|
|
}
|
|
|
|
// TestAtomicFileAbortAfterCommitIsNoOp confirms a deferred Abort after a
|
|
// successful Commit does not remove the installed file.
|
|
func TestAtomicFileAbortAfterCommitIsNoOp(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "conf")
|
|
|
|
af, err := newAtomicFile(path, 0644)
|
|
require.NoError(t, err)
|
|
_, err = af.Write([]byte("kept\n"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, af.Commit())
|
|
af.Abort() // Must be inert.
|
|
|
|
got, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "kept\n", string(got))
|
|
}
|
|
|
|
// TestWriteConfigFilePreservesMode confirms the buffer-all convenience wrapper
|
|
// preserves an existing file's mode.
|
|
func TestWriteConfigFilePreservesMode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "conf")
|
|
require.NoError(t, os.WriteFile(path, []byte("x"), 0600))
|
|
require.NoError(t, os.Chmod(path, 0640))
|
|
|
|
require.NoError(t, writeConfigFile(path, []byte("y\n"), 0644))
|
|
|
|
fi, err := os.Stat(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, os.FileMode(0640), fi.Mode().Perm())
|
|
got, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "y\n", string(got))
|
|
noTempLeftover(t, dir)
|
|
}
|
|
|
|
// TestAtomicFilePreservesOwner confirms a rewrite restores the original file's
|
|
// ownership. It requires root to assign a foreign owner, so it is skipped for an
|
|
// unprivileged run.
|
|
func TestAtomicFilePreservesOwner(t *testing.T) {
|
|
if os.Geteuid() != 0 {
|
|
t.Skip("requires root to chown to a foreign owner")
|
|
}
|
|
const uid, gid = 65534, 65534 // nobody/nogroup on most systems.
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "conf")
|
|
require.NoError(t, os.WriteFile(path, []byte("old\n"), 0644))
|
|
require.NoError(t, os.Chown(path, uid, gid))
|
|
|
|
require.NoError(t, writeConfigFile(path, []byte("new\n"), 0644))
|
|
|
|
fi, err := os.Stat(path)
|
|
require.NoError(t, err)
|
|
st, ok := fi.Sys().(*syscall.Stat_t)
|
|
require.True(t, ok)
|
|
require.Equal(t, uint32(uid), st.Uid, "owner uid must be preserved")
|
|
require.Equal(t, uint32(gid), st.Gid, "owner gid must be preserved")
|
|
}
|
|
|
|
// TestAtomicFileStagesInDestinationDir confirms the temp file is created next to
|
|
// the destination so the final rename is atomic within one filesystem.
|
|
func TestAtomicFileStagesInDestinationDir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "conf")
|
|
|
|
af, err := newAtomicFile(path, 0644)
|
|
require.NoError(t, err)
|
|
require.Equal(t, dir, filepath.Dir(af.tmp))
|
|
require.True(t, strings.HasPrefix(filepath.Base(af.tmp), "conf.tmp."))
|
|
af.Abort()
|
|
}
|