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