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