// 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 . package sync import ( "fmt" "io/fs" "math/rand" "os" "path/filepath" "regexp" "testing" "ruderich.org/simon/safcm" ft "ruderich.org/simon/safcm/cmd/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) 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 anything here! // 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{ ".": { 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, []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: 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", false, 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() { 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: sync remote: files: "." (group): unchanged`, `4: sync remote: files: "dir" (group): unchanged`, `4: sync remote: files: "dir/file" (group): unchanged`, }, nil, }, { "invalid File: user", false, safcm.MsgSyncReq{ Files: map[string]*safcm.File{ ".": { Path: ".", Mode: fs.ModeDir | 0700, User: "user", Uid: 1, Gid: -1, OrigGroup: "group", }, }, }, 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{ ".": { Path: ".", Mode: fs.ModeDir | 0700, Uid: -1, Group: "group", Gid: 1, OrigGroup: "group", }, }, }, 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{ "/": { Path: "/", Mode: fs.ModeDir | 0755, Uid: 0, Gid: 0, OrigGroup: "group", }, "/etc": { Path: "/etc", Mode: fs.ModeDir | 0755, Uid: 0, Gid: 0, OrigGroup: "group", }, "/tmp": { Path: "/tmp", Mode: fs.ModeDir | 0777 | fs.ModeSticky, Uid: 0, Gid: 0, OrigGroup: "group", }, }, }, nil, nil, []ft.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`, }, nil, }, { "triggers: no change", false, 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() { 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: sync remote: files: "." (group): unchanged`, `4: sync remote: files: "dir" (group): unchanged`, `4: sync remote: files: "dir/file" (group): unchanged`, }, nil, }, { "triggers: change root", false, 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) } 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: 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", false, 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() { 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: 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", false, 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() { 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: 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", false, 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", }, []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: 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", skipUnlessCiRun, safcm.MsgSyncReq{ Files: map[string]*safcm.File{ "/": { Path: "/", Mode: fs.ModeDir | 0755, Uid: 0, Gid: 0, OrigGroup: "group", TriggerCommands: []string{ "echo trigger /", }, }, "/tmp": { Path: "/tmp", Mode: fs.ModeDir | 0777 | fs.ModeSticky, Uid: 0, Gid: 0, OrigGroup: "group", TriggerCommands: []string{ "echo trigger /tmp", }, }, tmpTestFilePath: { Path: tmpTestFilePath, Mode: 0600, Uid: -1, Gid: -1, OrigGroup: "group", 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: 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 { 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, }) s.setDefaults() 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) 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 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, []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: 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, []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: 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() { ft.CreateFile("file", "content\n", 0644) }, false, []ft.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() { ft.CreateFile("file", "content\n", 0644) }, false, []ft.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() { 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: 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() { 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: 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, []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: 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() { 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: 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, []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: 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() { ft.CreateSymlink("link", "target") }, false, []ft.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() { 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: 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, []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: 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, []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: 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() { ft.CreateDirectory("dir", 0755) }, false, []ft.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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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: 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() { 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, 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() { 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: 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() { 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: 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 { 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, }) s.setDefaults() // 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) } } }