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