]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - cmd/safcm-remote/sync/files_test.go
First working version
[safcm/safcm.git] / cmd / safcm-remote / sync / files_test.go
1 // Copyright (C) 2021  Simon Ruderich
2 //
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16 package sync
17
18 import (
19         "fmt"
20         "io/fs"
21         "math/rand"
22         "os"
23         "os/user"
24         "path/filepath"
25         "reflect"
26         "regexp"
27         "strconv"
28         "syscall"
29         "testing"
30
31         "github.com/google/go-cmp/cmp"
32
33         "ruderich.org/simon/safcm"
34 )
35
36 type File struct {
37         Path string
38         Mode fs.FileMode
39         Data []byte
40 }
41
42 func walkDir(basePath string) ([]File, error) {
43         var res []File
44         err := filepath.WalkDir(basePath, func(path string, d fs.DirEntry, err error) error {
45                 if err != nil {
46                         return err
47                 }
48                 info, err := d.Info()
49                 if err != nil {
50                         return err
51                 }
52                 rel, err := filepath.Rel(basePath, path)
53                 if err != nil {
54                         return err
55                 }
56
57                 f := File{
58                         Path: rel,
59                         Mode: info.Mode(),
60                 }
61                 if f.Mode.Type() == 0 {
62                         x, err := os.ReadFile(path)
63                         if err != nil {
64                                 return err
65                         }
66                         f.Data = x
67                 } else if f.Mode.Type() == fs.ModeSymlink {
68                         x, err := os.Readlink(path)
69                         if err != nil {
70                                 return err
71                         }
72                         f.Data = []byte(x)
73                 }
74                 res = append(res, f)
75                 return nil
76         })
77         if err != nil {
78                 return nil, err
79         }
80         return res, nil
81 }
82
83 var randFilesRegexp = regexp.MustCompile(`\d+"$`)
84
85 func TestSyncFiles(t *testing.T) {
86         cwd, err := os.Getwd()
87         if err != nil {
88                 t.Fatal(err)
89         }
90         defer os.Chdir(cwd)
91
92         err = os.RemoveAll("testdata")
93         if err != nil {
94                 t.Fatal(err)
95         }
96         err = os.Mkdir("testdata", 0700)
97         if err != nil {
98                 t.Fatal(err)
99         }
100
101         root := File{
102                 Path: ".",
103                 Mode: fs.ModeDir | 0700,
104         }
105         user, uid, group, gid := currentUserAndGroup()
106
107         tmpTestFilePath := "/tmp/safcm-sync-files-test-file"
108
109         tests := []struct {
110                 name     string
111                 req      safcm.MsgSyncReq
112                 prepare  func()
113                 triggers []string
114                 expFiles []File
115                 expResp  safcm.MsgSyncResp
116                 expDbg   []string
117                 expErr   error
118         }{
119
120                 // NOTE: Also update MsgSyncResp in safcm test cases when
121                 // changing anything here!
122
123                 // See TestSyncFile() for most file related tests. This
124                 // function only tests the overall results and triggers.
125
126                 {
127                         "basic: create",
128                         safcm.MsgSyncReq{
129                                 Files: map[string]*safcm.File{
130                                         ".": {
131                                                 Path:      ".",
132                                                 Mode:      fs.ModeDir | 0700,
133                                                 Uid:       -1,
134                                                 Gid:       -1,
135                                                 OrigGroup: "group",
136                                         },
137                                         "dir": {
138                                                 Path:      "dir",
139                                                 Mode:      fs.ModeDir | 0755,
140                                                 Uid:       -1,
141                                                 Gid:       -1,
142                                                 OrigGroup: "group",
143                                         },
144                                         "dir/file": {
145                                                 Path:      "dir/file",
146                                                 Mode:      0644,
147                                                 Uid:       -1,
148                                                 Gid:       -1,
149                                                 Data:      []byte("content\n"),
150                                                 OrigGroup: "group",
151                                         },
152                                 },
153                         },
154                         nil,
155                         nil,
156                         []File{
157                                 root,
158                                 {
159                                         Path: "dir",
160                                         Mode: fs.ModeDir | 0755,
161                                 },
162                                 {
163                                         Path: "dir/file",
164                                         Mode: 0644,
165                                         Data: []byte("content\n"),
166                                 },
167                         },
168                         safcm.MsgSyncResp{
169                                 FileChanges: []safcm.FileChange{
170                                         {
171                                                 Path:    "dir",
172                                                 Created: true,
173                                                 New: safcm.FileChangeInfo{
174                                                         Mode:  fs.ModeDir | 0755,
175                                                         User:  user,
176                                                         Uid:   uid,
177                                                         Group: group,
178                                                         Gid:   gid,
179                                                 },
180                                         },
181                                         {
182                                                 Path:    "dir/file",
183                                                 Created: true,
184                                                 New: safcm.FileChangeInfo{
185                                                         Mode:  0644,
186                                                         User:  user,
187                                                         Uid:   uid,
188                                                         Group: group,
189                                                         Gid:   gid,
190                                                 },
191                                         },
192                                 },
193                         },
194                         []string{
195                                 `4: sync remote: files: "." (group): unchanged`,
196                                 `4: sync remote: files: "dir" (group): will create`,
197                                 `3: sync remote: files: "dir" (group): creating`,
198                                 `4: sync remote: files: "dir" (group): creating directory`,
199                                 `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
200                                 fmt.Sprintf(`4: sync remote: files: "dir" (group): chowning %d/%d`, uid, gid),
201                                 `4: sync remote: files: "dir/file" (group): will create`,
202                                 `3: sync remote: files: "dir/file" (group): creating`,
203                                 `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
204                                 `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
205                         },
206                         nil,
207                 },
208
209                 {
210                         "basic: no change",
211                         safcm.MsgSyncReq{
212                                 Files: map[string]*safcm.File{
213                                         ".": {
214                                                 Path:      ".",
215                                                 Mode:      fs.ModeDir | 0700,
216                                                 Uid:       -1,
217                                                 Gid:       -1,
218                                                 OrigGroup: "group",
219                                         },
220                                         "dir": {
221                                                 Path:      "dir",
222                                                 Mode:      fs.ModeDir | 0755,
223                                                 Uid:       -1,
224                                                 Gid:       -1,
225                                                 OrigGroup: "group",
226                                         },
227                                         "dir/file": {
228                                                 Path:      "dir/file",
229                                                 Mode:      0644,
230                                                 Uid:       -1,
231                                                 Gid:       -1,
232                                                 Data:      []byte("content\n"),
233                                                 OrigGroup: "group",
234                                         },
235                                 },
236                         },
237                         func() {
238                                 createDirectory("dir", 0755)
239                                 createFile("dir/file", "content\n", 0644)
240                         },
241                         nil,
242                         []File{
243                                 root,
244                                 {
245                                         Path: "dir",
246                                         Mode: fs.ModeDir | 0755,
247                                 },
248                                 {
249                                         Path: "dir/file",
250                                         Mode: 0644,
251                                         Data: []byte("content\n"),
252                                 },
253                         },
254                         safcm.MsgSyncResp{},
255                         []string{
256                                 `4: sync remote: files: "." (group): unchanged`,
257                                 `4: sync remote: files: "dir" (group): unchanged`,
258                                 `4: sync remote: files: "dir/file" (group): unchanged`,
259                         },
260                         nil,
261                 },
262
263                 {
264                         "invalid File: user",
265                         safcm.MsgSyncReq{
266                                 Files: map[string]*safcm.File{
267                                         ".": {
268                                                 Path:      ".",
269                                                 Mode:      fs.ModeDir | 0700,
270                                                 User:      "user",
271                                                 Uid:       1,
272                                                 Gid:       -1,
273                                                 OrigGroup: "group",
274                                         },
275                                 },
276                         },
277                         nil,
278                         nil,
279                         []File{
280                                 root,
281                         },
282                         safcm.MsgSyncResp{},
283                         nil,
284                         fmt.Errorf("\".\": cannot set both User (\"user\") and Uid (1)"),
285                 },
286                 {
287                         "invalid File: group",
288                         safcm.MsgSyncReq{
289                                 Files: map[string]*safcm.File{
290                                         ".": {
291                                                 Path:      ".",
292                                                 Mode:      fs.ModeDir | 0700,
293                                                 Uid:       -1,
294                                                 Group:     "group",
295                                                 Gid:       1,
296                                                 OrigGroup: "group",
297                                         },
298                                 },
299                         },
300                         nil,
301                         nil,
302                         []File{
303                                 root,
304                         },
305                         safcm.MsgSyncResp{},
306                         nil,
307                         fmt.Errorf("\".\": cannot set both Group (\"group\") and Gid (1)"),
308                 },
309
310                 {
311                         // We use relative paths for most tests because we
312                         // don't want to modify the running system. Use this
313                         // test (and the one below for triggers) as a basic
314                         // check that absolute paths work.
315                         "absolute paths: no change",
316                         safcm.MsgSyncReq{
317                                 Files: map[string]*safcm.File{
318                                         "/": {
319                                                 Path:      "/",
320                                                 Mode:      fs.ModeDir | 0755,
321                                                 User:      "root",
322                                                 Uid:       -1,
323                                                 Group:     "root",
324                                                 Gid:       -1,
325                                                 OrigGroup: "group",
326                                         },
327                                         "/etc": {
328                                                 Path:      "/etc",
329                                                 Mode:      fs.ModeDir | 0755,
330                                                 User:      "root",
331                                                 Uid:       -1,
332                                                 Group:     "root",
333                                                 Gid:       -1,
334                                                 OrigGroup: "group",
335                                         },
336                                         "/tmp": {
337                                                 Path:      "/tmp",
338                                                 Mode:      fs.ModeDir | 0777 | fs.ModeSticky,
339                                                 User:      "root",
340                                                 Uid:       -1,
341                                                 Group:     "root",
342                                                 Gid:       -1,
343                                                 OrigGroup: "group",
344                                         },
345                                         "/var/tmp": {
346                                                 Path:      "/var/tmp",
347                                                 Mode:      fs.ModeDir | 0777 | fs.ModeSticky,
348                                                 User:      "root",
349                                                 Uid:       -1,
350                                                 Group:     "root",
351                                                 Gid:       -1,
352                                                 OrigGroup: "group",
353                                         },
354                                 },
355                         },
356                         nil,
357                         nil,
358                         []File{
359                                 root,
360                         },
361                         safcm.MsgSyncResp{},
362                         []string{
363                                 `4: sync remote: files: "/" (group): unchanged`,
364                                 `4: sync remote: files: "/etc" (group): unchanged`,
365                                 `4: sync remote: files: "/tmp" (group): unchanged`,
366                                 `4: sync remote: files: "/var/tmp" (group): unchanged`,
367                         },
368                         nil,
369                 },
370
371                 {
372                         "triggers: no change",
373                         safcm.MsgSyncReq{
374                                 Files: map[string]*safcm.File{
375                                         ".": {
376                                                 Path:      ".",
377                                                 Mode:      fs.ModeDir | 0700,
378                                                 Uid:       -1,
379                                                 Gid:       -1,
380                                                 OrigGroup: "group",
381                                                 TriggerCommands: []string{
382                                                         "echo trigger .",
383                                                 },
384                                         },
385                                         "dir": {
386                                                 Path:      "dir",
387                                                 Mode:      fs.ModeDir | 0755,
388                                                 Uid:       -1,
389                                                 Gid:       -1,
390                                                 OrigGroup: "group",
391                                                 TriggerCommands: []string{
392                                                         "echo trigger dir",
393                                                 },
394                                         },
395                                         "dir/file": {
396                                                 Path:      "dir/file",
397                                                 Mode:      0644,
398                                                 Uid:       -1,
399                                                 Gid:       -1,
400                                                 Data:      []byte("content\n"),
401                                                 OrigGroup: "group",
402                                                 TriggerCommands: []string{
403                                                         "echo trigger dir/file",
404                                                 },
405                                         },
406                                 },
407                         },
408                         func() {
409                                 createDirectory("dir", 0755)
410                                 createFile("dir/file", "content\n", 0644)
411                         },
412                         nil,
413                         []File{
414                                 root,
415                                 {
416                                         Path: "dir",
417                                         Mode: fs.ModeDir | 0755,
418                                 },
419                                 {
420                                         Path: "dir/file",
421                                         Mode: 0644,
422                                         Data: []byte("content\n"),
423                                 },
424                         },
425                         safcm.MsgSyncResp{},
426                         []string{
427                                 `4: sync remote: files: "." (group): unchanged`,
428                                 `4: sync remote: files: "dir" (group): unchanged`,
429                                 `4: sync remote: files: "dir/file" (group): unchanged`,
430                         },
431                         nil,
432                 },
433
434                 {
435                         "triggers: change root",
436                         safcm.MsgSyncReq{
437                                 Files: map[string]*safcm.File{
438                                         ".": {
439                                                 Path:      ".",
440                                                 Mode:      fs.ModeDir | 0700,
441                                                 Uid:       -1,
442                                                 Gid:       -1,
443                                                 OrigGroup: "group",
444                                                 TriggerCommands: []string{
445                                                         "echo trigger .",
446                                                 },
447                                         },
448                                         "dir": {
449                                                 Path:      "dir",
450                                                 Mode:      fs.ModeDir | 0755,
451                                                 Uid:       -1,
452                                                 Gid:       -1,
453                                                 OrigGroup: "group",
454                                                 TriggerCommands: []string{
455                                                         "echo trigger dir",
456                                                 },
457                                         },
458                                         "dir/file": {
459                                                 Path:      "dir/file",
460                                                 Mode:      0644,
461                                                 Uid:       -1,
462                                                 Gid:       -1,
463                                                 Data:      []byte("content\n"),
464                                                 OrigGroup: "group",
465                                                 TriggerCommands: []string{
466                                                         "echo trigger dir/file",
467                                                 },
468                                         },
469                                 },
470                         },
471                         func() {
472                                 err = os.Chmod(".", 0750)
473                                 if err != nil {
474                                         panic(err)
475                                 }
476                                 createDirectory("dir", 0755)
477                                 createFile("dir/file", "content\n", 0644)
478                         },
479                         []string{
480                                 ".",
481                         },
482                         []File{
483                                 root,
484                                 {
485                                         Path: "dir",
486                                         Mode: fs.ModeDir | 0755,
487                                 },
488                                 {
489                                         Path: "dir/file",
490                                         Mode: 0644,
491                                         Data: []byte("content\n"),
492                                 },
493                         },
494                         safcm.MsgSyncResp{
495                                 FileChanges: []safcm.FileChange{
496                                         {
497                                                 Path: ".",
498                                                 Old: safcm.FileChangeInfo{
499                                                         Mode:  fs.ModeDir | 0750,
500                                                         User:  user,
501                                                         Uid:   uid,
502                                                         Group: group,
503                                                         Gid:   gid,
504                                                 },
505                                                 New: safcm.FileChangeInfo{
506                                                         Mode:  fs.ModeDir | 0700,
507                                                         User:  user,
508                                                         Uid:   uid,
509                                                         Group: group,
510                                                         Gid:   gid,
511                                                 },
512                                         },
513                                 },
514                         },
515                         []string{
516                                 `4: sync remote: files: "." (group): permission differs drwxr-x--- -> drwx------`,
517                                 `3: sync remote: files: "." (group): updating`,
518                                 `4: sync remote: files: "." (group): chmodding drwx------`,
519                                 `3: sync remote: files: ".": queuing trigger on "."`,
520                                 `4: sync remote: files: "dir" (group): unchanged`,
521                                 `4: sync remote: files: "dir/file" (group): unchanged`,
522                         },
523                         nil,
524                 },
525
526                 {
527                         "triggers: change middle",
528                         safcm.MsgSyncReq{
529                                 Files: map[string]*safcm.File{
530                                         ".": {
531                                                 Path:      ".",
532                                                 Mode:      fs.ModeDir | 0700,
533                                                 Uid:       -1,
534                                                 Gid:       -1,
535                                                 OrigGroup: "group",
536                                                 TriggerCommands: []string{
537                                                         "echo trigger .",
538                                                 },
539                                         },
540                                         "dir": {
541                                                 Path:      "dir",
542                                                 Mode:      fs.ModeDir | 0755,
543                                                 Uid:       -1,
544                                                 Gid:       -1,
545                                                 OrigGroup: "group",
546                                                 TriggerCommands: []string{
547                                                         "echo trigger dir",
548                                                 },
549                                         },
550                                         "dir/file": {
551                                                 Path:      "dir/file",
552                                                 Mode:      0644,
553                                                 Uid:       -1,
554                                                 Gid:       -1,
555                                                 Data:      []byte("content\n"),
556                                                 OrigGroup: "group",
557                                                 TriggerCommands: []string{
558                                                         "echo trigger dir/file",
559                                                 },
560                                         },
561                                 },
562                         },
563                         func() {
564                                 createDirectory("dir", 0750)
565                                 createFile("dir/file", "content\n", 0644)
566                         },
567                         []string{
568                                 ".",
569                                 "dir",
570                         },
571                         []File{
572                                 root,
573                                 {
574                                         Path: "dir",
575                                         Mode: fs.ModeDir | 0755,
576                                 },
577                                 {
578                                         Path: "dir/file",
579                                         Mode: 0644,
580                                         Data: []byte("content\n"),
581                                 },
582                         },
583                         safcm.MsgSyncResp{
584                                 FileChanges: []safcm.FileChange{
585                                         {
586                                                 Path: "dir",
587                                                 Old: safcm.FileChangeInfo{
588                                                         Mode:  fs.ModeDir | 0750,
589                                                         User:  user,
590                                                         Uid:   uid,
591                                                         Group: group,
592                                                         Gid:   gid,
593                                                 },
594                                                 New: safcm.FileChangeInfo{
595                                                         Mode:  fs.ModeDir | 0755,
596                                                         User:  user,
597                                                         Uid:   uid,
598                                                         Group: group,
599                                                         Gid:   gid,
600                                                 },
601                                         },
602                                 },
603                         },
604                         []string{
605                                 `4: sync remote: files: "." (group): unchanged`,
606                                 `4: sync remote: files: "dir" (group): permission differs drwxr-x--- -> drwxr-xr-x`,
607                                 `3: sync remote: files: "dir" (group): updating`,
608                                 `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
609                                 `3: sync remote: files: "dir": queuing trigger on "."`,
610                                 `3: sync remote: files: "dir": queuing trigger on "dir"`,
611                                 `4: sync remote: files: "dir/file" (group): unchanged`,
612                         },
613                         nil,
614                 },
615
616                 {
617                         "triggers: change leaf",
618                         safcm.MsgSyncReq{
619                                 Files: map[string]*safcm.File{
620                                         ".": {
621                                                 Path:      ".",
622                                                 Mode:      fs.ModeDir | 0700,
623                                                 Uid:       -1,
624                                                 Gid:       -1,
625                                                 OrigGroup: "group",
626                                                 TriggerCommands: []string{
627                                                         "echo trigger .",
628                                                 },
629                                         },
630                                         "dir": {
631                                                 Path:      "dir",
632                                                 Mode:      fs.ModeDir | 0755,
633                                                 Uid:       -1,
634                                                 Gid:       -1,
635                                                 OrigGroup: "group",
636                                                 TriggerCommands: []string{
637                                                         "echo trigger dir",
638                                                 },
639                                         },
640                                         "dir/file": {
641                                                 Path:      "dir/file",
642                                                 Mode:      0644,
643                                                 Uid:       -1,
644                                                 Gid:       -1,
645                                                 Data:      []byte("content\n"),
646                                                 OrigGroup: "group",
647                                                 TriggerCommands: []string{
648                                                         "echo trigger dir/file",
649                                                 },
650                                         },
651                                 },
652                         },
653                         func() {
654                                 createDirectory("dir", 0755)
655                         },
656                         []string{
657                                 ".",
658                                 "dir",
659                                 "dir/file",
660                         },
661                         []File{
662                                 root,
663                                 {
664                                         Path: "dir",
665                                         Mode: fs.ModeDir | 0755,
666                                 },
667                                 {
668                                         Path: "dir/file",
669                                         Mode: 0644,
670                                         Data: []byte("content\n"),
671                                 },
672                         },
673                         safcm.MsgSyncResp{
674                                 FileChanges: []safcm.FileChange{
675                                         {
676                                                 Path:    "dir/file",
677                                                 Created: true,
678                                                 New: safcm.FileChangeInfo{
679                                                         Mode:  0644,
680                                                         User:  user,
681                                                         Uid:   uid,
682                                                         Group: group,
683                                                         Gid:   gid,
684                                                 },
685                                         },
686                                 },
687                         },
688                         []string{
689                                 `4: sync remote: files: "." (group): unchanged`,
690                                 `4: sync remote: files: "dir" (group): unchanged`,
691                                 `4: sync remote: files: "dir/file" (group): will create`,
692                                 `3: sync remote: files: "dir/file" (group): creating`,
693                                 `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
694                                 `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
695                                 `3: sync remote: files: "dir/file": queuing trigger on "."`,
696                                 `3: sync remote: files: "dir/file": queuing trigger on "dir"`,
697                                 `3: sync remote: files: "dir/file": queuing trigger on "dir/file"`,
698                         },
699                         nil,
700                 },
701
702                 {
703                         "triggers: multiple changes",
704                         safcm.MsgSyncReq{
705                                 Files: map[string]*safcm.File{
706                                         ".": {
707                                                 Path:      ".",
708                                                 Mode:      fs.ModeDir | 0700,
709                                                 Uid:       -1,
710                                                 Gid:       -1,
711                                                 OrigGroup: "group",
712                                                 TriggerCommands: []string{
713                                                         "echo trigger .",
714                                                 },
715                                         },
716                                         "dir": {
717                                                 Path:      "dir",
718                                                 Mode:      fs.ModeDir | 0755,
719                                                 Uid:       -1,
720                                                 Gid:       -1,
721                                                 OrigGroup: "group",
722                                                 TriggerCommands: []string{
723                                                         "echo trigger dir",
724                                                 },
725                                         },
726                                         "dir/file": {
727                                                 Path:      "dir/file",
728                                                 Mode:      0644,
729                                                 Uid:       -1,
730                                                 Gid:       -1,
731                                                 Data:      []byte("content\n"),
732                                                 OrigGroup: "group",
733                                                 TriggerCommands: []string{
734                                                         "echo trigger dir/file",
735                                                 },
736                                         },
737                                 },
738                         },
739                         nil,
740                         []string{
741                                 ".",
742                                 "dir",
743                                 "dir/file",
744                         },
745                         []File{
746                                 root,
747                                 {
748                                         Path: "dir",
749                                         Mode: fs.ModeDir | 0755,
750                                 },
751                                 {
752                                         Path: "dir/file",
753                                         Mode: 0644,
754                                         Data: []byte("content\n"),
755                                 },
756                         },
757                         safcm.MsgSyncResp{
758                                 FileChanges: []safcm.FileChange{
759                                         {
760                                                 Path:    "dir",
761                                                 Created: true,
762                                                 New: safcm.FileChangeInfo{
763                                                         Mode:  fs.ModeDir | 0755,
764                                                         User:  user,
765                                                         Uid:   uid,
766                                                         Group: group,
767                                                         Gid:   gid,
768                                                 },
769                                         },
770                                         {
771                                                 Path:    "dir/file",
772                                                 Created: true,
773                                                 New: safcm.FileChangeInfo{
774                                                         Mode:  0644,
775                                                         User:  user,
776                                                         Uid:   uid,
777                                                         Group: group,
778                                                         Gid:   gid,
779                                                 },
780                                         },
781                                 },
782                         },
783                         []string{
784                                 `4: sync remote: files: "." (group): unchanged`,
785                                 `4: sync remote: files: "dir" (group): will create`,
786                                 `3: sync remote: files: "dir" (group): creating`,
787                                 `4: sync remote: files: "dir" (group): creating directory`,
788                                 `4: sync remote: files: "dir" (group): chmodding drwxr-xr-x`,
789                                 fmt.Sprintf(`4: sync remote: files: "dir" (group): chowning %d/%d`, uid, gid),
790                                 `3: sync remote: files: "dir": queuing trigger on "."`,
791                                 `3: sync remote: files: "dir": queuing trigger on "dir"`,
792                                 `4: sync remote: files: "dir/file" (group): will create`,
793                                 `3: sync remote: files: "dir/file" (group): creating`,
794                                 `4: sync remote: files: "dir/file" (group): creating temporary file "dir/.file*"`,
795                                 `4: sync remote: files: "dir/file" (group): renaming "dir/.fileRND"`,
796                                 `4: sync remote: files: "dir/file": skipping trigger on ".", already active`,
797                                 `4: sync remote: files: "dir/file": skipping trigger on "dir", already active`,
798                                 `3: sync remote: files: "dir/file": queuing trigger on "dir/file"`,
799                         },
800                         nil,
801                 },
802
803                 {
804                         "triggers: absolute paths",
805                         safcm.MsgSyncReq{
806                                 Files: map[string]*safcm.File{
807                                         "/": {
808                                                 Path:      "/",
809                                                 Mode:      fs.ModeDir | 0755,
810                                                 User:      "root",
811                                                 Uid:       -1,
812                                                 Group:     "root",
813                                                 Gid:       -1,
814                                                 OrigGroup: "group",
815                                                 TriggerCommands: []string{
816                                                         "echo trigger /",
817                                                 },
818                                         },
819                                         "/tmp": {
820                                                 Path:      "/tmp",
821                                                 Mode:      fs.ModeDir | 0777 | fs.ModeSticky,
822                                                 User:      "root",
823                                                 Uid:       -1,
824                                                 Group:     "root",
825                                                 Gid:       -1,
826                                                 OrigGroup: "group",
827                                                 TriggerCommands: []string{
828                                                         "echo trigger /tmp",
829                                                 },
830                                         },
831                                         tmpTestFilePath: {
832                                                 Path:      tmpTestFilePath,
833                                                 Mode:      0600,
834                                                 Uid:       -1,
835                                                 Gid:       -1,
836                                                 OrigGroup: "group",
837                                                 TriggerCommands: []string{
838                                                         "echo trigger /tmp/file",
839                                                 },
840                                         },
841                                 },
842                         },
843                         func() {
844                                 // This is slightly racy but the file name
845                                 // should be rare enough that this isn't an
846                                 // issue
847                                 _, err := os.Stat(tmpTestFilePath)
848                                 if err == nil {
849                                         t.Fatalf("%q exists, aborting",
850                                                 tmpTestFilePath)
851                                 }
852                         },
853                         []string{
854                                 "/",
855                                 "/tmp",
856                                 // Don't use variable for more robust test
857                                 "/tmp/safcm-sync-files-test-file",
858                         },
859                         []File{
860                                 root,
861                         },
862                         safcm.MsgSyncResp{
863                                 FileChanges: []safcm.FileChange{
864                                         {
865                                                 Path:    "/tmp/safcm-sync-files-test-file",
866                                                 Created: true,
867                                                 New: safcm.FileChangeInfo{
868                                                         Mode:  0600,
869                                                         User:  user,
870                                                         Uid:   uid,
871                                                         Group: group,
872                                                         Gid:   gid,
873                                                 },
874                                         },
875                                 },
876                         },
877                         []string{
878                                 `4: sync remote: files: "/" (group): unchanged`,
879                                 `4: sync remote: files: "/tmp" (group): unchanged`,
880                                 `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): will create`,
881                                 `3: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): creating`,
882                                 `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): creating temporary file "/tmp/.safcm-sync-files-test-file*"`,
883                                 `4: sync remote: files: "/tmp/safcm-sync-files-test-file" (group): renaming "/tmp/.safcm-sync-files-test-fileRND"`,
884                                 `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/"`,
885                                 `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/tmp"`,
886                                 `3: sync remote: files: "/tmp/safcm-sync-files-test-file": queuing trigger on "/tmp/safcm-sync-files-test-file"`,
887                         },
888                         nil,
889                 },
890         }
891
892         for _, tc := range tests {
893                 // Create separate test directory for each test case
894                 path := filepath.Join(cwd, "testdata", "files-"+tc.name)
895                 err = os.Mkdir(path, 0700)
896                 if err != nil {
897                         t.Fatal(err)
898                 }
899                 err = os.Chdir(path)
900                 if err != nil {
901                         t.Fatal(err)
902                 }
903
904                 if tc.prepare != nil {
905                         tc.prepare()
906                 }
907
908                 s, res := prepareSync(tc.req, &testRunner{
909                         t:    t,
910                         name: tc.name,
911                 })
912                 s.setDefaults()
913
914                 err := s.syncFiles()
915                 // Ugly but the simplest way to compare errors (including nil)
916                 if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", tc.expErr) {
917                         t.Errorf("%s: err = %#v, want %#v",
918                                 tc.name, err, tc.expErr)
919                 }
920                 dbg := res.Wait()
921                 // Remove random file names from result
922                 for i, x := range dbg {
923                         dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
924                 }
925                 if !reflect.DeepEqual(tc.expDbg, dbg) {
926                         t.Errorf("%s: dbg: %s", tc.name,
927                                 cmp.Diff(tc.expDbg, dbg))
928                 }
929
930                 files, err := walkDir(path)
931                 if err != nil {
932                         t.Fatal(err)
933                 }
934                 if !reflect.DeepEqual(tc.expFiles, files) {
935                         t.Errorf("%s: files: %s", tc.name,
936                                 cmp.Diff(tc.expFiles, files))
937                 }
938
939                 if !reflect.DeepEqual(tc.expResp, s.resp) {
940                         t.Errorf("%s: resp: %s", tc.name,
941                                 cmp.Diff(tc.expResp, s.resp))
942                 }
943                 if !reflect.DeepEqual(tc.triggers, s.triggers) {
944                         t.Errorf("%s: triggers: %s", tc.name,
945                                 cmp.Diff(tc.triggers, s.triggers))
946                 }
947         }
948
949         os.Remove(tmpTestFilePath)
950         if !t.Failed() {
951                 err = os.RemoveAll(filepath.Join(cwd, "testdata"))
952                 if err != nil {
953                         t.Fatal(err)
954                 }
955         }
956 }
957
958 func TestSyncFile(t *testing.T) {
959         cwd, err := os.Getwd()
960         if err != nil {
961                 t.Fatal(err)
962         }
963         defer os.Chdir(cwd)
964
965         err = os.RemoveAll("testdata")
966         if err != nil {
967                 t.Fatal(err)
968         }
969         err = os.Mkdir("testdata", 0700)
970         if err != nil {
971                 t.Fatal(err)
972         }
973
974         root := File{
975                 Path: ".",
976                 Mode: fs.ModeDir | 0700,
977         }
978         user, uid, group, gid := currentUserAndGroup()
979
980         tests := []struct {
981                 name       string
982                 req        safcm.MsgSyncReq
983                 file       *safcm.File
984                 prepare    func()
985                 expChanged bool
986                 expFiles   []File
987                 expResp    safcm.MsgSyncResp
988                 expDbg     []string
989                 expErr     error
990         }{
991
992                 // NOTE: Also update MsgSyncResp in safcm test cases when
993                 // changing anything here!
994
995                 // TODO: Add tests for chown and run them only as root
996
997                 // Regular file
998
999                 {
1000                         "file: create",
1001                         safcm.MsgSyncReq{},
1002                         &safcm.File{
1003                                 Path:      "file",
1004                                 Mode:      0644,
1005                                 Uid:       -1,
1006                                 Gid:       -1,
1007                                 Data:      []byte("content\n"),
1008                                 OrigGroup: "group",
1009                         },
1010                         nil,
1011                         true,
1012                         []File{
1013                                 root,
1014                                 {
1015                                         Path: "file",
1016                                         Mode: 0644,
1017                                         Data: []byte("content\n"),
1018                                 },
1019                         },
1020                         safcm.MsgSyncResp{
1021                                 FileChanges: []safcm.FileChange{
1022                                         {
1023                                                 Path:    "file",
1024                                                 Created: true,
1025                                                 New: safcm.FileChangeInfo{
1026                                                         Mode:  0644,
1027                                                         User:  user,
1028                                                         Uid:   uid,
1029                                                         Group: group,
1030                                                         Gid:   gid,
1031                                                 },
1032                                         },
1033                                 },
1034                         },
1035                         []string{
1036                                 `4: sync remote: files: "file" (group): will create`,
1037                                 `3: sync remote: files: "file" (group): creating`,
1038                                 `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
1039                                 `4: sync remote: files: "file" (group): renaming "./.fileRND"`,
1040                         },
1041                         nil,
1042                 },
1043                 {
1044                         "file: create (dry-run)",
1045                         safcm.MsgSyncReq{
1046                                 DryRun: true,
1047                         },
1048                         &safcm.File{
1049                                 Path:      "file",
1050                                 Mode:      0644,
1051                                 Uid:       -1,
1052                                 Gid:       -1,
1053                                 Data:      []byte("content\n"),
1054                                 OrigGroup: "group",
1055                         },
1056                         nil,
1057                         true,
1058                         []File{root},
1059                         safcm.MsgSyncResp{
1060                                 FileChanges: []safcm.FileChange{
1061                                         {
1062                                                 Path:    "file",
1063                                                 Created: true,
1064                                                 New: safcm.FileChangeInfo{
1065                                                         Mode:  0644,
1066                                                         User:  user,
1067                                                         Uid:   uid,
1068                                                         Group: group,
1069                                                         Gid:   gid,
1070                                                 },
1071                                         },
1072                                 },
1073                         },
1074                         []string{
1075                                 `4: sync remote: files: "file" (group): will create`,
1076                                 `3: sync remote: files: "file" (group): creating`,
1077                                 `4: sync remote: files: "file" (group): dry-run, skipping changes`,
1078                         },
1079                         nil,
1080                 },
1081
1082                 {
1083                         "file: unchanged",
1084                         safcm.MsgSyncReq{},
1085                         &safcm.File{
1086                                 Path:      "file",
1087                                 Mode:      0644,
1088                                 Uid:       -1,
1089                                 Gid:       -1,
1090                                 Data:      []byte("content\n"),
1091                                 OrigGroup: "group",
1092                         },
1093                         func() {
1094                                 createFile("file", "content\n", 0644)
1095                         },
1096                         false,
1097                         []File{
1098                                 root,
1099                                 {
1100                                         Path: "file",
1101                                         Mode: 0644,
1102                                         Data: []byte("content\n"),
1103                                 },
1104                         },
1105                         safcm.MsgSyncResp{},
1106                         []string{
1107                                 `4: sync remote: files: "file" (group): unchanged`,
1108                         },
1109                         nil,
1110                 },
1111
1112                 {
1113                         "file: unchanged (non-default user-group)",
1114                         safcm.MsgSyncReq{},
1115                         &safcm.File{
1116                                 Path:      "file",
1117                                 Mode:      0644,
1118                                 User:      user,
1119                                 Uid:       -1,
1120                                 Group:     group,
1121                                 Gid:       -1,
1122                                 Data:      []byte("content\n"),
1123                                 OrigGroup: "group",
1124                         },
1125                         func() {
1126                                 createFile("file", "content\n", 0644)
1127                         },
1128                         false,
1129                         []File{
1130                                 root,
1131                                 {
1132                                         Path: "file",
1133                                         Mode: 0644,
1134                                         Data: []byte("content\n"),
1135                                 },
1136                         },
1137                         safcm.MsgSyncResp{},
1138                         []string{
1139                                 `4: sync remote: files: "file" (group): unchanged`,
1140                         },
1141                         nil,
1142                 },
1143
1144                 {
1145                         "file: permission",
1146                         safcm.MsgSyncReq{},
1147                         &safcm.File{
1148                                 Path:      "file",
1149                                 Mode:      0755 | fs.ModeSetuid,
1150                                 Uid:       -1,
1151                                 Gid:       -1,
1152                                 Data:      []byte("content\n"),
1153                                 OrigGroup: "group",
1154                         },
1155                         func() {
1156                                 createFile("file", "content\n", 0755)
1157                         },
1158                         true,
1159                         []File{
1160                                 root,
1161                                 {
1162                                         Path: "file",
1163                                         Mode: 0755 | fs.ModeSetuid,
1164                                         Data: []byte("content\n"),
1165                                 },
1166                         },
1167                         safcm.MsgSyncResp{
1168                                 FileChanges: []safcm.FileChange{
1169                                         {
1170                                                 Path: "file",
1171                                                 Old: safcm.FileChangeInfo{
1172                                                         Mode:  0755,
1173                                                         User:  user,
1174                                                         Uid:   uid,
1175                                                         Group: group,
1176                                                         Gid:   gid,
1177                                                 },
1178                                                 New: safcm.FileChangeInfo{
1179                                                         Mode:  0755 | fs.ModeSetuid,
1180                                                         User:  user,
1181                                                         Uid:   uid,
1182                                                         Group: group,
1183                                                         Gid:   gid,
1184                                                 },
1185                                         },
1186                                 },
1187                         },
1188                         []string{
1189                                 `4: sync remote: files: "file" (group): permission differs -rwxr-xr-x -> urwxr-xr-x`,
1190                                 `3: sync remote: files: "file" (group): updating`,
1191                                 `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
1192                                 `4: sync remote: files: "file" (group): renaming "./.fileRND"`,
1193                         },
1194                         nil,
1195                 },
1196
1197                 {
1198                         "file: content",
1199                         safcm.MsgSyncReq{},
1200                         &safcm.File{
1201                                 Path:      "file",
1202                                 Mode:      0644,
1203                                 Uid:       -1,
1204                                 Gid:       -1,
1205                                 Data:      []byte("content\n"),
1206                                 OrigGroup: "group",
1207                         },
1208                         func() {
1209                                 createFile("file", "old content\n", 0644)
1210                         },
1211                         true,
1212                         []File{
1213                                 root,
1214                                 {
1215                                         Path: "file",
1216                                         Mode: 0644,
1217                                         Data: []byte("content\n"),
1218                                 },
1219                         },
1220                         safcm.MsgSyncResp{
1221                                 FileChanges: []safcm.FileChange{
1222                                         {
1223                                                 Path: "file",
1224                                                 Old: safcm.FileChangeInfo{
1225                                                         Mode:  0644,
1226                                                         User:  user,
1227                                                         Uid:   uid,
1228                                                         Group: group,
1229                                                         Gid:   gid,
1230                                                 },
1231                                                 New: safcm.FileChangeInfo{
1232                                                         Mode:  0644,
1233                                                         User:  user,
1234                                                         Uid:   uid,
1235                                                         Group: group,
1236                                                         Gid:   gid,
1237                                                 },
1238                                                 DataDiff: `@@ -1,2 +1,2 @@
1239 -old content
1240 +content
1241  
1242 `,
1243                                         },
1244                                 },
1245                         },
1246                         []string{
1247                                 `4: sync remote: files: "file" (group): content differs`,
1248                                 `3: sync remote: files: "file" (group): updating`,
1249                                 `4: sync remote: files: "file" (group): creating temporary file ".file*"`,
1250                                 `4: sync remote: files: "file" (group): renaming "./.fileRND"`,
1251                         },
1252                         nil,
1253                 },
1254
1255                 // Symbolic link
1256
1257                 {
1258                         "symlink: create",
1259                         safcm.MsgSyncReq{},
1260                         &safcm.File{
1261                                 Path:      "link",
1262                                 Mode:      fs.ModeSymlink | 0777,
1263                                 Uid:       -1,
1264                                 Gid:       -1,
1265                                 Data:      []byte("target"),
1266                                 OrigGroup: "group",
1267                         },
1268                         nil,
1269                         true,
1270                         []File{
1271                                 root,
1272                                 {
1273                                         Path: "link",
1274                                         Mode: fs.ModeSymlink | 0777,
1275                                         Data: []byte("target"),
1276                                 },
1277                         },
1278                         safcm.MsgSyncResp{
1279                                 FileChanges: []safcm.FileChange{
1280                                         {
1281                                                 Path:    "link",
1282                                                 Created: true,
1283                                                 New: safcm.FileChangeInfo{
1284                                                         Mode:  fs.ModeSymlink | 0777,
1285                                                         User:  user,
1286                                                         Uid:   uid,
1287                                                         Group: group,
1288                                                         Gid:   gid,
1289                                                 },
1290                                         },
1291                                 },
1292                         },
1293                         []string{
1294                                 `4: sync remote: files: "link" (group): will create`,
1295                                 `3: sync remote: files: "link" (group): creating`,
1296                                 `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
1297                                 `4: sync remote: files: "link" (group): renaming ".linkRND"`,
1298                         },
1299                         nil,
1300                 },
1301                 {
1302                         "symlink: create (conflict)",
1303                         safcm.MsgSyncReq{},
1304                         &safcm.File{
1305                                 Path:      "link",
1306                                 Mode:      fs.ModeSymlink | 0777,
1307                                 Uid:       -1,
1308                                 Gid:       -1,
1309                                 Data:      []byte("target"),
1310                                 OrigGroup: "group",
1311                         },
1312                         func() {
1313                                 createFile(".link8717895732742165505", "", 0600)
1314                         },
1315                         true,
1316                         []File{
1317                                 root,
1318                                 {
1319                                         Path: ".link8717895732742165505",
1320                                         Mode: 0600,
1321                                         Data: []byte(""),
1322                                 },
1323                                 {
1324                                         Path: "link",
1325                                         Mode: fs.ModeSymlink | 0777,
1326                                         Data: []byte("target"),
1327                                 },
1328                         },
1329                         safcm.MsgSyncResp{
1330                                 FileChanges: []safcm.FileChange{
1331                                         {
1332                                                 Path:    "link",
1333                                                 Created: true,
1334                                                 New: safcm.FileChangeInfo{
1335                                                         Mode:  fs.ModeSymlink | 0777,
1336                                                         User:  user,
1337                                                         Uid:   uid,
1338                                                         Group: group,
1339                                                         Gid:   gid,
1340                                                 },
1341                                         },
1342                                 },
1343                         },
1344                         []string{
1345                                 `4: sync remote: files: "link" (group): will create`,
1346                                 `3: sync remote: files: "link" (group): creating`,
1347                                 `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
1348                                 `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
1349                                 `4: sync remote: files: "link" (group): renaming ".linkRND"`,
1350                         },
1351                         nil,
1352                 },
1353                 {
1354                         "symlink: create (dry-run)",
1355                         safcm.MsgSyncReq{
1356                                 DryRun: true,
1357                         },
1358                         &safcm.File{
1359                                 Path:      "link",
1360                                 Mode:      fs.ModeSymlink | 0777,
1361                                 Uid:       -1,
1362                                 Gid:       -1,
1363                                 Data:      []byte("target"),
1364                                 OrigGroup: "group",
1365                         },
1366                         nil,
1367                         true,
1368                         []File{root},
1369                         safcm.MsgSyncResp{
1370                                 FileChanges: []safcm.FileChange{
1371                                         {
1372                                                 Path:    "link",
1373                                                 Created: true,
1374                                                 New: safcm.FileChangeInfo{
1375                                                         Mode:  fs.ModeSymlink | 0777,
1376                                                         User:  user,
1377                                                         Uid:   uid,
1378                                                         Group: group,
1379                                                         Gid:   gid,
1380                                                 },
1381                                         },
1382                                 },
1383                         },
1384                         []string{
1385                                 `4: sync remote: files: "link" (group): will create`,
1386                                 `3: sync remote: files: "link" (group): creating`,
1387                                 `4: sync remote: files: "link" (group): dry-run, skipping changes`,
1388                         },
1389                         nil,
1390                 },
1391
1392                 {
1393                         "symlink: unchanged",
1394                         safcm.MsgSyncReq{},
1395                         &safcm.File{
1396                                 Path:      "link",
1397                                 Mode:      fs.ModeSymlink | 0777,
1398                                 Uid:       -1,
1399                                 Gid:       -1,
1400                                 Data:      []byte("target"),
1401                                 OrigGroup: "group",
1402                         },
1403                         func() {
1404                                 createSymlink("link", "target")
1405                         },
1406                         false,
1407                         []File{
1408                                 root,
1409                                 {
1410                                         Path: "link",
1411                                         Mode: fs.ModeSymlink | 0777,
1412                                         Data: []byte("target"),
1413                                 },
1414                         },
1415                         safcm.MsgSyncResp{},
1416                         []string{
1417                                 `4: sync remote: files: "link" (group): unchanged`,
1418                         },
1419                         nil,
1420                 },
1421
1422                 {
1423                         "symlink: content",
1424                         safcm.MsgSyncReq{},
1425                         &safcm.File{
1426                                 Path:      "link",
1427                                 Mode:      fs.ModeSymlink | 0777,
1428                                 Uid:       -1,
1429                                 Gid:       -1,
1430                                 Data:      []byte("target"),
1431                                 OrigGroup: "group",
1432                         },
1433                         func() {
1434                                 createSymlink("link", "old-target")
1435                         },
1436                         true,
1437                         []File{
1438                                 root,
1439                                 {
1440                                         Path: "link",
1441                                         Mode: fs.ModeSymlink | 0777,
1442                                         Data: []byte("target"),
1443                                 },
1444                         },
1445                         safcm.MsgSyncResp{
1446                                 FileChanges: []safcm.FileChange{
1447                                         {
1448                                                 Path: "link",
1449                                                 Old: safcm.FileChangeInfo{
1450                                                         Mode:  fs.ModeSymlink | 0777,
1451                                                         User:  user,
1452                                                         Uid:   uid,
1453                                                         Group: group,
1454                                                         Gid:   gid,
1455                                                 },
1456                                                 New: safcm.FileChangeInfo{
1457                                                         Mode:  fs.ModeSymlink | 0777,
1458                                                         User:  user,
1459                                                         Uid:   uid,
1460                                                         Group: group,
1461                                                         Gid:   gid,
1462                                                 },
1463                                                 DataDiff: `@@ -1 +1 @@
1464 -old-target
1465 +target
1466 `,
1467                                         },
1468                                 },
1469                         },
1470                         []string{
1471                                 `4: sync remote: files: "link" (group): content differs`,
1472                                 `3: sync remote: files: "link" (group): updating`,
1473                                 `4: sync remote: files: "link" (group): creating temporary symlink ".linkRND"`,
1474                                 `4: sync remote: files: "link" (group): renaming ".linkRND"`,
1475                         },
1476                         nil,
1477                 },
1478
1479                 // Directory
1480
1481                 {
1482                         "directory: create",
1483                         safcm.MsgSyncReq{},
1484                         &safcm.File{
1485                                 Path:      "dir",
1486                                 Mode:      fs.ModeDir | 0705,
1487                                 Uid:       -1,
1488                                 Gid:       -1,
1489                                 OrigGroup: "group",
1490                         },
1491                         nil,
1492                         true,
1493                         []File{
1494                                 root,
1495                                 {
1496                                         Path: "dir",
1497                                         Mode: fs.ModeDir | 0705,
1498                                 },
1499                         },
1500                         safcm.MsgSyncResp{
1501                                 FileChanges: []safcm.FileChange{
1502                                         {
1503                                                 Path:    "dir",
1504                                                 Created: true,
1505                                                 New: safcm.FileChangeInfo{
1506                                                         Mode:  fs.ModeDir | 0705,
1507                                                         User:  user,
1508                                                         Uid:   uid,
1509                                                         Group: group,
1510                                                         Gid:   gid,
1511                                                 },
1512                                         },
1513                                 },
1514                         },
1515                         []string{
1516                                 `4: sync remote: files: "dir" (group): will create`,
1517                                 `3: sync remote: files: "dir" (group): creating`,
1518                                 `4: sync remote: files: "dir" (group): creating directory`,
1519                                 `4: sync remote: files: "dir" (group): chmodding drwx---r-x`,
1520                                 fmt.Sprintf(`4: sync remote: files: "dir" (group): chowning %d/%d`, uid, gid),
1521                         },
1522                         nil,
1523                 },
1524                 {
1525                         "directory: create (dry-run)",
1526                         safcm.MsgSyncReq{
1527                                 DryRun: true,
1528                         },
1529                         &safcm.File{
1530                                 Path:      "dir",
1531                                 Mode:      fs.ModeDir | 0644,
1532                                 Uid:       -1,
1533                                 Gid:       -1,
1534                                 OrigGroup: "group",
1535                         },
1536                         nil,
1537                         true,
1538                         []File{root},
1539                         safcm.MsgSyncResp{
1540                                 FileChanges: []safcm.FileChange{
1541                                         {
1542                                                 Path:    "dir",
1543                                                 Created: true,
1544                                                 New: safcm.FileChangeInfo{
1545                                                         Mode:  fs.ModeDir | 0644,
1546                                                         User:  user,
1547                                                         Uid:   uid,
1548                                                         Group: group,
1549                                                         Gid:   gid,
1550                                                 },
1551                                         },
1552                                 },
1553                         },
1554                         []string{
1555                                 `4: sync remote: files: "dir" (group): will create`,
1556                                 `3: sync remote: files: "dir" (group): creating`,
1557                                 `4: sync remote: files: "dir" (group): dry-run, skipping changes`,
1558                         },
1559                         nil,
1560                 },
1561
1562                 {
1563                         "directory: unchanged",
1564                         safcm.MsgSyncReq{},
1565                         &safcm.File{
1566                                 Path:      "dir",
1567                                 Mode:      fs.ModeDir | 0755,
1568                                 Uid:       -1,
1569                                 Gid:       -1,
1570                                 OrigGroup: "group",
1571                         },
1572                         func() {
1573                                 createDirectory("dir", 0755)
1574                         },
1575                         false,
1576                         []File{
1577                                 root,
1578                                 {
1579                                         Path: "dir",
1580                                         Mode: fs.ModeDir | 0755,
1581                                 },
1582                         },
1583                         safcm.MsgSyncResp{},
1584                         []string{
1585                                 `4: sync remote: files: "dir" (group): unchanged`,
1586                         },
1587                         nil,
1588                 },
1589
1590                 {
1591                         "directory: permission",
1592                         safcm.MsgSyncReq{},
1593                         &safcm.File{
1594                                 Path:      "dir",
1595                                 Mode:      fs.ModeDir | 0755 | fs.ModeSetgid,
1596                                 Uid:       -1,
1597                                 Gid:       -1,
1598                                 OrigGroup: "group",
1599                         },
1600                         func() {
1601                                 createDirectory("dir", 0500|fs.ModeSticky)
1602                         },
1603                         true,
1604                         []File{
1605                                 root,
1606                                 {
1607                                         Path: "dir",
1608                                         Mode: fs.ModeDir | 0755 | fs.ModeSetgid,
1609                                 },
1610                         },
1611                         safcm.MsgSyncResp{
1612                                 FileChanges: []safcm.FileChange{
1613                                         {
1614                                                 Path: "dir",
1615                                                 Old: safcm.FileChangeInfo{
1616                                                         Mode:  fs.ModeDir | 0500 | fs.ModeSticky,
1617                                                         User:  user,
1618                                                         Uid:   uid,
1619                                                         Group: group,
1620                                                         Gid:   gid,
1621                                                 },
1622                                                 New: safcm.FileChangeInfo{
1623                                                         Mode:  fs.ModeDir | 0755 | fs.ModeSetgid,
1624                                                         User:  user,
1625                                                         Uid:   uid,
1626                                                         Group: group,
1627                                                         Gid:   gid,
1628                                                 },
1629                                         },
1630                                 },
1631                         },
1632                         []string{
1633                                 `4: sync remote: files: "dir" (group): permission differs dtr-x------ -> dgrwxr-xr-x`,
1634                                 `3: sync remote: files: "dir" (group): updating`,
1635                                 `4: sync remote: files: "dir" (group): chmodding dgrwxr-xr-x`,
1636                         },
1637                         nil,
1638                 },
1639
1640                 // Type changes
1641
1642                 {
1643                         "change: file to directory",
1644                         safcm.MsgSyncReq{},
1645                         &safcm.File{
1646                                 Path:      "path",
1647                                 Mode:      fs.ModeDir | 0751,
1648                                 Uid:       -1,
1649                                 Gid:       -1,
1650                                 OrigGroup: "group",
1651                         },
1652                         func() {
1653                                 createFile("path", "content\n", 0644)
1654                         },
1655                         true,
1656                         []File{
1657                                 root,
1658                                 {
1659                                         Path: "path",
1660                                         Mode: fs.ModeDir | 0751,
1661                                 },
1662                         },
1663                         safcm.MsgSyncResp{
1664                                 FileChanges: []safcm.FileChange{
1665                                         {
1666                                                 Path: "path",
1667                                                 Old: safcm.FileChangeInfo{
1668                                                         Mode:  0644,
1669                                                         User:  user,
1670                                                         Uid:   uid,
1671                                                         Group: group,
1672                                                         Gid:   gid,
1673                                                 },
1674                                                 New: safcm.FileChangeInfo{
1675                                                         Mode:  fs.ModeDir | 0751,
1676                                                         User:  user,
1677                                                         Uid:   uid,
1678                                                         Group: group,
1679                                                         Gid:   gid,
1680                                                 },
1681                                                 DataDiff: `@@ -1,2 +1 @@
1682 -content
1683  
1684 `,
1685                                         },
1686                                 },
1687                         },
1688                         []string{
1689                                 `4: sync remote: files: "path" (group): type differs ---------- -> d---------`,
1690                                 `3: sync remote: files: "path" (group): updating`,
1691                                 `4: sync remote: files: "path" (group): removing (due to type change)`,
1692                                 `4: sync remote: files: "path" (group): creating directory`,
1693                                 `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
1694                                 fmt.Sprintf(`4: sync remote: files: "path" (group): chowning %d/%d`, uid, gid),
1695                         },
1696                         nil,
1697                 },
1698
1699                 {
1700                         "change: file to symlink",
1701                         safcm.MsgSyncReq{},
1702                         &safcm.File{
1703                                 Path:      "path",
1704                                 Mode:      fs.ModeSymlink | 0777,
1705                                 Uid:       -1,
1706                                 Gid:       -1,
1707                                 OrigGroup: "group",
1708                                 Data:      []byte("target"),
1709                         },
1710                         func() {
1711                                 createFile("path", "content\n", 0644)
1712                         },
1713                         true,
1714                         []File{
1715                                 root,
1716                                 {
1717                                         Path: "path",
1718                                         Mode: fs.ModeSymlink | 0777,
1719                                         Data: []byte("target"),
1720                                 },
1721                         },
1722                         safcm.MsgSyncResp{
1723                                 FileChanges: []safcm.FileChange{
1724                                         {
1725                                                 Path: "path",
1726                                                 Old: safcm.FileChangeInfo{
1727                                                         Mode:  0644,
1728                                                         User:  user,
1729                                                         Uid:   uid,
1730                                                         Group: group,
1731                                                         Gid:   gid,
1732                                                 },
1733                                                 New: safcm.FileChangeInfo{
1734                                                         Mode:  fs.ModeSymlink | 0777,
1735                                                         User:  user,
1736                                                         Uid:   uid,
1737                                                         Group: group,
1738                                                         Gid:   gid,
1739                                                 },
1740                                                 DataDiff: `@@ -1,2 +1 @@
1741 -content
1742 -
1743 +target
1744 `,
1745                                         },
1746                                 },
1747                         },
1748                         []string{
1749                                 `4: sync remote: files: "path" (group): type differs ---------- -> L---------`,
1750                                 `3: sync remote: files: "path" (group): updating`,
1751                                 `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
1752                                 `4: sync remote: files: "path" (group): renaming ".pathRND"`,
1753                         },
1754                         nil,
1755                 },
1756
1757                 {
1758                         "change: symlink to file",
1759                         safcm.MsgSyncReq{},
1760                         &safcm.File{
1761                                 Path:      "path",
1762                                 Mode:      0640,
1763                                 Uid:       -1,
1764                                 Gid:       -1,
1765                                 OrigGroup: "group",
1766                                 Data:      []byte("content\n"),
1767                         },
1768                         func() {
1769                                 createSymlink("path", "target")
1770                         },
1771                         true,
1772                         []File{
1773                                 root,
1774                                 {
1775                                         Path: "path",
1776                                         Mode: 0640,
1777                                         Data: []byte("content\n"),
1778                                 },
1779                         },
1780                         safcm.MsgSyncResp{
1781                                 FileChanges: []safcm.FileChange{
1782                                         {
1783                                                 Path: "path",
1784                                                 Old: safcm.FileChangeInfo{
1785                                                         Mode:  fs.ModeSymlink | 0777,
1786                                                         User:  user,
1787                                                         Uid:   uid,
1788                                                         Group: group,
1789                                                         Gid:   gid,
1790                                                 },
1791                                                 New: safcm.FileChangeInfo{
1792                                                         Mode:  0640,
1793                                                         User:  user,
1794                                                         Uid:   uid,
1795                                                         Group: group,
1796                                                         Gid:   gid,
1797                                                 },
1798                                                 DataDiff: `@@ -1 +1,2 @@
1799 -target
1800 +content
1801 +
1802 `,
1803                                         },
1804                                 },
1805                         },
1806                         []string{
1807                                 `4: sync remote: files: "path" (group): type differs L--------- -> ----------`,
1808                                 `3: sync remote: files: "path" (group): updating`,
1809                                 `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
1810                                 `4: sync remote: files: "path" (group): renaming "./.pathRND"`,
1811                         },
1812                         nil,
1813                 },
1814
1815                 {
1816                         "change: symlink to directory",
1817                         safcm.MsgSyncReq{},
1818                         &safcm.File{
1819                                 Path:      "path",
1820                                 Mode:      fs.ModeDir | 0751,
1821                                 Uid:       -1,
1822                                 Gid:       -1,
1823                                 OrigGroup: "group",
1824                         },
1825                         func() {
1826                                 createSymlink("path", "target")
1827                         },
1828                         true,
1829                         []File{
1830                                 root,
1831                                 {
1832                                         Path: "path",
1833                                         Mode: fs.ModeDir | 0751,
1834                                 },
1835                         },
1836                         safcm.MsgSyncResp{
1837                                 FileChanges: []safcm.FileChange{
1838                                         {
1839                                                 Path: "path",
1840                                                 Old: safcm.FileChangeInfo{
1841                                                         Mode:  fs.ModeSymlink | 0777,
1842                                                         User:  user,
1843                                                         Uid:   uid,
1844                                                         Group: group,
1845                                                         Gid:   gid,
1846                                                 },
1847                                                 New: safcm.FileChangeInfo{
1848                                                         Mode:  fs.ModeDir | 0751,
1849                                                         User:  user,
1850                                                         Uid:   uid,
1851                                                         Group: group,
1852                                                         Gid:   gid,
1853                                                 },
1854                                                 DataDiff: `@@ -1 +1 @@
1855 -target
1856 +
1857 `,
1858                                         },
1859                                 },
1860                         },
1861                         []string{
1862                                 `4: sync remote: files: "path" (group): type differs L--------- -> d---------`,
1863                                 `3: sync remote: files: "path" (group): updating`,
1864                                 `4: sync remote: files: "path" (group): removing (due to type change)`,
1865                                 `4: sync remote: files: "path" (group): creating directory`,
1866                                 `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
1867                                 fmt.Sprintf(`4: sync remote: files: "path" (group): chowning %d/%d`, uid, gid),
1868                         },
1869                         nil,
1870                 },
1871
1872                 {
1873                         "change: directory to file",
1874                         safcm.MsgSyncReq{},
1875                         &safcm.File{
1876                                 Path:      "path",
1877                                 Mode:      0666,
1878                                 Uid:       -1,
1879                                 Gid:       -1,
1880                                 OrigGroup: "group",
1881                                 Data:      []byte("content\n"),
1882                         },
1883                         func() {
1884                                 createDirectory("path", 0777)
1885                         },
1886                         true,
1887                         []File{
1888                                 root,
1889                                 {
1890                                         Path: "path",
1891                                         Mode: 0666,
1892                                         Data: []byte("content\n"),
1893                                 },
1894                         },
1895                         safcm.MsgSyncResp{
1896                                 FileChanges: []safcm.FileChange{
1897                                         {
1898                                                 Path: "path",
1899                                                 Old: safcm.FileChangeInfo{
1900                                                         Mode:  fs.ModeDir | 0777,
1901                                                         User:  user,
1902                                                         Uid:   uid,
1903                                                         Group: group,
1904                                                         Gid:   gid,
1905                                                 },
1906                                                 New: safcm.FileChangeInfo{
1907                                                         Mode:  0666,
1908                                                         User:  user,
1909                                                         Uid:   uid,
1910                                                         Group: group,
1911                                                         Gid:   gid,
1912                                                 },
1913                                         },
1914                                 },
1915                         },
1916                         []string{
1917                                 `4: sync remote: files: "path" (group): type differs d--------- -> ----------`,
1918                                 `3: sync remote: files: "path" (group): updating`,
1919                                 `4: sync remote: files: "path" (group): removing (due to type change)`,
1920                                 `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
1921                                 `4: sync remote: files: "path" (group): renaming "./.pathRND"`,
1922                         },
1923                         nil,
1924                 },
1925
1926                 {
1927                         "change: directory to symlink",
1928                         safcm.MsgSyncReq{},
1929                         &safcm.File{
1930                                 Path:      "path",
1931                                 Mode:      fs.ModeSymlink | 0777,
1932                                 Uid:       -1,
1933                                 Gid:       -1,
1934                                 OrigGroup: "group",
1935                                 Data:      []byte("target"),
1936                         },
1937                         func() {
1938                                 createDirectory("path", 0777)
1939                         },
1940                         true,
1941                         []File{
1942                                 root,
1943                                 {
1944                                         Path: "path",
1945                                         Mode: fs.ModeSymlink | 0777,
1946                                         Data: []byte("target"),
1947                                 },
1948                         },
1949                         safcm.MsgSyncResp{
1950                                 FileChanges: []safcm.FileChange{
1951                                         {
1952                                                 Path: "path",
1953                                                 Old: safcm.FileChangeInfo{
1954                                                         Mode:  fs.ModeDir | 0777,
1955                                                         User:  user,
1956                                                         Uid:   uid,
1957                                                         Group: group,
1958                                                         Gid:   gid,
1959                                                 },
1960                                                 New: safcm.FileChangeInfo{
1961                                                         Mode:  fs.ModeSymlink | 0777,
1962                                                         User:  user,
1963                                                         Uid:   uid,
1964                                                         Group: group,
1965                                                         Gid:   gid,
1966                                                 },
1967                                         },
1968                                 },
1969                         },
1970                         []string{
1971                                 `4: sync remote: files: "path" (group): type differs d--------- -> L---------`,
1972                                 `3: sync remote: files: "path" (group): updating`,
1973                                 `4: sync remote: files: "path" (group): removing (due to type change)`,
1974                                 `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
1975                                 `4: sync remote: files: "path" (group): renaming ".pathRND"`,
1976                         },
1977                         nil,
1978                 },
1979
1980                 {
1981                         "change: other to file",
1982                         safcm.MsgSyncReq{},
1983                         &safcm.File{
1984                                 Path:      "path",
1985                                 Mode:      0640,
1986                                 Uid:       -1,
1987                                 Gid:       -1,
1988                                 OrigGroup: "group",
1989                                 Data:      []byte("content\n"),
1990                         },
1991                         func() {
1992                                 createFifo("path", 0666)
1993                         },
1994                         true,
1995                         []File{
1996                                 root,
1997                                 {
1998                                         Path: "path",
1999                                         Mode: 0640,
2000                                         Data: []byte("content\n"),
2001                                 },
2002                         },
2003                         safcm.MsgSyncResp{
2004                                 FileChanges: []safcm.FileChange{
2005                                         {
2006                                                 Path: "path",
2007                                                 Old: safcm.FileChangeInfo{
2008                                                         Mode:  fs.ModeNamedPipe | 0666,
2009                                                         User:  user,
2010                                                         Uid:   uid,
2011                                                         Group: group,
2012                                                         Gid:   gid,
2013                                                 },
2014                                                 New: safcm.FileChangeInfo{
2015                                                         Mode:  0640,
2016                                                         User:  user,
2017                                                         Uid:   uid,
2018                                                         Group: group,
2019                                                         Gid:   gid,
2020                                                 },
2021                                         },
2022                                 },
2023                         },
2024                         []string{
2025                                 `4: sync remote: files: "path" (group): type differs p--------- -> ----------`,
2026                                 `3: sync remote: files: "path" (group): updating`,
2027                                 `4: sync remote: files: "path" (group): creating temporary file ".path*"`,
2028                                 `4: sync remote: files: "path" (group): renaming "./.pathRND"`,
2029                         },
2030                         nil,
2031                 },
2032
2033                 {
2034                         "change: other to symlink",
2035                         safcm.MsgSyncReq{},
2036                         &safcm.File{
2037                                 Path:      "path",
2038                                 Mode:      fs.ModeSymlink | 0777,
2039                                 Uid:       -1,
2040                                 Gid:       -1,
2041                                 OrigGroup: "group",
2042                                 Data:      []byte("target"),
2043                         },
2044                         func() {
2045                                 createFifo("path", 0666)
2046                         },
2047                         true,
2048                         []File{
2049                                 root,
2050                                 {
2051                                         Path: "path",
2052                                         Mode: fs.ModeSymlink | 0777,
2053                                         Data: []byte("target"),
2054                                 },
2055                         },
2056                         safcm.MsgSyncResp{
2057                                 FileChanges: []safcm.FileChange{
2058                                         {
2059                                                 Path: "path",
2060                                                 Old: safcm.FileChangeInfo{
2061                                                         Mode:  fs.ModeNamedPipe | 0666,
2062                                                         User:  user,
2063                                                         Uid:   uid,
2064                                                         Group: group,
2065                                                         Gid:   gid,
2066                                                 },
2067                                                 New: safcm.FileChangeInfo{
2068                                                         Mode:  fs.ModeSymlink | 0777,
2069                                                         User:  user,
2070                                                         Uid:   uid,
2071                                                         Group: group,
2072                                                         Gid:   gid,
2073                                                 },
2074                                         },
2075                                 },
2076                         },
2077                         []string{
2078                                 `4: sync remote: files: "path" (group): type differs p--------- -> L---------`,
2079                                 `3: sync remote: files: "path" (group): updating`,
2080                                 `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
2081                                 `4: sync remote: files: "path" (group): renaming ".pathRND"`,
2082                         },
2083                         nil,
2084                 },
2085
2086                 {
2087                         "change: other to directory",
2088                         safcm.MsgSyncReq{},
2089                         &safcm.File{
2090                                 Path:      "path",
2091                                 Mode:      fs.ModeDir | 0751,
2092                                 Uid:       -1,
2093                                 Gid:       -1,
2094                                 OrigGroup: "group",
2095                         },
2096                         func() {
2097                                 createFifo("path", 0666)
2098                         },
2099                         true,
2100                         []File{
2101                                 root,
2102                                 {
2103                                         Path: "path",
2104                                         Mode: fs.ModeDir | 0751,
2105                                 },
2106                         },
2107                         safcm.MsgSyncResp{
2108                                 FileChanges: []safcm.FileChange{
2109                                         {
2110                                                 Path: "path",
2111                                                 Old: safcm.FileChangeInfo{
2112                                                         Mode:  fs.ModeNamedPipe | 0666,
2113                                                         User:  user,
2114                                                         Uid:   uid,
2115                                                         Group: group,
2116                                                         Gid:   gid,
2117                                                 },
2118                                                 New: safcm.FileChangeInfo{
2119                                                         Mode:  fs.ModeDir | 0751,
2120                                                         User:  user,
2121                                                         Uid:   uid,
2122                                                         Group: group,
2123                                                         Gid:   gid,
2124                                                 },
2125                                         },
2126                                 },
2127                         },
2128                         []string{
2129                                 `4: sync remote: files: "path" (group): type differs p--------- -> d---------`,
2130                                 `3: sync remote: files: "path" (group): updating`,
2131                                 `4: sync remote: files: "path" (group): removing (due to type change)`,
2132                                 `4: sync remote: files: "path" (group): creating directory`,
2133                                 `4: sync remote: files: "path" (group): chmodding drwxr-x--x`,
2134                                 fmt.Sprintf(`4: sync remote: files: "path" (group): chowning %d/%d`, uid, gid),
2135                         },
2136                         nil,
2137                 },
2138
2139                 {
2140                         "change: file to symlink (same content)",
2141                         safcm.MsgSyncReq{},
2142                         &safcm.File{
2143                                 Path:      "path",
2144                                 Mode:      fs.ModeSymlink | 0777,
2145                                 Uid:       -1,
2146                                 Gid:       -1,
2147                                 OrigGroup: "group",
2148                                 Data:      []byte("target"),
2149                         },
2150                         func() {
2151                                 createFile("path", "target", 0644)
2152                         },
2153                         true,
2154                         []File{
2155                                 root,
2156                                 {
2157                                         Path: "path",
2158                                         Mode: fs.ModeSymlink | 0777,
2159                                         Data: []byte("target"),
2160                                 },
2161                         },
2162                         safcm.MsgSyncResp{
2163                                 FileChanges: []safcm.FileChange{
2164                                         {
2165                                                 Path: "path",
2166                                                 Old: safcm.FileChangeInfo{
2167                                                         Mode:  0644,
2168                                                         User:  user,
2169                                                         Uid:   uid,
2170                                                         Group: group,
2171                                                         Gid:   gid,
2172                                                 },
2173                                                 New: safcm.FileChangeInfo{
2174                                                         Mode:  fs.ModeSymlink | 0777,
2175                                                         User:  user,
2176                                                         Uid:   uid,
2177                                                         Group: group,
2178                                                         Gid:   gid,
2179                                                 },
2180                                         },
2181                                 },
2182                         },
2183                         []string{
2184                                 `4: sync remote: files: "path" (group): type differs ---------- -> L---------`,
2185                                 `3: sync remote: files: "path" (group): updating`,
2186                                 `4: sync remote: files: "path" (group): creating temporary symlink ".pathRND"`,
2187                                 `4: sync remote: files: "path" (group): renaming ".pathRND"`,
2188                         },
2189                         nil,
2190                 },
2191
2192                 // Diffs
2193
2194                 {
2195                         "diff: textual",
2196                         safcm.MsgSyncReq{
2197                                 DryRun: true,
2198                         },
2199                         &safcm.File{
2200                                 Path: "file",
2201                                 Mode: 0644,
2202                                 Uid:  -1,
2203                                 Gid:  -1,
2204                                 Data: []byte(`
2205 this
2206 is
2207 a
2208 simple
2209 file
2210 `),
2211                                 OrigGroup: "group",
2212                         },
2213                         func() {
2214                                 createFile("file", `this
2215 is
2216 file
2217 !
2218 `, 0644)
2219                         },
2220                         true,
2221                         []File{
2222                                 root,
2223                                 {
2224                                         Path: "file",
2225                                         Mode: 0644,
2226                                         Data: []byte(`this
2227 is
2228 file
2229 !
2230 `),
2231                                 },
2232                         },
2233                         safcm.MsgSyncResp{
2234                                 FileChanges: []safcm.FileChange{
2235                                         {
2236                                                 Path: "file",
2237                                                 Old: safcm.FileChangeInfo{
2238                                                         Mode:  0644,
2239                                                         User:  user,
2240                                                         Uid:   uid,
2241                                                         Group: group,
2242                                                         Gid:   gid,
2243                                                 },
2244                                                 New: safcm.FileChangeInfo{
2245                                                         Mode:  0644,
2246                                                         User:  user,
2247                                                         Uid:   uid,
2248                                                         Group: group,
2249                                                         Gid:   gid,
2250                                                 },
2251                                                 DataDiff: `@@ -1,5 +1,7 @@
2252 +
2253  this
2254  is
2255 +a
2256 +simple
2257  file
2258 -!
2259  
2260 `,
2261                                         },
2262                                 },
2263                         },
2264                         []string{
2265                                 `4: sync remote: files: "file" (group): content differs`,
2266                                 `3: sync remote: files: "file" (group): updating`,
2267                                 `4: sync remote: files: "file" (group): dry-run, skipping changes`,
2268                         },
2269                         nil,
2270                 },
2271
2272                 {
2273                         "diff: binary both",
2274                         safcm.MsgSyncReq{
2275                                 DryRun: true,
2276                         },
2277                         &safcm.File{
2278                                 Path:      "file",
2279                                 Mode:      0644,
2280                                 Uid:       -1,
2281                                 Gid:       -1,
2282                                 Data:      []byte("\x00\x01\x02\x03"),
2283                                 OrigGroup: "group",
2284                         },
2285                         func() {
2286                                 createFile("file", "\x00\x01\x02", 0644)
2287                         },
2288                         true,
2289                         []File{
2290                                 root,
2291                                 {
2292                                         Path: "file",
2293                                         Mode: 0644,
2294                                         Data: []byte("\x00\x01\x02"),
2295                                 },
2296                         },
2297                         safcm.MsgSyncResp{
2298                                 FileChanges: []safcm.FileChange{
2299                                         {
2300                                                 Path: "file",
2301                                                 Old: safcm.FileChangeInfo{
2302                                                         Mode:  0644,
2303                                                         User:  user,
2304                                                         Uid:   uid,
2305                                                         Group: group,
2306                                                         Gid:   gid,
2307                                                 },
2308                                                 New: safcm.FileChangeInfo{
2309                                                         Mode:  0644,
2310                                                         User:  user,
2311                                                         Uid:   uid,
2312                                                         Group: group,
2313                                                         Gid:   gid,
2314                                                 },
2315                                                 DataDiff: "Binary files differ, cannot show diff",
2316                                         },
2317                                 },
2318                         },
2319                         []string{
2320                                 `4: sync remote: files: "file" (group): content differs`,
2321                                 `3: sync remote: files: "file" (group): updating`,
2322                                 `4: sync remote: files: "file" (group): dry-run, skipping changes`,
2323                         },
2324                         nil,
2325                 },
2326
2327                 {
2328                         "diff: binary old",
2329                         safcm.MsgSyncReq{
2330                                 DryRun: true,
2331                         },
2332                         &safcm.File{
2333                                 Path:      "file",
2334                                 Mode:      0644,
2335                                 Uid:       -1,
2336                                 Gid:       -1,
2337                                 Data:      []byte("content\n"),
2338                                 OrigGroup: "group",
2339                         },
2340                         func() {
2341                                 createFile("file", "\x00\x01\x02", 0644)
2342                         },
2343                         true,
2344                         []File{
2345                                 root,
2346                                 {
2347                                         Path: "file",
2348                                         Mode: 0644,
2349                                         Data: []byte("\x00\x01\x02"),
2350                                 },
2351                         },
2352                         safcm.MsgSyncResp{
2353                                 FileChanges: []safcm.FileChange{
2354                                         {
2355                                                 Path: "file",
2356                                                 Old: safcm.FileChangeInfo{
2357                                                         Mode:  0644,
2358                                                         User:  user,
2359                                                         Uid:   uid,
2360                                                         Group: group,
2361                                                         Gid:   gid,
2362                                                 },
2363                                                 New: safcm.FileChangeInfo{
2364                                                         Mode:  0644,
2365                                                         User:  user,
2366                                                         Uid:   uid,
2367                                                         Group: group,
2368                                                         Gid:   gid,
2369                                                 },
2370                                                 DataDiff: `@@ -1,2 +1,2 @@
2371 -<binary content>
2372 +content
2373  
2374 `,
2375                                         },
2376                                 },
2377                         },
2378                         []string{
2379                                 `4: sync remote: files: "file" (group): content differs`,
2380                                 `3: sync remote: files: "file" (group): updating`,
2381                                 `4: sync remote: files: "file" (group): dry-run, skipping changes`,
2382                         },
2383                         nil,
2384                 },
2385
2386                 {
2387                         "diff: binary new",
2388                         safcm.MsgSyncReq{
2389                                 DryRun: true,
2390                         },
2391                         &safcm.File{
2392                                 Path:      "file",
2393                                 Mode:      0644,
2394                                 Uid:       -1,
2395                                 Gid:       -1,
2396                                 Data:      []byte("\x00\x01\x02\x03"),
2397                                 OrigGroup: "group",
2398                         },
2399                         func() {
2400                                 createFile("file", "content\n", 0644)
2401                         },
2402                         true,
2403                         []File{
2404                                 root,
2405                                 {
2406                                         Path: "file",
2407                                         Mode: 0644,
2408                                         Data: []byte("content\n"),
2409                                 },
2410                         },
2411                         safcm.MsgSyncResp{
2412                                 FileChanges: []safcm.FileChange{
2413                                         {
2414                                                 Path: "file",
2415                                                 Old: safcm.FileChangeInfo{
2416                                                         Mode:  0644,
2417                                                         User:  user,
2418                                                         Uid:   uid,
2419                                                         Group: group,
2420                                                         Gid:   gid,
2421                                                 },
2422                                                 New: safcm.FileChangeInfo{
2423                                                         Mode:  0644,
2424                                                         User:  user,
2425                                                         Uid:   uid,
2426                                                         Group: group,
2427                                                         Gid:   gid,
2428                                                 },
2429                                                 DataDiff: `@@ -1,2 +1,2 @@
2430 -content
2431 +<binary content>
2432  
2433 `,
2434                                         },
2435                                 },
2436                         },
2437                         []string{
2438                                 `4: sync remote: files: "file" (group): content differs`,
2439                                 `3: sync remote: files: "file" (group): updating`,
2440                                 `4: sync remote: files: "file" (group): dry-run, skipping changes`,
2441                         },
2442                         nil,
2443                 },
2444         }
2445
2446         for _, tc := range tests {
2447                 // Create separate test directory for each test case
2448                 path := filepath.Join(cwd, "testdata", "file-"+tc.name)
2449                 err = os.Mkdir(path, 0700)
2450                 if err != nil {
2451                         t.Fatal(err)
2452                 }
2453                 err = os.Chdir(path)
2454                 if err != nil {
2455                         t.Fatal(err)
2456                 }
2457
2458                 if tc.prepare != nil {
2459                         tc.prepare()
2460                 }
2461
2462                 s, res := prepareSync(tc.req, &testRunner{
2463                         t:    t,
2464                         name: tc.name,
2465                 })
2466                 s.setDefaults()
2467
2468                 // Deterministic temporary symlink names
2469                 rand.Seed(0)
2470
2471                 var changed bool
2472                 err := s.syncFile(tc.file, &changed)
2473                 // Ugly but the simplest way to compare errors (including nil)
2474                 if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", tc.expErr) {
2475                         t.Errorf("%s: err = %#v, want %#v",
2476                                 tc.name, err, tc.expErr)
2477                 }
2478                 dbg := res.Wait()
2479                 // Remove random file names from result
2480                 for i, x := range dbg {
2481                         dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
2482                 }
2483                 if !reflect.DeepEqual(tc.expDbg, dbg) {
2484                         t.Errorf("%s: dbg: %s", tc.name,
2485                                 cmp.Diff(tc.expDbg, dbg))
2486                 }
2487
2488                 files, err := walkDir(path)
2489                 if err != nil {
2490                         t.Fatal(err)
2491                 }
2492                 if !reflect.DeepEqual(tc.expFiles, files) {
2493                         t.Errorf("%s: files: %s", tc.name,
2494                                 cmp.Diff(tc.expFiles, files))
2495                 }
2496
2497                 if tc.expChanged != changed {
2498                         t.Errorf("%s: changed = %#v, want %#v",
2499                                 tc.name, changed, tc.expChanged)
2500                 }
2501                 if !reflect.DeepEqual(tc.expResp, s.resp) {
2502                         t.Errorf("%s: resp: %s", tc.name,
2503                                 cmp.Diff(tc.expResp, s.resp))
2504                 }
2505         }
2506
2507         if !t.Failed() {
2508                 err = os.RemoveAll(filepath.Join(cwd, "testdata"))
2509                 if err != nil {
2510                         t.Fatal(err)
2511                 }
2512         }
2513 }
2514
2515 // Helper functions
2516
2517 func createFile(path string, data string, mode fs.FileMode) {
2518         err := os.WriteFile(path, []byte(data), 0644)
2519         if err != nil {
2520                 panic(err)
2521         }
2522         err = os.Chmod(path, mode)
2523         if err != nil {
2524                 panic(err)
2525         }
2526 }
2527 func createSymlink(path string, data string) {
2528         err := os.Symlink(data, path)
2529         if err != nil {
2530                 panic(err)
2531         }
2532 }
2533 func createDirectory(path string, mode fs.FileMode) {
2534         err := os.Mkdir(path, 0700)
2535         if err != nil {
2536                 panic(err)
2537         }
2538         err = os.Chmod(path, mode)
2539         if err != nil {
2540                 panic(err)
2541         }
2542 }
2543 func createFifo(path string, mode fs.FileMode) {
2544         err := syscall.Mkfifo(path, 0600)
2545         if err != nil {
2546                 panic(err)
2547         }
2548         err = os.Chmod(path, mode)
2549         if err != nil {
2550                 panic(err)
2551         }
2552 }
2553
2554 func currentUserAndGroup() (string, int, string, int) {
2555         u, err := user.Current()
2556         if err != nil {
2557                 panic(err)
2558         }
2559         g, err := user.LookupGroupId(u.Gid)
2560         if err != nil {
2561                 panic(err)
2562         }
2563         uid, err := strconv.Atoi(u.Uid)
2564         if err != nil {
2565                 panic(err)
2566         }
2567         gid, err := strconv.Atoi(g.Gid)
2568         if err != nil {
2569                 panic(err)
2570         }
2571         return u.Username, uid, g.Name, gid
2572 }