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