// Copyright (C) 2021-2023 Simon Ruderich
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
package sync
import (
"fmt"
"io/fs"
"math/rand"
"os"
"path/filepath"
"regexp"
"testing"
"ruderich.org/simon/safcm"
ft "ruderich.org/simon/safcm/remote/sync/filetest"
"ruderich.org/simon/safcm/testutil"
)
var randFilesRegexp = regexp.MustCompile(`\d+"$`)
func TestSyncFiles(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer os.Chdir(cwd) //nolint:errcheck
err = os.RemoveAll("testdata")
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("testdata", 0700)
if err != nil {
t.Fatal(err)
}
root := ft.File{
Path: ".",
Mode: fs.ModeDir | 0700,
}
user, uid, group, gid := ft.CurrentUserAndGroup()
skipUnlessCiRun := len(os.Getenv("SAFCM_CI_RUN")) == 0
tmpTestFilePath := "/tmp/safcm-sync-files-test-file"
tests := []struct {
name string
skip bool
req safcm.MsgSyncReq
prepare func()
expTriggers []string
expFiles []ft.File
expResp safcm.MsgSyncResp
expDbg []string
expErr error
}{
// NOTE: Also update MsgSyncResp in safcm test cases when
// changing the MsgSyncResp struct!
// See TestSyncFile() for most file related tests. This
// function only tests the overall results and triggers.
{
"basic: create",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
Uid: -1,
Gid: -1,
},
"dir": {
OrigGroup: "group",
Path: "dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
},
"dir/file": {
OrigGroup: "group",
Path: "dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
},
},
},
nil,
nil,
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
{
Path: "dir/file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "dir",
Created: true,
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0755,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
{
Path: "dir/file",
Created: true,
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "." (group): unchanged`,
`4: files: "dir" (group): will create`,
`3: files: "dir" (group): creating`,
`4: files: "dir" (group): creating directory`,
`4: files: "dir" (group): chmodding drwxr-xr-x`,
fmt.Sprintf(`4: files: "dir" (group): chowning %d/%d`, uid, gid),
`4: files: "dir/file" (group): will create`,
`3: files: "dir/file" (group): creating`,
`4: files: "dir/file" (group): creating temporary file "dir/.file*"`,
`4: files: "dir/file" (group): renaming "dir/.fileRND"`,
},
nil,
},
{
"basic: no change",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
Uid: -1,
Gid: -1,
},
"dir": {
OrigGroup: "group",
Path: "dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
},
"dir/file": {
OrigGroup: "group",
Path: "dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
},
},
},
func() {
ft.CreateDirectory("dir", 0755)
ft.CreateFile("dir/file", "content\n", 0644)
},
nil,
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
{
Path: "dir/file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{},
[]string{
`4: files: "." (group): unchanged`,
`4: files: "dir" (group): unchanged`,
`4: files: "dir/file" (group): unchanged`,
},
nil,
},
{
"invalid File: user",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
User: "user",
Uid: 1,
Gid: -1,
},
},
},
nil,
nil,
[]ft.File{
root,
},
safcm.MsgSyncResp{},
nil,
fmt.Errorf("\".\": cannot set both User (\"user\") and Uid (1)"),
},
{
"invalid File: group",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
Uid: -1,
Group: "group",
Gid: 1,
},
},
},
nil,
nil,
[]ft.File{
root,
},
safcm.MsgSyncResp{},
nil,
fmt.Errorf("\".\": cannot set both Group (\"group\") and Gid (1)"),
},
{
// We use relative paths for most tests because we
// don't want to modify the running system. Use this
// test (and the one below for triggers) as a basic
// check that absolute paths work.
//
// Use numeric IDs as not all systems use root/root;
// for example BSDs use root/wheel.
"absolute paths: no change",
skipUnlessCiRun,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
"/": {
OrigGroup: "group",
Path: "/",
Mode: fs.ModeDir | 0755,
Uid: 0,
Gid: 0,
},
"/etc": {
OrigGroup: "group",
Path: "/etc",
Mode: fs.ModeDir | 0755,
Uid: 0,
Gid: 0,
},
"/tmp": {
OrigGroup: "group",
Path: "/tmp",
Mode: fs.ModeDir | 0777 | fs.ModeSticky,
Uid: 0,
Gid: 0,
},
},
},
nil,
nil,
[]ft.File{
root,
},
safcm.MsgSyncResp{},
[]string{
`4: files: "/" (group): unchanged`,
`4: files: "/etc" (group): unchanged`,
`4: files: "/tmp" (group): unchanged`,
},
nil,
},
{
"triggers: no change",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger .",
},
},
"dir": {
OrigGroup: "group",
Path: "dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger dir",
},
},
"dir/file": {
OrigGroup: "group",
Path: "dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
TriggerCommands: []string{
"echo trigger dir/file",
},
},
},
},
func() {
ft.CreateDirectory("dir", 0755)
ft.CreateFile("dir/file", "content\n", 0644)
},
nil,
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
{
Path: "dir/file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{},
[]string{
`4: files: "." (group): unchanged`,
`4: files: "dir" (group): unchanged`,
`4: files: "dir/file" (group): unchanged`,
},
nil,
},
{
"triggers: change root",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger .",
},
},
"dir": {
OrigGroup: "group",
Path: "dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger dir",
},
},
"dir/file": {
OrigGroup: "group",
Path: "dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
TriggerCommands: []string{
"echo trigger dir/file",
},
},
},
},
func() {
ft.CreateDirectoryExists(".", 0750)
ft.CreateDirectory("dir", 0755)
ft.CreateFile("dir/file", "content\n", 0644)
},
[]string{
".",
},
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
{
Path: "dir/file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: ".",
Old: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0750,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0700,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "." (group): permission differs drwxr-x--- -> drwx------`,
`3: files: "." (group): updating`,
`4: files: "." (group): chmodding drwx------`,
`3: files: ".": queuing trigger on "."`,
`4: files: "dir" (group): unchanged`,
`4: files: "dir/file" (group): unchanged`,
},
nil,
},
{
"triggers: change middle",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger .",
},
},
"dir": {
OrigGroup: "group",
Path: "dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger dir",
},
},
"dir/file": {
OrigGroup: "group",
Path: "dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
TriggerCommands: []string{
"echo trigger dir/file",
},
},
},
},
func() {
ft.CreateDirectory("dir", 0750)
ft.CreateFile("dir/file", "content\n", 0644)
},
[]string{
".",
"dir",
},
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
{
Path: "dir/file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "dir",
Old: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0750,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0755,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "." (group): unchanged`,
`4: files: "dir" (group): permission differs drwxr-x--- -> drwxr-xr-x`,
`3: files: "dir" (group): updating`,
`4: files: "dir" (group): chmodding drwxr-xr-x`,
`3: files: "dir": queuing trigger on "."`,
`3: files: "dir": queuing trigger on "dir"`,
`4: files: "dir/file" (group): unchanged`,
},
nil,
},
{
"triggers: change leaf",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger .",
},
},
"dir": {
OrigGroup: "group",
Path: "dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger dir",
},
},
"dir/file": {
OrigGroup: "group",
Path: "dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
TriggerCommands: []string{
"echo trigger dir/file",
},
},
},
},
func() {
ft.CreateDirectory("dir", 0755)
},
[]string{
".",
"dir",
"dir/file",
},
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
{
Path: "dir/file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "dir/file",
Created: true,
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "." (group): unchanged`,
`4: files: "dir" (group): unchanged`,
`4: files: "dir/file" (group): will create`,
`3: files: "dir/file" (group): creating`,
`4: files: "dir/file" (group): creating temporary file "dir/.file*"`,
`4: files: "dir/file" (group): renaming "dir/.fileRND"`,
`3: files: "dir/file": queuing trigger on "."`,
`3: files: "dir/file": queuing trigger on "dir"`,
`3: files: "dir/file": queuing trigger on "dir/file"`,
},
nil,
},
{
"triggers: multiple changes",
false,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
".": {
OrigGroup: "group",
Path: ".",
Mode: fs.ModeDir | 0700,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger .",
},
},
"dir": {
OrigGroup: "group",
Path: "dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger dir",
},
},
"dir/file": {
OrigGroup: "group",
Path: "dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
TriggerCommands: []string{
"echo trigger dir/file",
},
},
},
},
nil,
[]string{
".",
"dir",
"dir/file",
},
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
{
Path: "dir/file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "dir",
Created: true,
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0755,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
{
Path: "dir/file",
Created: true,
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "." (group): unchanged`,
`4: files: "dir" (group): will create`,
`3: files: "dir" (group): creating`,
`4: files: "dir" (group): creating directory`,
`4: files: "dir" (group): chmodding drwxr-xr-x`,
fmt.Sprintf(`4: files: "dir" (group): chowning %d/%d`, uid, gid),
`3: files: "dir": queuing trigger on "."`,
`3: files: "dir": queuing trigger on "dir"`,
`4: files: "dir/file" (group): will create`,
`3: files: "dir/file" (group): creating`,
`4: files: "dir/file" (group): creating temporary file "dir/.file*"`,
`4: files: "dir/file" (group): renaming "dir/.fileRND"`,
`4: files: "dir/file": skipping trigger on ".", already active`,
`4: files: "dir/file": skipping trigger on "dir", already active`,
`3: files: "dir/file": queuing trigger on "dir/file"`,
},
nil,
},
{
"triggers: absolute paths",
skipUnlessCiRun,
safcm.MsgSyncReq{
Files: map[string]*safcm.File{
"/": {
OrigGroup: "group",
Path: "/",
Mode: fs.ModeDir | 0755,
Uid: 0,
Gid: 0,
TriggerCommands: []string{
"echo trigger /",
},
},
"/tmp": {
OrigGroup: "group",
Path: "/tmp",
Mode: fs.ModeDir | 0777 | fs.ModeSticky,
Uid: 0,
Gid: 0,
TriggerCommands: []string{
"echo trigger /tmp",
},
},
tmpTestFilePath: {
OrigGroup: "group",
Path: tmpTestFilePath,
Mode: 0600,
Uid: -1,
Gid: -1,
TriggerCommands: []string{
"echo trigger /tmp/file",
},
},
},
},
nil,
[]string{
"/",
"/tmp",
// Don't use variable for more robust test
"/tmp/safcm-sync-files-test-file",
},
[]ft.File{
root,
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "/tmp/safcm-sync-files-test-file",
Created: true,
New: safcm.FileChangeInfo{
Mode: 0600,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "/" (group): unchanged`,
`4: files: "/tmp" (group): unchanged`,
`4: files: "/tmp/safcm-sync-files-test-file" (group): will create`,
`3: files: "/tmp/safcm-sync-files-test-file" (group): creating`,
`4: files: "/tmp/safcm-sync-files-test-file" (group): creating temporary file "/tmp/.safcm-sync-files-test-file*"`,
`4: files: "/tmp/safcm-sync-files-test-file" (group): renaming "/tmp/.safcm-sync-files-test-fileRND"`,
`3: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/"`,
`3: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/tmp"`,
`3: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/tmp/safcm-sync-files-test-file"`,
},
nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.skip {
t.SkipNow()
}
// Create separate test directory for each test case
path := filepath.Join(cwd, "testdata", "files-"+tc.name)
err := os.Mkdir(path, 0700)
if err != nil {
t.Fatal(err)
}
err = os.Chdir(path)
if err != nil {
t.Fatal(err)
}
if tc.prepare != nil {
tc.prepare()
}
s, res := prepareSync(tc.req, &testRunner{
t: t,
})
err = s.setDefaults()
if err != nil {
t.Fatal(err)
}
err = s.syncFiles()
testutil.AssertErrorEqual(t, "err", err, tc.expErr)
dbg := res.Wait()
// Remove random file names from result
for i, x := range dbg {
dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
}
testutil.AssertEqual(t, "dbg", dbg, tc.expDbg)
files, err := ft.WalkDir(path)
if err != nil {
t.Fatal(err)
}
testutil.AssertEqual(t, "files", files, tc.expFiles)
testutil.AssertEqual(t, "resp", s.resp, tc.expResp)
testutil.AssertEqual(t, "triggers",
s.triggers, tc.expTriggers)
})
}
os.Remove(tmpTestFilePath)
if !t.Failed() {
err = os.RemoveAll(filepath.Join(cwd, "testdata"))
if err != nil {
t.Fatal(err)
}
}
}
func TestSyncFile(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer os.Chdir(cwd) //nolint:errcheck
err = os.RemoveAll("testdata")
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("testdata", 0700)
if err != nil {
t.Fatal(err)
}
root := ft.File{
Path: ".",
Mode: fs.ModeDir | 0700,
}
user, uid, group, gid := ft.CurrentUserAndGroup()
tests := []struct {
name string
req safcm.MsgSyncReq
file *safcm.File
prepare func()
expChanged bool
expFiles []ft.File
expResp safcm.MsgSyncResp
expDbg []string
expErr error
}{
// NOTE: Also update MsgSyncResp in safcm test cases when
// changing the MsgSyncResp struct!
// TODO: Add tests for chown and run them only as root
// Regular file
{
"file: create",
safcm.MsgSyncReq{},
&safcm.File{
Path: "file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
OrigGroup: "group",
},
nil,
true,
[]ft.File{
root,
{
Path: "file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "file",
Created: true,
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "file" (group): will create`,
`3: files: "file" (group): creating`,
`4: files: "file" (group): creating temporary file ".file*"`,
`4: files: "file" (group): renaming ".fileRND"`,
},
nil,
},
{
"file: create (dry-run)",
safcm.MsgSyncReq{
DryRun: true,
},
&safcm.File{
Path: "file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
OrigGroup: "group",
},
nil,
true,
[]ft.File{root},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "file",
Created: true,
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "file" (group): will create`,
`3: files: "file" (group): creating`,
`4: files: "file" (group): dry-run, skipping changes`,
},
nil,
},
{
"file: unchanged",
safcm.MsgSyncReq{},
&safcm.File{
Path: "file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
OrigGroup: "group",
},
func() {
ft.CreateFile("file", "content\n", 0644)
},
false,
[]ft.File{
root,
{
Path: "file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{},
[]string{
`4: files: "file" (group): unchanged`,
},
nil,
},
{
"file: unchanged (non-default user-group)",
safcm.MsgSyncReq{},
&safcm.File{
Path: "file",
Mode: 0644,
User: user,
Uid: -1,
Group: group,
Gid: -1,
Data: []byte("content\n"),
OrigGroup: "group",
},
func() {
ft.CreateFile("file", "content\n", 0644)
},
false,
[]ft.File{
root,
{
Path: "file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{},
[]string{
`4: files: "file" (group): unchanged`,
},
nil,
},
{
"file: permission",
safcm.MsgSyncReq{},
&safcm.File{
Path: "file",
Mode: 0755 | fs.ModeSetuid,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
OrigGroup: "group",
},
func() {
ft.CreateFile("file", "content\n", 0755)
},
true,
[]ft.File{
root,
{
Path: "file",
Mode: 0755 | fs.ModeSetuid,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "file",
Old: safcm.FileChangeInfo{
Mode: 0755,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0755 | fs.ModeSetuid,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "file" (group): permission differs -rwxr-xr-x -> urwxr-xr-x`,
`3: files: "file" (group): updating`,
`4: files: "file" (group): creating temporary file ".file*"`,
`4: files: "file" (group): renaming ".fileRND"`,
},
nil,
},
{
"file: content",
safcm.MsgSyncReq{},
&safcm.File{
Path: "file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
OrigGroup: "group",
},
func() {
ft.CreateFile("file", "old content\n", 0644)
},
true,
[]ft.File{
root,
{
Path: "file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "file",
Old: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1,2 +1,2 @@
-old content
+content
`,
},
},
},
[]string{
`4: files: "file" (group): content differs`,
`3: files: "file" (group): updating`,
`4: files: "file" (group): creating temporary file ".file*"`,
`4: files: "file" (group): renaming ".fileRND"`,
},
nil,
},
// Symbolic link
{
"symlink: create",
safcm.MsgSyncReq{},
&safcm.File{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
Data: []byte("target"),
OrigGroup: "group",
},
nil,
true,
[]ft.File{
root,
{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Data: []byte("target"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "link",
Created: true,
New: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "link" (group): will create`,
`3: files: "link" (group): creating`,
`4: files: "link" (group): creating temporary symlink ".linkRND"`,
`4: files: "link" (group): renaming ".linkRND"`,
},
nil,
},
{
"symlink: create (conflict)",
safcm.MsgSyncReq{},
&safcm.File{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
Data: []byte("target"),
OrigGroup: "group",
},
func() {
ft.CreateFile(".link8717895732742165505", "", 0600)
},
true,
[]ft.File{
root,
{
Path: ".link8717895732742165505",
Mode: 0600,
Data: []byte(""),
},
{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Data: []byte("target"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "link",
Created: true,
New: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "link" (group): will create`,
`3: files: "link" (group): creating`,
`4: files: "link" (group): creating temporary symlink ".linkRND"`,
`4: files: "link" (group): creating temporary symlink ".linkRND"`,
`4: files: "link" (group): renaming ".linkRND"`,
},
nil,
},
{
"symlink: create (dry-run)",
safcm.MsgSyncReq{
DryRun: true,
},
&safcm.File{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
Data: []byte("target"),
OrigGroup: "group",
},
nil,
true,
[]ft.File{root},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "link",
Created: true,
New: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "link" (group): will create`,
`3: files: "link" (group): creating`,
`4: files: "link" (group): dry-run, skipping changes`,
},
nil,
},
{
"symlink: unchanged",
safcm.MsgSyncReq{},
&safcm.File{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
Data: []byte("target"),
OrigGroup: "group",
},
func() {
ft.CreateSymlink("link", "target")
},
false,
[]ft.File{
root,
{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Data: []byte("target"),
},
},
safcm.MsgSyncResp{},
[]string{
`4: files: "link" (group): unchanged`,
},
nil,
},
{
"symlink: content",
safcm.MsgSyncReq{},
&safcm.File{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
Data: []byte("target"),
OrigGroup: "group",
},
func() {
ft.CreateSymlink("link", "old-target")
},
true,
[]ft.File{
root,
{
Path: "link",
Mode: fs.ModeSymlink | 0777,
Data: []byte("target"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "link",
Old: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1 +1 @@
-old-target
+target
`,
},
},
},
[]string{
`4: files: "link" (group): content differs`,
`3: files: "link" (group): updating`,
`4: files: "link" (group): creating temporary symlink ".linkRND"`,
`4: files: "link" (group): renaming ".linkRND"`,
},
nil,
},
// Directory
{
"directory: create",
safcm.MsgSyncReq{},
&safcm.File{
Path: "dir",
Mode: fs.ModeDir | 0705,
Uid: -1,
Gid: -1,
OrigGroup: "group",
},
nil,
true,
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0705,
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "dir",
Created: true,
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0705,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "dir" (group): will create`,
`3: files: "dir" (group): creating`,
`4: files: "dir" (group): creating directory`,
`4: files: "dir" (group): chmodding drwx---r-x`,
fmt.Sprintf(`4: files: "dir" (group): chowning %d/%d`, uid, gid),
},
nil,
},
{
"directory: create (dry-run)",
safcm.MsgSyncReq{
DryRun: true,
},
&safcm.File{
Path: "dir",
Mode: fs.ModeDir | 0644,
Uid: -1,
Gid: -1,
OrigGroup: "group",
},
nil,
true,
[]ft.File{root},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "dir",
Created: true,
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "dir" (group): will create`,
`3: files: "dir" (group): creating`,
`4: files: "dir" (group): dry-run, skipping changes`,
},
nil,
},
{
"directory: unchanged",
safcm.MsgSyncReq{},
&safcm.File{
Path: "dir",
Mode: fs.ModeDir | 0755,
Uid: -1,
Gid: -1,
OrigGroup: "group",
},
func() {
ft.CreateDirectory("dir", 0755)
},
false,
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
},
safcm.MsgSyncResp{},
[]string{
`4: files: "dir" (group): unchanged`,
},
nil,
},
{
"directory: permission",
safcm.MsgSyncReq{},
&safcm.File{
Path: "dir",
Mode: fs.ModeDir | 0755 | fs.ModeSetgid,
Uid: -1,
Gid: -1,
OrigGroup: "group",
},
func() {
ft.CreateDirectory("dir", 0500|fs.ModeSticky)
},
true,
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755 | fs.ModeSetgid,
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "dir",
Old: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0500 | fs.ModeSticky,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0755 | fs.ModeSetgid,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "dir" (group): permission differs dtr-x------ -> dgrwxr-xr-x`,
`3: files: "dir" (group): updating`,
`4: files: "dir" (group): chmodding dgrwxr-xr-x`,
},
nil,
},
// Type changes
{
"change: file to directory",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: fs.ModeDir | 0751,
Uid: -1,
Gid: -1,
OrigGroup: "group",
},
func() {
ft.CreateFile("path", "content\n", 0644)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: fs.ModeDir | 0751,
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0751,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1,2 +1 @@
-content
`,
},
},
},
[]string{
`4: files: "path" (group): type differs ---------- -> d---------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): removing (due to type change)`,
`4: files: "path" (group): creating directory`,
`4: files: "path" (group): chmodding drwxr-x--x`,
fmt.Sprintf(`4: files: "path" (group): chowning %d/%d`, uid, gid),
},
nil,
},
{
"change: file to symlink",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("target"),
},
func() {
ft.CreateFile("path", "content\n", 0644)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: fs.ModeSymlink | 0777,
Data: []byte("target"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1,2 +1 @@
-content
-
+target
`,
},
},
},
[]string{
`4: files: "path" (group): type differs ---------- -> L---------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): creating temporary symlink ".pathRND"`,
`4: files: "path" (group): renaming ".pathRND"`,
},
nil,
},
{
"change: symlink to file",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: 0640,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("content\n"),
},
func() {
ft.CreateSymlink("path", "target")
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: 0640,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0640,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1 +1,2 @@
-target
+content
+
`,
},
},
},
[]string{
`4: files: "path" (group): type differs L--------- -> ----------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): creating temporary file ".path*"`,
`4: files: "path" (group): renaming ".pathRND"`,
},
nil,
},
{
"change: symlink to directory",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: fs.ModeDir | 0751,
Uid: -1,
Gid: -1,
OrigGroup: "group",
},
func() {
ft.CreateSymlink("path", "target")
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: fs.ModeDir | 0751,
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0751,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1 +1 @@
-target
+
`,
},
},
},
[]string{
`4: files: "path" (group): type differs L--------- -> d---------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): removing (due to type change)`,
`4: files: "path" (group): creating directory`,
`4: files: "path" (group): chmodding drwxr-x--x`,
fmt.Sprintf(`4: files: "path" (group): chowning %d/%d`, uid, gid),
},
nil,
},
{
"change: directory to file",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: 0666,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("content\n"),
},
func() {
ft.CreateDirectory("path", 0777)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: 0666,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0666,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "path" (group): type differs d--------- -> ----------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): removing (due to type change)`,
`4: files: "path" (group): creating temporary file ".path*"`,
`4: files: "path" (group): renaming ".pathRND"`,
},
nil,
},
{
"change: directory to file (non-empty)",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: 0666,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("content\n"),
},
func() {
ft.CreateDirectory("path", 0777)
ft.CreateFile("path/file", "content\n", 0644)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: fs.ModeDir | 0777,
},
{
Path: "path/file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0666,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "path" (group): type differs d--------- -> ----------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): removing (due to type change)`,
},
fmt.Errorf("will not replace non-empty directory, please remove manually"),
},
{
"change: directory to symlink",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("target"),
},
func() {
ft.CreateDirectory("path", 0777)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: fs.ModeSymlink | 0777,
Data: []byte("target"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "path" (group): type differs d--------- -> L---------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): removing (due to type change)`,
`4: files: "path" (group): creating temporary symlink ".pathRND"`,
`4: files: "path" (group): renaming ".pathRND"`,
},
nil,
},
{
"change: other to file",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: 0640,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("content\n"),
},
func() {
ft.CreateFifo("path", 0666)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: 0640,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: fs.ModeNamedPipe | 0666,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0640,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "path" (group): type differs p--------- -> ----------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): creating temporary file ".path*"`,
`4: files: "path" (group): renaming ".pathRND"`,
},
nil,
},
{
"change: other to symlink",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("target"),
},
func() {
ft.CreateFifo("path", 0666)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: fs.ModeSymlink | 0777,
Data: []byte("target"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: fs.ModeNamedPipe | 0666,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "path" (group): type differs p--------- -> L---------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): creating temporary symlink ".pathRND"`,
`4: files: "path" (group): renaming ".pathRND"`,
},
nil,
},
{
"change: other to directory",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: fs.ModeDir | 0751,
Uid: -1,
Gid: -1,
OrigGroup: "group",
},
func() {
ft.CreateFifo("path", 0666)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: fs.ModeDir | 0751,
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: fs.ModeNamedPipe | 0666,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeDir | 0751,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "path" (group): type differs p--------- -> d---------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): removing (due to type change)`,
`4: files: "path" (group): creating directory`,
`4: files: "path" (group): chmodding drwxr-x--x`,
fmt.Sprintf(`4: files: "path" (group): chowning %d/%d`, uid, gid),
},
nil,
},
{
"change: file to symlink (same content)",
safcm.MsgSyncReq{},
&safcm.File{
Path: "path",
Mode: fs.ModeSymlink | 0777,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("target"),
},
func() {
ft.CreateFile("path", "target", 0644)
},
true,
[]ft.File{
root,
{
Path: "path",
Mode: fs.ModeSymlink | 0777,
Data: []byte("target"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "path",
Old: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: fs.ModeSymlink | 0777,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "path" (group): type differs ---------- -> L---------`,
`3: files: "path" (group): updating`,
`4: files: "path" (group): creating temporary symlink ".pathRND"`,
`4: files: "path" (group): renaming ".pathRND"`,
},
nil,
},
// Symlink "attacks"
{
"symlink in earlier path component",
safcm.MsgSyncReq{},
&safcm.File{
Path: "dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("content"),
},
func() {
ft.CreateDirectory("tmp", 0755)
ft.CreateSymlink("dir", "tmp")
},
false,
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeSymlink | 0777,
Data: []byte("tmp"),
},
{
Path: "tmp",
Mode: fs.ModeDir | 0755,
},
},
safcm.MsgSyncResp{},
nil,
fmt.Errorf("symlink not permitted in path: \"dir\""),
},
// Border cases
{
"relative path with leading dot",
safcm.MsgSyncReq{},
&safcm.File{
Path: "./dir/file",
Mode: 0644,
Uid: -1,
Gid: -1,
OrigGroup: "group",
Data: []byte("content"),
},
func() {
ft.CreateDirectory("dir", 0755)
},
true,
[]ft.File{
root,
{
Path: "dir",
Mode: fs.ModeDir | 0755,
},
{
Path: "dir/file",
Mode: 0644,
Data: []byte("content"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "./dir/file",
Created: true,
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
},
},
},
[]string{
`4: files: "./dir/file" (group): will create`,
`3: files: "./dir/file" (group): creating`,
`4: files: "./dir/file" (group): creating temporary file "dir/.file*"`,
`4: files: "./dir/file" (group): renaming "dir/.fileRND"`,
},
nil,
},
// Diffs
{
"diff: textual",
safcm.MsgSyncReq{
DryRun: true,
},
&safcm.File{
Path: "file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte(`
this
is
a
simple
file
`),
OrigGroup: "group",
},
func() {
ft.CreateFile("file", `this
is
file
!
`, 0644)
},
true,
[]ft.File{
root,
{
Path: "file",
Mode: 0644,
Data: []byte(`this
is
file
!
`),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "file",
Old: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1,5 +1,7 @@
+
this
is
+a
+simple
file
-!
`,
},
},
},
[]string{
`4: files: "file" (group): content differs`,
`3: files: "file" (group): updating`,
`4: files: "file" (group): dry-run, skipping changes`,
},
nil,
},
{
"diff: binary both",
safcm.MsgSyncReq{
DryRun: true,
},
&safcm.File{
Path: "file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("\x00\x01\x02\x03"),
OrigGroup: "group",
},
func() {
ft.CreateFile("file", "\x00\x01\x02", 0644)
},
true,
[]ft.File{
root,
{
Path: "file",
Mode: 0644,
Data: []byte("\x00\x01\x02"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "file",
Old: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: "Binary files differ (3 -> 4 bytes), cannot show diff",
},
},
},
[]string{
`4: files: "file" (group): content differs`,
`3: files: "file" (group): updating`,
`4: files: "file" (group): dry-run, skipping changes`,
},
nil,
},
{
"diff: binary old",
safcm.MsgSyncReq{
DryRun: true,
},
&safcm.File{
Path: "file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("content\n"),
OrigGroup: "group",
},
func() {
ft.CreateFile("file", "\x00\x01\x02", 0644)
},
true,
[]ft.File{
root,
{
Path: "file",
Mode: 0644,
Data: []byte("\x00\x01\x02"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "file",
Old: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1,2 +1,2 @@
-
+content
`,
},
},
},
[]string{
`4: files: "file" (group): content differs`,
`3: files: "file" (group): updating`,
`4: files: "file" (group): dry-run, skipping changes`,
},
nil,
},
{
"diff: binary new",
safcm.MsgSyncReq{
DryRun: true,
},
&safcm.File{
Path: "file",
Mode: 0644,
Uid: -1,
Gid: -1,
Data: []byte("\x00\x01\x02\x03"),
OrigGroup: "group",
},
func() {
ft.CreateFile("file", "content\n", 0644)
},
true,
[]ft.File{
root,
{
Path: "file",
Mode: 0644,
Data: []byte("content\n"),
},
},
safcm.MsgSyncResp{
FileChanges: []safcm.FileChange{
{
Path: "file",
Old: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
New: safcm.FileChangeInfo{
Mode: 0644,
User: user,
Uid: uid,
Group: group,
Gid: gid,
},
DataDiff: `@@ -1,2 +1,2 @@
-content
+
`,
},
},
},
[]string{
`4: files: "file" (group): content differs`,
`3: files: "file" (group): updating`,
`4: files: "file" (group): dry-run, skipping changes`,
},
nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create separate test directory for each test case
path := filepath.Join(cwd, "testdata", "file-"+tc.name)
err := os.Mkdir(path, 0700)
if err != nil {
t.Fatal(err)
}
err = os.Chdir(path)
if err != nil {
t.Fatal(err)
}
if tc.prepare != nil {
tc.prepare()
}
s, res := prepareSync(tc.req, &testRunner{
t: t,
})
err = s.setDefaults()
if err != nil {
t.Fatal(err)
}
// Deterministic temporary symlink names
rand.Seed(0)
var changed bool
err = s.syncFile(tc.file, &changed)
testutil.AssertErrorEqual(t, "err", err, tc.expErr)
dbg := res.Wait()
// Remove random file names from result
for i, x := range dbg {
dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
}
testutil.AssertEqual(t, "dbg", dbg, tc.expDbg)
files, err := ft.WalkDir(path)
if err != nil {
t.Fatal(err)
}
testutil.AssertEqual(t, "files", files, tc.expFiles)
testutil.AssertEqual(t, "changed", changed, tc.expChanged)
testutil.AssertEqual(t, "resp", s.resp, tc.expResp)
})
}
if !t.Failed() {
err = os.RemoveAll(filepath.Join(cwd, "testdata"))
if err != nil {
t.Fatal(err)
}
}
}