+// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
+
+package sync
+
+import (
+ "fmt"
+ "io/fs"
+ "math/rand"
+ "os"
+ "os/user"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "strconv"
+ "syscall"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "ruderich.org/simon/safcm"
+)
+
+type File struct {
+ Path string
+ Mode fs.FileMode
+ Data []byte
+}
+
+func walkDir(basePath string) ([]File, error) {
+ var res []File
+ err := filepath.WalkDir(basePath, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ info, err := d.Info()
+ if err != nil {
+ return err
+ }
+ rel, err := filepath.Rel(basePath, path)
+ if err != nil {
+ return err
+ }
+
+ f := File{
+ Path: rel,
+ Mode: info.Mode(),
+ }
+ if f.Mode.Type() == 0 {
+ x, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ f.Data = x
+ } else if f.Mode.Type() == fs.ModeSymlink {
+ x, err := os.Readlink(path)
+ if err != nil {
+ return err
+ }
+ f.Data = []byte(x)
+ }
+ res = append(res, f)
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return res, nil
+}
+
+var randFilesRegexp = regexp.MustCompile(`\d+"$`)
+
+func TestSyncFiles(t *testing.T) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Chdir(cwd)
+
+ err = os.RemoveAll("testdata")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.Mkdir("testdata", 0700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ root := File{
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ }
+ user, uid, group, gid := currentUserAndGroup()
+
+ tmpTestFilePath := "/tmp/safcm-sync-files-test-file"
+
+ tests := []struct {
+ name string
+ req safcm.MsgSyncReq
+ prepare func()
+ triggers []string
+ expFiles []File
+ expResp safcm.MsgSyncResp
+ expDbg []string
+ expErr error
+ }{
+
+ // NOTE: Also update MsgSyncResp in safcm test cases when
+ // changing anything here!
+
+ // See TestSyncFile() for most file related tests. This
+ // function only tests the overall results and triggers.
+
+ {
+ "basic: create",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ "dir": {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ "dir/file": {
+ Path: "dir/file",
+ Mode: 0644,
+ Uid: -1,
+ Gid: -1,
+ Data: []byte("content\n"),
+ OrigGroup: "group",
+ },
+ },
+ },
+ nil,
+ nil,
+ []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: sync remote: files: "." (group): unchanged`,
+ `4: sync remote: files: "dir" (group): will create`,
+ `3: sync remote: files: "dir" (group): creating`,
+ `4: sync remote: files: "dir" (group): creating directory`,
+ `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
+ fmt.Sprintf(`4: sync remote: files: "dir" (group): chowning %d/%d`, uid, gid),
+ `4: sync remote: files: "dir/file" (group): will create`,
+ `3: sync remote: files: "dir/file" (group): creating`,
+ `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
+ `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
+ },
+ nil,
+ },
+
+ {
+ "basic: no change",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ "dir": {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ "dir/file": {
+ Path: "dir/file",
+ Mode: 0644,
+ Uid: -1,
+ Gid: -1,
+ Data: []byte("content\n"),
+ OrigGroup: "group",
+ },
+ },
+ },
+ func() {
+ createDirectory("dir", 0755)
+ createFile("dir/file", "content\n", 0644)
+ },
+ nil,
+ []File{
+ root,
+ {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ },
+ {
+ Path: "dir/file",
+ Mode: 0644,
+ Data: []byte("content\n"),
+ },
+ },
+ safcm.MsgSyncResp{},
+ []string{
+ `4: sync remote: files: "." (group): unchanged`,
+ `4: sync remote: files: "dir" (group): unchanged`,
+ `4: sync remote: files: "dir/file" (group): unchanged`,
+ },
+ nil,
+ },
+
+ {
+ "invalid File: user",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ User: "user",
+ Uid: 1,
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ },
+ },
+ nil,
+ nil,
+ []File{
+ root,
+ },
+ safcm.MsgSyncResp{},
+ nil,
+ fmt.Errorf("\".\": cannot set both User (\"user\") and Uid (1)"),
+ },
+ {
+ "invalid File: group",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ Uid: -1,
+ Group: "group",
+ Gid: 1,
+ OrigGroup: "group",
+ },
+ },
+ },
+ nil,
+ nil,
+ []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.
+ "absolute paths: no change",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ "/": {
+ Path: "/",
+ Mode: fs.ModeDir | 0755,
+ User: "root",
+ Uid: -1,
+ Group: "root",
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ "/etc": {
+ Path: "/etc",
+ Mode: fs.ModeDir | 0755,
+ User: "root",
+ Uid: -1,
+ Group: "root",
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ "/tmp": {
+ Path: "/tmp",
+ Mode: fs.ModeDir | 0777 | fs.ModeSticky,
+ User: "root",
+ Uid: -1,
+ Group: "root",
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ "/var/tmp": {
+ Path: "/var/tmp",
+ Mode: fs.ModeDir | 0777 | fs.ModeSticky,
+ User: "root",
+ Uid: -1,
+ Group: "root",
+ Gid: -1,
+ OrigGroup: "group",
+ },
+ },
+ },
+ nil,
+ nil,
+ []File{
+ root,
+ },
+ safcm.MsgSyncResp{},
+ []string{
+ `4: sync remote: files: "/" (group): unchanged`,
+ `4: sync remote: files: "/etc" (group): unchanged`,
+ `4: sync remote: files: "/tmp" (group): unchanged`,
+ `4: sync remote: files: "/var/tmp" (group): unchanged`,
+ },
+ nil,
+ },
+
+ {
+ "triggers: no change",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger .",
+ },
+ },
+ "dir": {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir",
+ },
+ },
+ "dir/file": {
+ Path: "dir/file",
+ Mode: 0644,
+ Uid: -1,
+ Gid: -1,
+ Data: []byte("content\n"),
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir/file",
+ },
+ },
+ },
+ },
+ func() {
+ createDirectory("dir", 0755)
+ createFile("dir/file", "content\n", 0644)
+ },
+ nil,
+ []File{
+ root,
+ {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ },
+ {
+ Path: "dir/file",
+ Mode: 0644,
+ Data: []byte("content\n"),
+ },
+ },
+ safcm.MsgSyncResp{},
+ []string{
+ `4: sync remote: files: "." (group): unchanged`,
+ `4: sync remote: files: "dir" (group): unchanged`,
+ `4: sync remote: files: "dir/file" (group): unchanged`,
+ },
+ nil,
+ },
+
+ {
+ "triggers: change root",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger .",
+ },
+ },
+ "dir": {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir",
+ },
+ },
+ "dir/file": {
+ Path: "dir/file",
+ Mode: 0644,
+ Uid: -1,
+ Gid: -1,
+ Data: []byte("content\n"),
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir/file",
+ },
+ },
+ },
+ },
+ func() {
+ err = os.Chmod(".", 0750)
+ if err != nil {
+ panic(err)
+ }
+ createDirectory("dir", 0755)
+ createFile("dir/file", "content\n", 0644)
+ },
+ []string{
+ ".",
+ },
+ []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: sync remote: files: "." (group): permission differs drwxr-x--- -> drwx------`,
+ `3: sync remote: files: "." (group): updating`,
+ `4: sync remote: files: "." (group): chmodding drwx------`,
+ `3: sync remote: files: ".": queuing trigger on "."`,
+ `4: sync remote: files: "dir" (group): unchanged`,
+ `4: sync remote: files: "dir/file" (group): unchanged`,
+ },
+ nil,
+ },
+
+ {
+ "triggers: change middle",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger .",
+ },
+ },
+ "dir": {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir",
+ },
+ },
+ "dir/file": {
+ Path: "dir/file",
+ Mode: 0644,
+ Uid: -1,
+ Gid: -1,
+ Data: []byte("content\n"),
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir/file",
+ },
+ },
+ },
+ },
+ func() {
+ createDirectory("dir", 0750)
+ createFile("dir/file", "content\n", 0644)
+ },
+ []string{
+ ".",
+ "dir",
+ },
+ []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: sync remote: files: "." (group): unchanged`,
+ `4: sync remote: files: "dir" (group): permission differs drwxr-x--- -> drwxr-xr-x`,
+ `3: sync remote: files: "dir" (group): updating`,
+ `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
+ `3: sync remote: files: "dir": queuing trigger on "."`,
+ `3: sync remote: files: "dir": queuing trigger on "dir"`,
+ `4: sync remote: files: "dir/file" (group): unchanged`,
+ },
+ nil,
+ },
+
+ {
+ "triggers: change leaf",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger .",
+ },
+ },
+ "dir": {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir",
+ },
+ },
+ "dir/file": {
+ Path: "dir/file",
+ Mode: 0644,
+ Uid: -1,
+ Gid: -1,
+ Data: []byte("content\n"),
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir/file",
+ },
+ },
+ },
+ },
+ func() {
+ createDirectory("dir", 0755)
+ },
+ []string{
+ ".",
+ "dir",
+ "dir/file",
+ },
+ []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: sync remote: files: "." (group): unchanged`,
+ `4: sync remote: files: "dir" (group): unchanged`,
+ `4: sync remote: files: "dir/file" (group): will create`,
+ `3: sync remote: files: "dir/file" (group): creating`,
+ `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
+ `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
+ `3: sync remote: files: "dir/file": queuing trigger on "."`,
+ `3: sync remote: files: "dir/file": queuing trigger on "dir"`,
+ `3: sync remote: files: "dir/file": queuing trigger on "dir/file"`,
+ },
+ nil,
+ },
+
+ {
+ "triggers: multiple changes",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ ".": {
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger .",
+ },
+ },
+ "dir": {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir",
+ },
+ },
+ "dir/file": {
+ Path: "dir/file",
+ Mode: 0644,
+ Uid: -1,
+ Gid: -1,
+ Data: []byte("content\n"),
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger dir/file",
+ },
+ },
+ },
+ },
+ nil,
+ []string{
+ ".",
+ "dir",
+ "dir/file",
+ },
+ []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: sync remote: files: "." (group): unchanged`,
+ `4: sync remote: files: "dir" (group): will create`,
+ `3: sync remote: files: "dir" (group): creating`,
+ `4: sync remote: files: "dir" (group): creating directory`,
+ `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
+ fmt.Sprintf(`4: sync remote: files: "dir" (group): chowning %d/%d`, uid, gid),
+ `3: sync remote: files: "dir": queuing trigger on "."`,
+ `3: sync remote: files: "dir": queuing trigger on "dir"`,
+ `4: sync remote: files: "dir/file" (group): will create`,
+ `3: sync remote: files: "dir/file" (group): creating`,
+ `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
+ `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
+ `4: sync remote: files: "dir/file": skipping trigger on ".", already active`,
+ `4: sync remote: files: "dir/file": skipping trigger on "dir", already active`,
+ `3: sync remote: files: "dir/file": queuing trigger on "dir/file"`,
+ },
+ nil,
+ },
+
+ {
+ "triggers: absolute paths",
+ safcm.MsgSyncReq{
+ Files: map[string]*safcm.File{
+ "/": {
+ Path: "/",
+ Mode: fs.ModeDir | 0755,
+ User: "root",
+ Uid: -1,
+ Group: "root",
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger /",
+ },
+ },
+ "/tmp": {
+ Path: "/tmp",
+ Mode: fs.ModeDir | 0777 | fs.ModeSticky,
+ User: "root",
+ Uid: -1,
+ Group: "root",
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger /tmp",
+ },
+ },
+ tmpTestFilePath: {
+ Path: tmpTestFilePath,
+ Mode: 0600,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ TriggerCommands: []string{
+ "echo trigger /tmp/file",
+ },
+ },
+ },
+ },
+ func() {
+ // This is slightly racy but the file name
+ // should be rare enough that this isn't an
+ // issue
+ _, err := os.Stat(tmpTestFilePath)
+ if err == nil {
+ t.Fatalf("%q exists, aborting",
+ tmpTestFilePath)
+ }
+ },
+ []string{
+ "/",
+ "/tmp",
+ // Don't use variable for more robust test
+ "/tmp/safcm-sync-files-test-file",
+ },
+ []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: sync remote: files: "/" (group): unchanged`,
+ `4: sync remote: files: "/tmp" (group): unchanged`,
+ `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): will create`,
+ `3: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): creating`,
+ `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): creating temporary file "/tmp/.safcm-sync-files-test-file*"`,
+ `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): renaming "/tmp/.safcm-sync-files-test-fileRND"`,
+ `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/"`,
+ `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/tmp"`,
+ `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/tmp/safcm-sync-files-test-file"`,
+ },
+ nil,
+ },
+ }
+
+ for _, tc := range tests {
+ // 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,
+ name: tc.name,
+ })
+ s.setDefaults()
+
+ err := s.syncFiles()
+ // Ugly but the simplest way to compare errors (including nil)
+ if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", tc.expErr) {
+ t.Errorf("%s: err = %#v, want %#v",
+ tc.name, err, tc.expErr)
+ }
+ dbg := res.Wait()
+ // Remove random file names from result
+ for i, x := range dbg {
+ dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
+ }
+ if !reflect.DeepEqual(tc.expDbg, dbg) {
+ t.Errorf("%s: dbg: %s", tc.name,
+ cmp.Diff(tc.expDbg, dbg))
+ }
+
+ files, err := walkDir(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(tc.expFiles, files) {
+ t.Errorf("%s: files: %s", tc.name,
+ cmp.Diff(tc.expFiles, files))
+ }
+
+ if !reflect.DeepEqual(tc.expResp, s.resp) {
+ t.Errorf("%s: resp: %s", tc.name,
+ cmp.Diff(tc.expResp, s.resp))
+ }
+ if !reflect.DeepEqual(tc.triggers, s.triggers) {
+ t.Errorf("%s: triggers: %s", tc.name,
+ cmp.Diff(tc.triggers, s.triggers))
+ }
+ }
+
+ 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)
+
+ err = os.RemoveAll("testdata")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.Mkdir("testdata", 0700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ root := File{
+ Path: ".",
+ Mode: fs.ModeDir | 0700,
+ }
+ user, uid, group, gid := currentUserAndGroup()
+
+ tests := []struct {
+ name string
+ req safcm.MsgSyncReq
+ file *safcm.File
+ prepare func()
+ expChanged bool
+ expFiles []File
+ expResp safcm.MsgSyncResp
+ expDbg []string
+ expErr error
+ }{
+
+ // NOTE: Also update MsgSyncResp in safcm test cases when
+ // changing anything here!
+
+ // 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,
+ []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: sync remote: files: "file" (group): will create`,
+ `3: sync remote: files: "file" (group): creating`,
+ `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
+ `4: sync remote: 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,
+ []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: sync remote: files: "file" (group): will create`,
+ `3: sync remote: files: "file" (group): creating`,
+ `4: sync remote: 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() {
+ createFile("file", "content\n", 0644)
+ },
+ false,
+ []File{
+ root,
+ {
+ Path: "file",
+ Mode: 0644,
+ Data: []byte("content\n"),
+ },
+ },
+ safcm.MsgSyncResp{},
+ []string{
+ `4: sync remote: 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() {
+ createFile("file", "content\n", 0644)
+ },
+ false,
+ []File{
+ root,
+ {
+ Path: "file",
+ Mode: 0644,
+ Data: []byte("content\n"),
+ },
+ },
+ safcm.MsgSyncResp{},
+ []string{
+ `4: sync remote: 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() {
+ createFile("file", "content\n", 0755)
+ },
+ true,
+ []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: sync remote: files: "file" (group): permission differs -rwxr-xr-x -> urwxr-xr-x`,
+ `3: sync remote: files: "file" (group): updating`,
+ `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
+ `4: sync remote: 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() {
+ createFile("file", "old content\n", 0644)
+ },
+ true,
+ []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: sync remote: files: "file" (group): content differs`,
+ `3: sync remote: files: "file" (group): updating`,
+ `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
+ `4: sync remote: 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,
+ []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: sync remote: files: "link" (group): will create`,
+ `3: sync remote: files: "link" (group): creating`,
+ `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
+ `4: sync remote: 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() {
+ createFile(".link8717895732742165505", "", 0600)
+ },
+ true,
+ []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: sync remote: files: "link" (group): will create`,
+ `3: sync remote: files: "link" (group): creating`,
+ `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
+ `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
+ `4: sync remote: 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,
+ []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: sync remote: files: "link" (group): will create`,
+ `3: sync remote: files: "link" (group): creating`,
+ `4: sync remote: 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() {
+ createSymlink("link", "target")
+ },
+ false,
+ []File{
+ root,
+ {
+ Path: "link",
+ Mode: fs.ModeSymlink | 0777,
+ Data: []byte("target"),
+ },
+ },
+ safcm.MsgSyncResp{},
+ []string{
+ `4: sync remote: 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() {
+ createSymlink("link", "old-target")
+ },
+ true,
+ []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: sync remote: files: "link" (group): content differs`,
+ `3: sync remote: files: "link" (group): updating`,
+ `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
+ `4: sync remote: 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,
+ []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: sync remote: files: "dir" (group): will create`,
+ `3: sync remote: files: "dir" (group): creating`,
+ `4: sync remote: files: "dir" (group): creating directory`,
+ `4: sync remote: files: "dir" (group): chmodding drwx---r-x`,
+ fmt.Sprintf(`4: sync remote: 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,
+ []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: sync remote: files: "dir" (group): will create`,
+ `3: sync remote: files: "dir" (group): creating`,
+ `4: sync remote: 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() {
+ createDirectory("dir", 0755)
+ },
+ false,
+ []File{
+ root,
+ {
+ Path: "dir",
+ Mode: fs.ModeDir | 0755,
+ },
+ },
+ safcm.MsgSyncResp{},
+ []string{
+ `4: sync remote: 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() {
+ createDirectory("dir", 0500|fs.ModeSticky)
+ },
+ true,
+ []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: sync remote: files: "dir" (group): permission differs dtr-x------ -> dgrwxr-xr-x`,
+ `3: sync remote: files: "dir" (group): updating`,
+ `4: sync remote: 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() {
+ createFile("path", "content\n", 0644)
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs ---------- -> d---------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): removing (due to type change)`,
+ `4: sync remote: files: "path" (group): creating directory`,
+ `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
+ fmt.Sprintf(`4: sync remote: 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() {
+ createFile("path", "content\n", 0644)
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs ---------- -> L---------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
+ `4: sync remote: 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() {
+ createSymlink("path", "target")
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs L--------- -> ----------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
+ `4: sync remote: 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() {
+ createSymlink("path", "target")
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs L--------- -> d---------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): removing (due to type change)`,
+ `4: sync remote: files: "path" (group): creating directory`,
+ `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
+ fmt.Sprintf(`4: sync remote: 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() {
+ createDirectory("path", 0777)
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs d--------- -> ----------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): removing (due to type change)`,
+ `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
+ `4: sync remote: files: "path" (group): renaming "./.pathRND"`,
+ },
+ nil,
+ },
+
+ {
+ "change: directory to symlink",
+ safcm.MsgSyncReq{},
+ &safcm.File{
+ Path: "path",
+ Mode: fs.ModeSymlink | 0777,
+ Uid: -1,
+ Gid: -1,
+ OrigGroup: "group",
+ Data: []byte("target"),
+ },
+ func() {
+ createDirectory("path", 0777)
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs d--------- -> L---------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): removing (due to type change)`,
+ `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
+ `4: sync remote: 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() {
+ createFifo("path", 0666)
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs p--------- -> ----------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
+ `4: sync remote: 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() {
+ createFifo("path", 0666)
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs p--------- -> L---------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
+ `4: sync remote: 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() {
+ createFifo("path", 0666)
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs p--------- -> d---------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): removing (due to type change)`,
+ `4: sync remote: files: "path" (group): creating directory`,
+ `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
+ fmt.Sprintf(`4: sync remote: 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() {
+ createFile("path", "target", 0644)
+ },
+ true,
+ []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: sync remote: files: "path" (group): type differs ---------- -> L---------`,
+ `3: sync remote: files: "path" (group): updating`,
+ `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
+ `4: sync remote: files: "path" (group): renaming ".pathRND"`,
+ },
+ 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() {
+ createFile("file", `this
+is
+file
+!
+`, 0644)
+ },
+ true,
+ []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: sync remote: files: "file" (group): content differs`,
+ `3: sync remote: files: "file" (group): updating`,
+ `4: sync remote: 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() {
+ createFile("file", "\x00\x01\x02", 0644)
+ },
+ true,
+ []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, cannot show diff",
+ },
+ },
+ },
+ []string{
+ `4: sync remote: files: "file" (group): content differs`,
+ `3: sync remote: files: "file" (group): updating`,
+ `4: sync remote: 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() {
+ createFile("file", "\x00\x01\x02", 0644)
+ },
+ true,
+ []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 @@
+-<binary content>
++content
+
+`,
+ },
+ },
+ },
+ []string{
+ `4: sync remote: files: "file" (group): content differs`,
+ `3: sync remote: files: "file" (group): updating`,
+ `4: sync remote: 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() {
+ createFile("file", "content\n", 0644)
+ },
+ true,
+ []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
++<binary content>
+
+`,
+ },
+ },
+ },
+ []string{
+ `4: sync remote: files: "file" (group): content differs`,
+ `3: sync remote: files: "file" (group): updating`,
+ `4: sync remote: files: "file" (group): dry-run, skipping changes`,
+ },
+ nil,
+ },
+ }
+
+ for _, tc := range tests {
+ // 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,
+ name: tc.name,
+ })
+ s.setDefaults()
+
+ // Deterministic temporary symlink names
+ rand.Seed(0)
+
+ var changed bool
+ err := s.syncFile(tc.file, &changed)
+ // Ugly but the simplest way to compare errors (including nil)
+ if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", tc.expErr) {
+ t.Errorf("%s: err = %#v, want %#v",
+ tc.name, err, tc.expErr)
+ }
+ dbg := res.Wait()
+ // Remove random file names from result
+ for i, x := range dbg {
+ dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
+ }
+ if !reflect.DeepEqual(tc.expDbg, dbg) {
+ t.Errorf("%s: dbg: %s", tc.name,
+ cmp.Diff(tc.expDbg, dbg))
+ }
+
+ files, err := walkDir(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(tc.expFiles, files) {
+ t.Errorf("%s: files: %s", tc.name,
+ cmp.Diff(tc.expFiles, files))
+ }
+
+ if tc.expChanged != changed {
+ t.Errorf("%s: changed = %#v, want %#v",
+ tc.name, changed, tc.expChanged)
+ }
+ if !reflect.DeepEqual(tc.expResp, s.resp) {
+ t.Errorf("%s: resp: %s", tc.name,
+ cmp.Diff(tc.expResp, s.resp))
+ }
+ }
+
+ if !t.Failed() {
+ err = os.RemoveAll(filepath.Join(cwd, "testdata"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+// Helper functions
+
+func createFile(path string, data string, mode fs.FileMode) {
+ err := os.WriteFile(path, []byte(data), 0644)
+ if err != nil {
+ panic(err)
+ }
+ err = os.Chmod(path, mode)
+ if err != nil {
+ panic(err)
+ }
+}
+func createSymlink(path string, data string) {
+ err := os.Symlink(data, path)
+ if err != nil {
+ panic(err)
+ }
+}
+func createDirectory(path string, mode fs.FileMode) {
+ err := os.Mkdir(path, 0700)
+ if err != nil {
+ panic(err)
+ }
+ err = os.Chmod(path, mode)
+ if err != nil {
+ panic(err)
+ }
+}
+func createFifo(path string, mode fs.FileMode) {
+ err := syscall.Mkfifo(path, 0600)
+ if err != nil {
+ panic(err)
+ }
+ err = os.Chmod(path, mode)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func currentUserAndGroup() (string, int, string, int) {
+ u, err := user.Current()
+ if err != nil {
+ panic(err)
+ }
+ g, err := user.LookupGroupId(u.Gid)
+ if err != nil {
+ panic(err)
+ }
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ panic(err)
+ }
+ gid, err := strconv.Atoi(g.Gid)
+ if err != nil {
+ panic(err)
+ }
+ return u.Username, uid, g.Name, gid
+}