]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - remote/sync/files_test.go
dd32804bcd696c3d88d94ab24b60da30610bd982
[safcm/safcm.git] / remote / sync / files_test.go
1 // Copyright (C) 2021-2024  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) //nolint:errcheck
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                                                 OrigGroup: "group",
85                                                 Path:      ".",
86                                                 Mode:      fs.ModeDir | 0700,
87                                                 Uid:       -1,
88                                                 Gid:       -1,
89                                         },
90                                         "dir": {
91                                                 OrigGroup: "group",
92                                                 Path:      "dir",
93                                                 Mode:      fs.ModeDir | 0755,
94                                                 Uid:       -1,
95                                                 Gid:       -1,
96                                         },
97                                         "dir/file": {
98                                                 OrigGroup: "group",
99                                                 Path:      "dir/file",
100                                                 Mode:      0644,
101                                                 Uid:       -1,
102                                                 Gid:       -1,
103                                                 Data:      []byte("content\n"),
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                                                 OrigGroup: "group",
169                                                 Path:      ".",
170                                                 Mode:      fs.ModeDir | 0700,
171                                                 Uid:       -1,
172                                                 Gid:       -1,
173                                         },
174                                         "dir": {
175                                                 OrigGroup: "group",
176                                                 Path:      "dir",
177                                                 Mode:      fs.ModeDir | 0755,
178                                                 Uid:       -1,
179                                                 Gid:       -1,
180                                         },
181                                         "dir/file": {
182                                                 OrigGroup: "group",
183                                                 Path:      "dir/file",
184                                                 Mode:      0644,
185                                                 Uid:       -1,
186                                                 Gid:       -1,
187                                                 Data:      []byte("content\n"),
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                                                 OrigGroup: "group",
224                                                 Path:      ".",
225                                                 Mode:      fs.ModeDir | 0700,
226                                                 User:      "user",
227                                                 Uid:       1,
228                                                 Gid:       -1,
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                                                 OrigGroup: "group",
248                                                 Path:      ".",
249                                                 Mode:      fs.ModeDir | 0700,
250                                                 Uid:       -1,
251                                                 Group:     "group",
252                                                 Gid:       1,
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                                                 OrigGroup: "group",
280                                                 Path:      "/",
281                                                 Mode:      fs.ModeDir | 0755,
282                                                 Uid:       0,
283                                                 Gid:       0,
284                                         },
285                                         "/etc": {
286                                                 OrigGroup: "group",
287                                                 Path:      "/etc",
288                                                 Mode:      fs.ModeDir | 0755,
289                                                 Uid:       0,
290                                                 Gid:       0,
291                                         },
292                                         "/tmp": {
293                                                 OrigGroup: "group",
294                                                 Path:      "/tmp",
295                                                 Mode:      fs.ModeDir | 0777 | fs.ModeSticky,
296                                                 Uid:       0,
297                                                 Gid:       0,
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                                                 OrigGroup: "group",
322                                                 Path:      ".",
323                                                 Mode:      fs.ModeDir | 0700,
324                                                 Uid:       -1,
325                                                 Gid:       -1,
326                                                 TriggerCommands: []string{
327                                                         "echo trigger .",
328                                                 },
329                                         },
330                                         "dir": {
331                                                 OrigGroup: "group",
332                                                 Path:      "dir",
333                                                 Mode:      fs.ModeDir | 0755,
334                                                 Uid:       -1,
335                                                 Gid:       -1,
336                                                 TriggerCommands: []string{
337                                                         "echo trigger dir",
338                                                 },
339                                         },
340                                         "dir/file": {
341                                                 OrigGroup: "group",
342                                                 Path:      "dir/file",
343                                                 Mode:      0644,
344                                                 Uid:       -1,
345                                                 Gid:       -1,
346                                                 Data:      []byte("content\n"),
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                                                 OrigGroup: "group",
386                                                 Path:      ".",
387                                                 Mode:      fs.ModeDir | 0700,
388                                                 Uid:       -1,
389                                                 Gid:       -1,
390                                                 TriggerCommands: []string{
391                                                         "echo trigger .",
392                                                 },
393                                         },
394                                         "dir": {
395                                                 OrigGroup: "group",
396                                                 Path:      "dir",
397                                                 Mode:      fs.ModeDir | 0755,
398                                                 Uid:       -1,
399                                                 Gid:       -1,
400                                                 TriggerCommands: []string{
401                                                         "echo trigger dir",
402                                                 },
403                                         },
404                                         "dir/file": {
405                                                 OrigGroup: "group",
406                                                 Path:      "dir/file",
407                                                 Mode:      0644,
408                                                 Uid:       -1,
409                                                 Gid:       -1,
410                                                 Data:      []byte("content\n"),
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                                                 OrigGroup: "group",
476                                                 Path:      ".",
477                                                 Mode:      fs.ModeDir | 0700,
478                                                 Uid:       -1,
479                                                 Gid:       -1,
480                                                 TriggerCommands: []string{
481                                                         "echo trigger .",
482                                                 },
483                                         },
484                                         "dir": {
485                                                 OrigGroup: "group",
486                                                 Path:      "dir",
487                                                 Mode:      fs.ModeDir | 0755,
488                                                 Uid:       -1,
489                                                 Gid:       -1,
490                                                 TriggerCommands: []string{
491                                                         "echo trigger dir",
492                                                 },
493                                         },
494                                         "dir/file": {
495                                                 OrigGroup: "group",
496                                                 Path:      "dir/file",
497                                                 Mode:      0644,
498                                                 Uid:       -1,
499                                                 Gid:       -1,
500                                                 Data:      []byte("content\n"),
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                                                 OrigGroup: "group",
567                                                 Path:      ".",
568                                                 Mode:      fs.ModeDir | 0700,
569                                                 Uid:       -1,
570                                                 Gid:       -1,
571                                                 TriggerCommands: []string{
572                                                         "echo trigger .",
573                                                 },
574                                         },
575                                         "dir": {
576                                                 OrigGroup: "group",
577                                                 Path:      "dir",
578                                                 Mode:      fs.ModeDir | 0755,
579                                                 Uid:       -1,
580                                                 Gid:       -1,
581                                                 TriggerCommands: []string{
582                                                         "echo trigger dir",
583                                                 },
584                                         },
585                                         "dir/file": {
586                                                 OrigGroup: "group",
587                                                 Path:      "dir/file",
588                                                 Mode:      0644,
589                                                 Uid:       -1,
590                                                 Gid:       -1,
591                                                 Data:      []byte("content\n"),
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                                                 OrigGroup: "group",
654                                                 Path:      ".",
655                                                 Mode:      fs.ModeDir | 0700,
656                                                 Uid:       -1,
657                                                 Gid:       -1,
658                                                 TriggerCommands: []string{
659                                                         "echo trigger .",
660                                                 },
661                                         },
662                                         "dir": {
663                                                 OrigGroup: "group",
664                                                 Path:      "dir",
665                                                 Mode:      fs.ModeDir | 0755,
666                                                 Uid:       -1,
667                                                 Gid:       -1,
668                                                 TriggerCommands: []string{
669                                                         "echo trigger dir",
670                                                 },
671                                         },
672                                         "dir/file": {
673                                                 OrigGroup: "group",
674                                                 Path:      "dir/file",
675                                                 Mode:      0644,
676                                                 Uid:       -1,
677                                                 Gid:       -1,
678                                                 Data:      []byte("content\n"),
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                                                 OrigGroup: "group",
756                                                 Path:      "/",
757                                                 Mode:      fs.ModeDir | 0755,
758                                                 Uid:       0,
759                                                 Gid:       0,
760                                                 TriggerCommands: []string{
761                                                         "echo trigger /",
762                                                 },
763                                         },
764                                         "/tmp": {
765                                                 OrigGroup: "group",
766                                                 Path:      "/tmp",
767                                                 Mode:      fs.ModeDir | 0777 | fs.ModeSticky,
768                                                 Uid:       0,
769                                                 Gid:       0,
770                                                 TriggerCommands: []string{
771                                                         "echo trigger /tmp",
772                                                 },
773                                         },
774                                         tmpTestFilePath: {
775                                                 OrigGroup: "group",
776                                                 Path:      tmpTestFilePath,
777                                                 Mode:      0600,
778                                                 Uid:       -1,
779                                                 Gid:       -1,
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) //nolint:errcheck
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: create, missing parent (dry-run)",
1011                         safcm.MsgSyncReq{
1012                                 DryRun: true,
1013                         },
1014                         &safcm.File{
1015                                 Path:      "does-not-exist/file",
1016                                 Mode:      0644,
1017                                 Uid:       -1,
1018                                 Gid:       -1,
1019                                 Data:      []byte("content\n"),
1020                                 OrigGroup: "group",
1021                         },
1022                         nil,
1023                         true,
1024                         []ft.File{root},
1025                         safcm.MsgSyncResp{
1026                                 FileChanges: []safcm.FileChange{
1027                                         {
1028                                                 Path:    "does-not-exist/file",
1029                                                 Created: true,
1030                                                 New: safcm.FileChangeInfo{
1031                                                         Mode:  0644,
1032                                                         User:  user,
1033                                                         Uid:   uid,
1034                                                         Group: group,
1035                                                         Gid:   gid,
1036                                                 },
1037                                         },
1038                                 },
1039                         },
1040                         []string{
1041                                 `4: files: "does-not-exist/file" (group): will create (parent missing)`,
1042                                 `4: files: "does-not-exist/file" (group): dry-run, skipping changes`,
1043                         },
1044                         nil,
1045                 },
1046
1047                 {
1048                         "file: unchanged",
1049                         safcm.MsgSyncReq{},
1050                         &safcm.File{
1051                                 Path:      "file",
1052                                 Mode:      0644,
1053                                 Uid:       -1,
1054                                 Gid:       -1,
1055                                 Data:      []byte("content\n"),
1056                                 OrigGroup: "group",
1057                         },
1058                         func() {
1059                                 ft.CreateFile("file", "content\n", 0644)
1060                         },
1061                         false,
1062                         []ft.File{
1063                                 root,
1064                                 {
1065                                         Path: "file",
1066                                         Mode: 0644,
1067                                         Data: []byte("content\n"),
1068                                 },
1069                         },
1070                         safcm.MsgSyncResp{},
1071                         []string{
1072                                 `4: files: "file" (group): unchanged`,
1073                         },
1074                         nil,
1075                 },
1076
1077                 {
1078                         "file: unchanged (non-default user-group)",
1079                         safcm.MsgSyncReq{},
1080                         &safcm.File{
1081                                 Path:      "file",
1082                                 Mode:      0644,
1083                                 User:      user,
1084                                 Uid:       -1,
1085                                 Group:     group,
1086                                 Gid:       -1,
1087                                 Data:      []byte("content\n"),
1088                                 OrigGroup: "group",
1089                         },
1090                         func() {
1091                                 ft.CreateFile("file", "content\n", 0644)
1092                         },
1093                         false,
1094                         []ft.File{
1095                                 root,
1096                                 {
1097                                         Path: "file",
1098                                         Mode: 0644,
1099                                         Data: []byte("content\n"),
1100                                 },
1101                         },
1102                         safcm.MsgSyncResp{},
1103                         []string{
1104                                 `4: files: "file" (group): unchanged`,
1105                         },
1106                         nil,
1107                 },
1108
1109                 {
1110                         "file: permission",
1111                         safcm.MsgSyncReq{},
1112                         &safcm.File{
1113                                 Path:      "file",
1114                                 Mode:      0755 | fs.ModeSetuid,
1115                                 Uid:       -1,
1116                                 Gid:       -1,
1117                                 Data:      []byte("content\n"),
1118                                 OrigGroup: "group",
1119                         },
1120                         func() {
1121                                 ft.CreateFile("file", "content\n", 0755)
1122                         },
1123                         true,
1124                         []ft.File{
1125                                 root,
1126                                 {
1127                                         Path: "file",
1128                                         Mode: 0755 | fs.ModeSetuid,
1129                                         Data: []byte("content\n"),
1130                                 },
1131                         },
1132                         safcm.MsgSyncResp{
1133                                 FileChanges: []safcm.FileChange{
1134                                         {
1135                                                 Path: "file",
1136                                                 Old: safcm.FileChangeInfo{
1137                                                         Mode:  0755,
1138                                                         User:  user,
1139                                                         Uid:   uid,
1140                                                         Group: group,
1141                                                         Gid:   gid,
1142                                                 },
1143                                                 New: safcm.FileChangeInfo{
1144                                                         Mode:  0755 | fs.ModeSetuid,
1145                                                         User:  user,
1146                                                         Uid:   uid,
1147                                                         Group: group,
1148                                                         Gid:   gid,
1149                                                 },
1150                                         },
1151                                 },
1152                         },
1153                         []string{
1154                                 `4: files: "file" (group): permission differs -rwxr-xr-x -> urwxr-xr-x`,
1155                                 `3: files: "file" (group): updating`,
1156                                 `4: files: "file" (group): creating temporary file ".file*"`,
1157                                 `4: files: "file" (group): renaming ".fileRND"`,
1158                         },
1159                         nil,
1160                 },
1161
1162                 {
1163                         "file: content",
1164                         safcm.MsgSyncReq{},
1165                         &safcm.File{
1166                                 Path:      "file",
1167                                 Mode:      0644,
1168                                 Uid:       -1,
1169                                 Gid:       -1,
1170                                 Data:      []byte("content\n"),
1171                                 OrigGroup: "group",
1172                         },
1173                         func() {
1174                                 ft.CreateFile("file", "old content\n", 0644)
1175                         },
1176                         true,
1177                         []ft.File{
1178                                 root,
1179                                 {
1180                                         Path: "file",
1181                                         Mode: 0644,
1182                                         Data: []byte("content\n"),
1183                                 },
1184                         },
1185                         safcm.MsgSyncResp{
1186                                 FileChanges: []safcm.FileChange{
1187                                         {
1188                                                 Path: "file",
1189                                                 Old: safcm.FileChangeInfo{
1190                                                         Mode:  0644,
1191                                                         User:  user,
1192                                                         Uid:   uid,
1193                                                         Group: group,
1194                                                         Gid:   gid,
1195                                                 },
1196                                                 New: safcm.FileChangeInfo{
1197                                                         Mode:  0644,
1198                                                         User:  user,
1199                                                         Uid:   uid,
1200                                                         Group: group,
1201                                                         Gid:   gid,
1202                                                 },
1203                                                 DataDiff: `@@ -1,2 +1,2 @@
1204 -old content
1205 +content
1206  
1207 `,
1208                                         },
1209                                 },
1210                         },
1211                         []string{
1212                                 `4: files: "file" (group): content differs`,
1213                                 `3: files: "file" (group): updating`,
1214                                 `4: files: "file" (group): creating temporary file ".file*"`,
1215                                 `4: files: "file" (group): renaming ".fileRND"`,
1216                         },
1217                         nil,
1218                 },
1219
1220                 // Symbolic link
1221
1222                 {
1223                         "symlink: create",
1224                         safcm.MsgSyncReq{},
1225                         &safcm.File{
1226                                 Path:      "link",
1227                                 Mode:      fs.ModeSymlink | 0777,
1228                                 Uid:       -1,
1229                                 Gid:       -1,
1230                                 Data:      []byte("target"),
1231                                 OrigGroup: "group",
1232                         },
1233                         nil,
1234                         true,
1235                         []ft.File{
1236                                 root,
1237                                 {
1238                                         Path: "link",
1239                                         Mode: fs.ModeSymlink | 0777,
1240                                         Data: []byte("target"),
1241                                 },
1242                         },
1243                         safcm.MsgSyncResp{
1244                                 FileChanges: []safcm.FileChange{
1245                                         {
1246                                                 Path:    "link",
1247                                                 Created: true,
1248                                                 New: safcm.FileChangeInfo{
1249                                                         Mode:  fs.ModeSymlink | 0777,
1250                                                         User:  user,
1251                                                         Uid:   uid,
1252                                                         Group: group,
1253                                                         Gid:   gid,
1254                                                 },
1255                                         },
1256                                 },
1257                         },
1258                         []string{
1259                                 `4: files: "link" (group): will create`,
1260                                 `3: files: "link" (group): creating`,
1261                                 `4: files: "link" (group): creating temporary symlink ".linkRND"`,
1262                                 `4: files: "link" (group): renaming ".linkRND"`,
1263                         },
1264                         nil,
1265                 },
1266                 {
1267                         "symlink: create (conflict)",
1268                         safcm.MsgSyncReq{},
1269                         &safcm.File{
1270                                 Path:      "link",
1271                                 Mode:      fs.ModeSymlink | 0777,
1272                                 Uid:       -1,
1273                                 Gid:       -1,
1274                                 Data:      []byte("target"),
1275                                 OrigGroup: "group",
1276                         },
1277                         func() {
1278                                 ft.CreateFile(".link8717895732742165505", "", 0600)
1279                         },
1280                         true,
1281                         []ft.File{
1282                                 root,
1283                                 {
1284                                         Path: ".link8717895732742165505",
1285                                         Mode: 0600,
1286                                         Data: []byte(""),
1287                                 },
1288                                 {
1289                                         Path: "link",
1290                                         Mode: fs.ModeSymlink | 0777,
1291                                         Data: []byte("target"),
1292                                 },
1293                         },
1294                         safcm.MsgSyncResp{
1295                                 FileChanges: []safcm.FileChange{
1296                                         {
1297                                                 Path:    "link",
1298                                                 Created: true,
1299                                                 New: safcm.FileChangeInfo{
1300                                                         Mode:  fs.ModeSymlink | 0777,
1301                                                         User:  user,
1302                                                         Uid:   uid,
1303                                                         Group: group,
1304                                                         Gid:   gid,
1305                                                 },
1306                                         },
1307                                 },
1308                         },
1309                         []string{
1310                                 `4: files: "link" (group): will create`,
1311                                 `3: files: "link" (group): creating`,
1312                                 `4: files: "link" (group): creating temporary symlink ".linkRND"`,
1313                                 `4: files: "link" (group): creating temporary symlink ".linkRND"`,
1314                                 `4: files: "link" (group): renaming ".linkRND"`,
1315                         },
1316                         nil,
1317                 },
1318                 {
1319                         "symlink: create (dry-run)",
1320                         safcm.MsgSyncReq{
1321                                 DryRun: true,
1322                         },
1323                         &safcm.File{
1324                                 Path:      "link",
1325                                 Mode:      fs.ModeSymlink | 0777,
1326                                 Uid:       -1,
1327                                 Gid:       -1,
1328                                 Data:      []byte("target"),
1329                                 OrigGroup: "group",
1330                         },
1331                         nil,
1332                         true,
1333                         []ft.File{root},
1334                         safcm.MsgSyncResp{
1335                                 FileChanges: []safcm.FileChange{
1336                                         {
1337                                                 Path:    "link",
1338                                                 Created: true,
1339                                                 New: safcm.FileChangeInfo{
1340                                                         Mode:  fs.ModeSymlink | 0777,
1341                                                         User:  user,
1342                                                         Uid:   uid,
1343                                                         Group: group,
1344                                                         Gid:   gid,
1345                                                 },
1346                                         },
1347                                 },
1348                         },
1349                         []string{
1350                                 `4: files: "link" (group): will create`,
1351                                 `3: files: "link" (group): creating`,
1352                                 `4: files: "link" (group): dry-run, skipping changes`,
1353                         },
1354                         nil,
1355                 },
1356
1357                 {
1358                         "symlink: unchanged",
1359                         safcm.MsgSyncReq{},
1360                         &safcm.File{
1361                                 Path:      "link",
1362                                 Mode:      fs.ModeSymlink | 0777,
1363                                 Uid:       -1,
1364                                 Gid:       -1,
1365                                 Data:      []byte("target"),
1366                                 OrigGroup: "group",
1367                         },
1368                         func() {
1369                                 ft.CreateSymlink("link", "target")
1370                         },
1371                         false,
1372                         []ft.File{
1373                                 root,
1374                                 {
1375                                         Path: "link",
1376                                         Mode: fs.ModeSymlink | 0777,
1377                                         Data: []byte("target"),
1378                                 },
1379                         },
1380                         safcm.MsgSyncResp{},
1381                         []string{
1382                                 `4: files: "link" (group): unchanged`,
1383                         },
1384                         nil,
1385                 },
1386
1387                 {
1388                         "symlink: content",
1389                         safcm.MsgSyncReq{},
1390                         &safcm.File{
1391                                 Path:      "link",
1392                                 Mode:      fs.ModeSymlink | 0777,
1393                                 Uid:       -1,
1394                                 Gid:       -1,
1395                                 Data:      []byte("target"),
1396                                 OrigGroup: "group",
1397                         },
1398                         func() {
1399                                 ft.CreateSymlink("link", "old-target")
1400                         },
1401                         true,
1402                         []ft.File{
1403                                 root,
1404                                 {
1405                                         Path: "link",
1406                                         Mode: fs.ModeSymlink | 0777,
1407                                         Data: []byte("target"),
1408                                 },
1409                         },
1410                         safcm.MsgSyncResp{
1411                                 FileChanges: []safcm.FileChange{
1412                                         {
1413                                                 Path: "link",
1414                                                 Old: safcm.FileChangeInfo{
1415                                                         Mode:  fs.ModeSymlink | 0777,
1416                                                         User:  user,
1417                                                         Uid:   uid,
1418                                                         Group: group,
1419                                                         Gid:   gid,
1420                                                 },
1421                                                 New: safcm.FileChangeInfo{
1422                                                         Mode:  fs.ModeSymlink | 0777,
1423                                                         User:  user,
1424                                                         Uid:   uid,
1425                                                         Group: group,
1426                                                         Gid:   gid,
1427                                                 },
1428                                                 DataDiff: `@@ -1 +1 @@
1429 -old-target
1430 +target
1431 `,
1432                                         },
1433                                 },
1434                         },
1435                         []string{
1436                                 `4: files: "link" (group): content differs`,
1437                                 `3: files: "link" (group): updating`,
1438                                 `4: files: "link" (group): creating temporary symlink ".linkRND"`,
1439                                 `4: files: "link" (group): renaming ".linkRND"`,
1440                         },
1441                         nil,
1442                 },
1443
1444                 // Directory
1445
1446                 {
1447                         "directory: create",
1448                         safcm.MsgSyncReq{},
1449                         &safcm.File{
1450                                 Path:      "dir",
1451                                 Mode:      fs.ModeDir | 0705,
1452                                 Uid:       -1,
1453                                 Gid:       -1,
1454                                 OrigGroup: "group",
1455                         },
1456                         nil,
1457                         true,
1458                         []ft.File{
1459                                 root,
1460                                 {
1461                                         Path: "dir",
1462                                         Mode: fs.ModeDir | 0705,
1463                                 },
1464                         },
1465                         safcm.MsgSyncResp{
1466                                 FileChanges: []safcm.FileChange{
1467                                         {
1468                                                 Path:    "dir",
1469                                                 Created: true,
1470                                                 New: safcm.FileChangeInfo{
1471                                                         Mode:  fs.ModeDir | 0705,
1472                                                         User:  user,
1473                                                         Uid:   uid,
1474                                                         Group: group,
1475                                                         Gid:   gid,
1476                                                 },
1477                                         },
1478                                 },
1479                         },
1480                         []string{
1481                                 `4: files: "dir" (group): will create`,
1482                                 `3: files: "dir" (group): creating`,
1483                                 `4: files: "dir" (group): creating directory`,
1484                                 `4: files: "dir" (group): chmodding drwx---r-x`,
1485                                 fmt.Sprintf(`4: files: "dir" (group): chowning %d/%d`, uid, gid),
1486                         },
1487                         nil,
1488                 },
1489                 {
1490                         "directory: create (dry-run)",
1491                         safcm.MsgSyncReq{
1492                                 DryRun: true,
1493                         },
1494                         &safcm.File{
1495                                 Path:      "dir",
1496                                 Mode:      fs.ModeDir | 0644,
1497                                 Uid:       -1,
1498                                 Gid:       -1,
1499                                 OrigGroup: "group",
1500                         },
1501                         nil,
1502                         true,
1503                         []ft.File{root},
1504                         safcm.MsgSyncResp{
1505                                 FileChanges: []safcm.FileChange{
1506                                         {
1507                                                 Path:    "dir",
1508                                                 Created: true,
1509                                                 New: safcm.FileChangeInfo{
1510                                                         Mode:  fs.ModeDir | 0644,
1511                                                         User:  user,
1512                                                         Uid:   uid,
1513                                                         Group: group,
1514                                                         Gid:   gid,
1515                                                 },
1516                                         },
1517                                 },
1518                         },
1519                         []string{
1520                                 `4: files: "dir" (group): will create`,
1521                                 `3: files: "dir" (group): creating`,
1522                                 `4: files: "dir" (group): dry-run, skipping changes`,
1523                         },
1524                         nil,
1525                 },
1526
1527                 {
1528                         "directory: create, missing parent (dry-run)",
1529                         safcm.MsgSyncReq{
1530                                 DryRun: true,
1531                         },
1532                         &safcm.File{
1533                                 Path:      "does-not-exist/dir",
1534                                 Mode:      fs.ModeDir | 0755,
1535                                 Uid:       -1,
1536                                 Gid:       -1,
1537                                 OrigGroup: "group",
1538                         },
1539                         nil,
1540                         true,
1541                         []ft.File{root},
1542                         safcm.MsgSyncResp{
1543                                 FileChanges: []safcm.FileChange{
1544                                         {
1545                                                 Path:    "does-not-exist/dir",
1546                                                 Created: true,
1547                                                 New: safcm.FileChangeInfo{
1548                                                         Mode:  fs.ModeDir | 0755,
1549                                                         User:  user,
1550                                                         Uid:   uid,
1551                                                         Group: group,
1552                                                         Gid:   gid,
1553                                                 },
1554                                         },
1555                                 },
1556                         },
1557                         []string{
1558                                 `4: files: "does-not-exist/dir" (group): will create (parent missing)`,
1559                                 `4: files: "does-not-exist/dir" (group): dry-run, skipping changes`,
1560                         },
1561                         nil,
1562                 },
1563
1564                 {
1565                         "directory: unchanged",
1566                         safcm.MsgSyncReq{},
1567                         &safcm.File{
1568                                 Path:      "dir",
1569                                 Mode:      fs.ModeDir | 0755,
1570                                 Uid:       -1,
1571                                 Gid:       -1,
1572                                 OrigGroup: "group",
1573                         },
1574                         func() {
1575                                 ft.CreateDirectory("dir", 0755)
1576                         },
1577                         false,
1578                         []ft.File{
1579                                 root,
1580                                 {
1581                                         Path: "dir",
1582                                         Mode: fs.ModeDir | 0755,
1583                                 },
1584                         },
1585                         safcm.MsgSyncResp{},
1586                         []string{
1587                                 `4: files: "dir" (group): unchanged`,
1588                         },
1589                         nil,
1590                 },
1591
1592                 {
1593                         "directory: permission",
1594                         safcm.MsgSyncReq{},
1595                         &safcm.File{
1596                                 Path:      "dir",
1597                                 Mode:      fs.ModeDir | 0755 | fs.ModeSetgid,
1598                                 Uid:       -1,
1599                                 Gid:       -1,
1600                                 OrigGroup: "group",
1601                         },
1602                         func() {
1603                                 ft.CreateDirectory("dir", 0500|fs.ModeSticky)
1604                         },
1605                         true,
1606                         []ft.File{
1607                                 root,
1608                                 {
1609                                         Path: "dir",
1610                                         Mode: fs.ModeDir | 0755 | fs.ModeSetgid,
1611                                 },
1612                         },
1613                         safcm.MsgSyncResp{
1614                                 FileChanges: []safcm.FileChange{
1615                                         {
1616                                                 Path: "dir",
1617                                                 Old: safcm.FileChangeInfo{
1618                                                         Mode:  fs.ModeDir | 0500 | fs.ModeSticky,
1619                                                         User:  user,
1620                                                         Uid:   uid,
1621                                                         Group: group,
1622                                                         Gid:   gid,
1623                                                 },
1624                                                 New: safcm.FileChangeInfo{
1625                                                         Mode:  fs.ModeDir | 0755 | fs.ModeSetgid,
1626                                                         User:  user,
1627                                                         Uid:   uid,
1628                                                         Group: group,
1629                                                         Gid:   gid,
1630                                                 },
1631                                         },
1632                                 },
1633                         },
1634                         []string{
1635                                 `4: files: "dir" (group): permission differs dtr-x------ -> dgrwxr-xr-x`,
1636                                 `3: files: "dir" (group): updating`,
1637                                 `4: files: "dir" (group): chmodding dgrwxr-xr-x`,
1638                         },
1639                         nil,
1640                 },
1641
1642                 // Type changes
1643
1644                 {
1645                         "change: file to directory",
1646                         safcm.MsgSyncReq{},
1647                         &safcm.File{
1648                                 Path:      "path",
1649                                 Mode:      fs.ModeDir | 0751,
1650                                 Uid:       -1,
1651                                 Gid:       -1,
1652                                 OrigGroup: "group",
1653                         },
1654                         func() {
1655                                 ft.CreateFile("path", "content\n", 0644)
1656                         },
1657                         true,
1658                         []ft.File{
1659                                 root,
1660                                 {
1661                                         Path: "path",
1662                                         Mode: fs.ModeDir | 0751,
1663                                 },
1664                         },
1665                         safcm.MsgSyncResp{
1666                                 FileChanges: []safcm.FileChange{
1667                                         {
1668                                                 Path: "path",
1669                                                 Old: safcm.FileChangeInfo{
1670                                                         Mode:  0644,
1671                                                         User:  user,
1672                                                         Uid:   uid,
1673                                                         Group: group,
1674                                                         Gid:   gid,
1675                                                 },
1676                                                 New: safcm.FileChangeInfo{
1677                                                         Mode:  fs.ModeDir | 0751,
1678                                                         User:  user,
1679                                                         Uid:   uid,
1680                                                         Group: group,
1681                                                         Gid:   gid,
1682                                                 },
1683                                                 DataDiff: `@@ -1,2 +1 @@
1684 -content
1685  
1686 `,
1687                                         },
1688                                 },
1689                         },
1690                         []string{
1691                                 `4: files: "path" (group): type differs ---------- -> d---------`,
1692                                 `3: files: "path" (group): updating`,
1693                                 `4: files: "path" (group): removing (due to type change)`,
1694                                 `4: files: "path" (group): creating directory`,
1695                                 `4: files: "path" (group): chmodding drwxr-x--x`,
1696                                 fmt.Sprintf(`4: files: "path" (group): chowning %d/%d`, uid, gid),
1697                         },
1698                         nil,
1699                 },
1700
1701                 {
1702                         "change: file to symlink",
1703                         safcm.MsgSyncReq{},
1704                         &safcm.File{
1705                                 Path:      "path",
1706                                 Mode:      fs.ModeSymlink | 0777,
1707                                 Uid:       -1,
1708                                 Gid:       -1,
1709                                 OrigGroup: "group",
1710                                 Data:      []byte("target"),
1711                         },
1712                         func() {
1713                                 ft.CreateFile("path", "content\n", 0644)
1714                         },
1715                         true,
1716                         []ft.File{
1717                                 root,
1718                                 {
1719                                         Path: "path",
1720                                         Mode: fs.ModeSymlink | 0777,
1721                                         Data: []byte("target"),
1722                                 },
1723                         },
1724                         safcm.MsgSyncResp{
1725                                 FileChanges: []safcm.FileChange{
1726                                         {
1727                                                 Path: "path",
1728                                                 Old: safcm.FileChangeInfo{
1729                                                         Mode:  0644,
1730                                                         User:  user,
1731                                                         Uid:   uid,
1732                                                         Group: group,
1733                                                         Gid:   gid,
1734                                                 },
1735                                                 New: safcm.FileChangeInfo{
1736                                                         Mode:  fs.ModeSymlink | 0777,
1737                                                         User:  user,
1738                                                         Uid:   uid,
1739                                                         Group: group,
1740                                                         Gid:   gid,
1741                                                 },
1742                                                 DataDiff: `@@ -1,2 +1 @@
1743 -content
1744 -
1745 +target
1746 `,
1747                                         },
1748                                 },
1749                         },
1750                         []string{
1751                                 `4: files: "path" (group): type differs ---------- -> L---------`,
1752                                 `3: files: "path" (group): updating`,
1753                                 `4: files: "path" (group): creating temporary symlink ".pathRND"`,
1754                                 `4: files: "path" (group): renaming ".pathRND"`,
1755                         },
1756                         nil,
1757                 },
1758
1759                 {
1760                         "change: symlink to file",
1761                         safcm.MsgSyncReq{},
1762                         &safcm.File{
1763                                 Path:      "path",
1764                                 Mode:      0640,
1765                                 Uid:       -1,
1766                                 Gid:       -1,
1767                                 OrigGroup: "group",
1768                                 Data:      []byte("content\n"),
1769                         },
1770                         func() {
1771                                 ft.CreateSymlink("path", "target")
1772                         },
1773                         true,
1774                         []ft.File{
1775                                 root,
1776                                 {
1777                                         Path: "path",
1778                                         Mode: 0640,
1779                                         Data: []byte("content\n"),
1780                                 },
1781                         },
1782                         safcm.MsgSyncResp{
1783                                 FileChanges: []safcm.FileChange{
1784                                         {
1785                                                 Path: "path",
1786                                                 Old: safcm.FileChangeInfo{
1787                                                         Mode:  fs.ModeSymlink | 0777,
1788                                                         User:  user,
1789                                                         Uid:   uid,
1790                                                         Group: group,
1791                                                         Gid:   gid,
1792                                                 },
1793                                                 New: safcm.FileChangeInfo{
1794                                                         Mode:  0640,
1795                                                         User:  user,
1796                                                         Uid:   uid,
1797                                                         Group: group,
1798                                                         Gid:   gid,
1799                                                 },
1800                                                 DataDiff: `@@ -1 +1,2 @@
1801 -target
1802 +content
1803 +
1804 `,
1805                                         },
1806                                 },
1807                         },
1808                         []string{
1809                                 `4: files: "path" (group): type differs L--------- -> ----------`,
1810                                 `3: files: "path" (group): updating`,
1811                                 `4: files: "path" (group): creating temporary file ".path*"`,
1812                                 `4: files: "path" (group): renaming ".pathRND"`,
1813                         },
1814                         nil,
1815                 },
1816
1817                 {
1818                         "change: symlink to directory",
1819                         safcm.MsgSyncReq{},
1820                         &safcm.File{
1821                                 Path:      "path",
1822                                 Mode:      fs.ModeDir | 0751,
1823                                 Uid:       -1,
1824                                 Gid:       -1,
1825                                 OrigGroup: "group",
1826                         },
1827                         func() {
1828                                 ft.CreateSymlink("path", "target")
1829                         },
1830                         true,
1831                         []ft.File{
1832                                 root,
1833                                 {
1834                                         Path: "path",
1835                                         Mode: fs.ModeDir | 0751,
1836                                 },
1837                         },
1838                         safcm.MsgSyncResp{
1839                                 FileChanges: []safcm.FileChange{
1840                                         {
1841                                                 Path: "path",
1842                                                 Old: safcm.FileChangeInfo{
1843                                                         Mode:  fs.ModeSymlink | 0777,
1844                                                         User:  user,
1845                                                         Uid:   uid,
1846                                                         Group: group,
1847                                                         Gid:   gid,
1848                                                 },
1849                                                 New: safcm.FileChangeInfo{
1850                                                         Mode:  fs.ModeDir | 0751,
1851                                                         User:  user,
1852                                                         Uid:   uid,
1853                                                         Group: group,
1854                                                         Gid:   gid,
1855                                                 },
1856                                                 DataDiff: `@@ -1 +1 @@
1857 -target
1858 +
1859 `,
1860                                         },
1861                                 },
1862                         },
1863                         []string{
1864                                 `4: files: "path" (group): type differs L--------- -> d---------`,
1865                                 `3: files: "path" (group): updating`,
1866                                 `4: files: "path" (group): removing (due to type change)`,
1867                                 `4: files: "path" (group): creating directory`,
1868                                 `4: files: "path" (group): chmodding drwxr-x--x`,
1869                                 fmt.Sprintf(`4: files: "path" (group): chowning %d/%d`, uid, gid),
1870                         },
1871                         nil,
1872                 },
1873
1874                 {
1875                         "change: directory to file",
1876                         safcm.MsgSyncReq{},
1877                         &safcm.File{
1878                                 Path:      "path",
1879                                 Mode:      0666,
1880                                 Uid:       -1,
1881                                 Gid:       -1,
1882                                 OrigGroup: "group",
1883                                 Data:      []byte("content\n"),
1884                         },
1885                         func() {
1886                                 ft.CreateDirectory("path", 0777)
1887                         },
1888                         true,
1889                         []ft.File{
1890                                 root,
1891                                 {
1892                                         Path: "path",
1893                                         Mode: 0666,
1894                                         Data: []byte("content\n"),
1895                                 },
1896                         },
1897                         safcm.MsgSyncResp{
1898                                 FileChanges: []safcm.FileChange{
1899                                         {
1900                                                 Path: "path",
1901                                                 Old: safcm.FileChangeInfo{
1902                                                         Mode:  fs.ModeDir | 0777,
1903                                                         User:  user,
1904                                                         Uid:   uid,
1905                                                         Group: group,
1906                                                         Gid:   gid,
1907                                                 },
1908                                                 New: safcm.FileChangeInfo{
1909                                                         Mode:  0666,
1910                                                         User:  user,
1911                                                         Uid:   uid,
1912                                                         Group: group,
1913                                                         Gid:   gid,
1914                                                 },
1915                                         },
1916                                 },
1917                         },
1918                         []string{
1919                                 `4: files: "path" (group): type differs d--------- -> ----------`,
1920                                 `3: files: "path" (group): updating`,
1921                                 `4: files: "path" (group): removing (due to type change)`,
1922                                 `4: files: "path" (group): creating temporary file ".path*"`,
1923                                 `4: files: "path" (group): renaming ".pathRND"`,
1924                         },
1925                         nil,
1926                 },
1927                 {
1928                         "change: directory to file (non-empty)",
1929                         safcm.MsgSyncReq{},
1930                         &safcm.File{
1931                                 Path:      "path",
1932                                 Mode:      0666,
1933                                 Uid:       -1,
1934                                 Gid:       -1,
1935                                 OrigGroup: "group",
1936                                 Data:      []byte("content\n"),
1937                         },
1938                         func() {
1939                                 ft.CreateDirectory("path", 0777)
1940                                 ft.CreateFile("path/file", "content\n", 0644)
1941                         },
1942                         true,
1943                         []ft.File{
1944                                 root,
1945                                 {
1946                                         Path: "path",
1947                                         Mode: fs.ModeDir | 0777,
1948                                 },
1949                                 {
1950                                         Path: "path/file",
1951                                         Mode: 0644,
1952                                         Data: []byte("content\n"),
1953                                 },
1954                         },
1955                         safcm.MsgSyncResp{
1956                                 FileChanges: []safcm.FileChange{
1957                                         {
1958                                                 Path: "path",
1959                                                 Old: safcm.FileChangeInfo{
1960                                                         Mode:  fs.ModeDir | 0777,
1961                                                         User:  user,
1962                                                         Uid:   uid,
1963                                                         Group: group,
1964                                                         Gid:   gid,
1965                                                 },
1966                                                 New: safcm.FileChangeInfo{
1967                                                         Mode:  0666,
1968                                                         User:  user,
1969                                                         Uid:   uid,
1970                                                         Group: group,
1971                                                         Gid:   gid,
1972                                                 },
1973                                         },
1974                                 },
1975                         },
1976                         []string{
1977                                 `4: files: "path" (group): type differs d--------- -> ----------`,
1978                                 `3: files: "path" (group): updating`,
1979                                 `4: files: "path" (group): removing (due to type change)`,
1980                         },
1981                         fmt.Errorf("will not replace non-empty directory, please remove manually"),
1982                 },
1983
1984                 {
1985                         "change: directory to symlink",
1986                         safcm.MsgSyncReq{},
1987                         &safcm.File{
1988                                 Path:      "path",
1989                                 Mode:      fs.ModeSymlink | 0777,
1990                                 Uid:       -1,
1991                                 Gid:       -1,
1992                                 OrigGroup: "group",
1993                                 Data:      []byte("target"),
1994                         },
1995                         func() {
1996                                 ft.CreateDirectory("path", 0777)
1997                         },
1998                         true,
1999                         []ft.File{
2000                                 root,
2001                                 {
2002                                         Path: "path",
2003                                         Mode: fs.ModeSymlink | 0777,
2004                                         Data: []byte("target"),
2005                                 },
2006                         },
2007                         safcm.MsgSyncResp{
2008                                 FileChanges: []safcm.FileChange{
2009                                         {
2010                                                 Path: "path",
2011                                                 Old: safcm.FileChangeInfo{
2012                                                         Mode:  fs.ModeDir | 0777,
2013                                                         User:  user,
2014                                                         Uid:   uid,
2015                                                         Group: group,
2016                                                         Gid:   gid,
2017                                                 },
2018                                                 New: safcm.FileChangeInfo{
2019                                                         Mode:  fs.ModeSymlink | 0777,
2020                                                         User:  user,
2021                                                         Uid:   uid,
2022                                                         Group: group,
2023                                                         Gid:   gid,
2024                                                 },
2025                                         },
2026                                 },
2027                         },
2028                         []string{
2029                                 `4: files: "path" (group): type differs d--------- -> L---------`,
2030                                 `3: files: "path" (group): updating`,
2031                                 `4: files: "path" (group): removing (due to type change)`,
2032                                 `4: files: "path" (group): creating temporary symlink ".pathRND"`,
2033                                 `4: files: "path" (group): renaming ".pathRND"`,
2034                         },
2035                         nil,
2036                 },
2037
2038                 {
2039                         "change: other to file",
2040                         safcm.MsgSyncReq{},
2041                         &safcm.File{
2042                                 Path:      "path",
2043                                 Mode:      0640,
2044                                 Uid:       -1,
2045                                 Gid:       -1,
2046                                 OrigGroup: "group",
2047                                 Data:      []byte("content\n"),
2048                         },
2049                         func() {
2050                                 ft.CreateFifo("path", 0666)
2051                         },
2052                         true,
2053                         []ft.File{
2054                                 root,
2055                                 {
2056                                         Path: "path",
2057                                         Mode: 0640,
2058                                         Data: []byte("content\n"),
2059                                 },
2060                         },
2061                         safcm.MsgSyncResp{
2062                                 FileChanges: []safcm.FileChange{
2063                                         {
2064                                                 Path: "path",
2065                                                 Old: safcm.FileChangeInfo{
2066                                                         Mode:  fs.ModeNamedPipe | 0666,
2067                                                         User:  user,
2068                                                         Uid:   uid,
2069                                                         Group: group,
2070                                                         Gid:   gid,
2071                                                 },
2072                                                 New: safcm.FileChangeInfo{
2073                                                         Mode:  0640,
2074                                                         User:  user,
2075                                                         Uid:   uid,
2076                                                         Group: group,
2077                                                         Gid:   gid,
2078                                                 },
2079                                         },
2080                                 },
2081                         },
2082                         []string{
2083                                 `4: files: "path" (group): type differs p--------- -> ----------`,
2084                                 `3: files: "path" (group): updating`,
2085                                 `4: files: "path" (group): creating temporary file ".path*"`,
2086                                 `4: files: "path" (group): renaming ".pathRND"`,
2087                         },
2088                         nil,
2089                 },
2090
2091                 {
2092                         "change: other to symlink",
2093                         safcm.MsgSyncReq{},
2094                         &safcm.File{
2095                                 Path:      "path",
2096                                 Mode:      fs.ModeSymlink | 0777,
2097                                 Uid:       -1,
2098                                 Gid:       -1,
2099                                 OrigGroup: "group",
2100                                 Data:      []byte("target"),
2101                         },
2102                         func() {
2103                                 ft.CreateFifo("path", 0666)
2104                         },
2105                         true,
2106                         []ft.File{
2107                                 root,
2108                                 {
2109                                         Path: "path",
2110                                         Mode: fs.ModeSymlink | 0777,
2111                                         Data: []byte("target"),
2112                                 },
2113                         },
2114                         safcm.MsgSyncResp{
2115                                 FileChanges: []safcm.FileChange{
2116                                         {
2117                                                 Path: "path",
2118                                                 Old: safcm.FileChangeInfo{
2119                                                         Mode:  fs.ModeNamedPipe | 0666,
2120                                                         User:  user,
2121                                                         Uid:   uid,
2122                                                         Group: group,
2123                                                         Gid:   gid,
2124                                                 },
2125                                                 New: safcm.FileChangeInfo{
2126                                                         Mode:  fs.ModeSymlink | 0777,
2127                                                         User:  user,
2128                                                         Uid:   uid,
2129                                                         Group: group,
2130                                                         Gid:   gid,
2131                                                 },
2132                                         },
2133                                 },
2134                         },
2135                         []string{
2136                                 `4: files: "path" (group): type differs p--------- -> L---------`,
2137                                 `3: files: "path" (group): updating`,
2138                                 `4: files: "path" (group): creating temporary symlink ".pathRND"`,
2139                                 `4: files: "path" (group): renaming ".pathRND"`,
2140                         },
2141                         nil,
2142                 },
2143
2144                 {
2145                         "change: other to directory",
2146                         safcm.MsgSyncReq{},
2147                         &safcm.File{
2148                                 Path:      "path",
2149                                 Mode:      fs.ModeDir | 0751,
2150                                 Uid:       -1,
2151                                 Gid:       -1,
2152                                 OrigGroup: "group",
2153                         },
2154                         func() {
2155                                 ft.CreateFifo("path", 0666)
2156                         },
2157                         true,
2158                         []ft.File{
2159                                 root,
2160                                 {
2161                                         Path: "path",
2162                                         Mode: fs.ModeDir | 0751,
2163                                 },
2164                         },
2165                         safcm.MsgSyncResp{
2166                                 FileChanges: []safcm.FileChange{
2167                                         {
2168                                                 Path: "path",
2169                                                 Old: safcm.FileChangeInfo{
2170                                                         Mode:  fs.ModeNamedPipe | 0666,
2171                                                         User:  user,
2172                                                         Uid:   uid,
2173                                                         Group: group,
2174                                                         Gid:   gid,
2175                                                 },
2176                                                 New: safcm.FileChangeInfo{
2177                                                         Mode:  fs.ModeDir | 0751,
2178                                                         User:  user,
2179                                                         Uid:   uid,
2180                                                         Group: group,
2181                                                         Gid:   gid,
2182                                                 },
2183                                         },
2184                                 },
2185                         },
2186                         []string{
2187                                 `4: files: "path" (group): type differs p--------- -> d---------`,
2188                                 `3: files: "path" (group): updating`,
2189                                 `4: files: "path" (group): removing (due to type change)`,
2190                                 `4: files: "path" (group): creating directory`,
2191                                 `4: files: "path" (group): chmodding drwxr-x--x`,
2192                                 fmt.Sprintf(`4: files: "path" (group): chowning %d/%d`, uid, gid),
2193                         },
2194                         nil,
2195                 },
2196
2197                 {
2198                         "change: file to symlink (same content)",
2199                         safcm.MsgSyncReq{},
2200                         &safcm.File{
2201                                 Path:      "path",
2202                                 Mode:      fs.ModeSymlink | 0777,
2203                                 Uid:       -1,
2204                                 Gid:       -1,
2205                                 OrigGroup: "group",
2206                                 Data:      []byte("target"),
2207                         },
2208                         func() {
2209                                 ft.CreateFile("path", "target", 0644)
2210                         },
2211                         true,
2212                         []ft.File{
2213                                 root,
2214                                 {
2215                                         Path: "path",
2216                                         Mode: fs.ModeSymlink | 0777,
2217                                         Data: []byte("target"),
2218                                 },
2219                         },
2220                         safcm.MsgSyncResp{
2221                                 FileChanges: []safcm.FileChange{
2222                                         {
2223                                                 Path: "path",
2224                                                 Old: safcm.FileChangeInfo{
2225                                                         Mode:  0644,
2226                                                         User:  user,
2227                                                         Uid:   uid,
2228                                                         Group: group,
2229                                                         Gid:   gid,
2230                                                 },
2231                                                 New: safcm.FileChangeInfo{
2232                                                         Mode:  fs.ModeSymlink | 0777,
2233                                                         User:  user,
2234                                                         Uid:   uid,
2235                                                         Group: group,
2236                                                         Gid:   gid,
2237                                                 },
2238                                         },
2239                                 },
2240                         },
2241                         []string{
2242                                 `4: files: "path" (group): type differs ---------- -> L---------`,
2243                                 `3: files: "path" (group): updating`,
2244                                 `4: files: "path" (group): creating temporary symlink ".pathRND"`,
2245                                 `4: files: "path" (group): renaming ".pathRND"`,
2246                         },
2247                         nil,
2248                 },
2249
2250                 // Symlink "attacks"
2251
2252                 {
2253                         "symlink in earlier path component",
2254                         safcm.MsgSyncReq{},
2255                         &safcm.File{
2256                                 Path:      "dir/file",
2257                                 Mode:      0644,
2258                                 Uid:       -1,
2259                                 Gid:       -1,
2260                                 OrigGroup: "group",
2261                                 Data:      []byte("content"),
2262                         },
2263                         func() {
2264                                 ft.CreateDirectory("tmp", 0755)
2265                                 ft.CreateSymlink("dir", "tmp")
2266                         },
2267                         false,
2268                         []ft.File{
2269                                 root,
2270                                 {
2271                                         Path: "dir",
2272                                         Mode: fs.ModeSymlink | 0777,
2273                                         Data: []byte("tmp"),
2274                                 },
2275                                 {
2276                                         Path: "tmp",
2277                                         Mode: fs.ModeDir | 0755,
2278                                 },
2279                         },
2280                         safcm.MsgSyncResp{},
2281                         nil,
2282                         fmt.Errorf("symlink not permitted in path: \"dir\""),
2283                 },
2284
2285                 // Border cases
2286
2287                 {
2288                         "relative path with leading dot",
2289                         safcm.MsgSyncReq{},
2290                         &safcm.File{
2291                                 Path:      "./dir/file",
2292                                 Mode:      0644,
2293                                 Uid:       -1,
2294                                 Gid:       -1,
2295                                 OrigGroup: "group",
2296                                 Data:      []byte("content"),
2297                         },
2298                         func() {
2299                                 ft.CreateDirectory("dir", 0755)
2300                         },
2301                         true,
2302                         []ft.File{
2303                                 root,
2304                                 {
2305                                         Path: "dir",
2306                                         Mode: fs.ModeDir | 0755,
2307                                 },
2308                                 {
2309                                         Path: "dir/file",
2310                                         Mode: 0644,
2311                                         Data: []byte("content"),
2312                                 },
2313                         },
2314                         safcm.MsgSyncResp{
2315                                 FileChanges: []safcm.FileChange{
2316                                         {
2317                                                 Path:    "./dir/file",
2318                                                 Created: true,
2319                                                 New: safcm.FileChangeInfo{
2320                                                         Mode:  0644,
2321                                                         User:  user,
2322                                                         Uid:   uid,
2323                                                         Group: group,
2324                                                         Gid:   gid,
2325                                                 },
2326                                         },
2327                                 },
2328                         },
2329                         []string{
2330                                 `4: files: "./dir/file" (group): will create`,
2331                                 `3: files: "./dir/file" (group): creating`,
2332                                 `4: files: "./dir/file" (group): creating temporary file "dir/.file*"`,
2333                                 `4: files: "./dir/file" (group): renaming "dir/.fileRND"`,
2334                         },
2335                         nil,
2336                 },
2337
2338                 // Diffs
2339
2340                 {
2341                         "diff: textual",
2342                         safcm.MsgSyncReq{
2343                                 DryRun: true,
2344                         },
2345                         &safcm.File{
2346                                 Path: "file",
2347                                 Mode: 0644,
2348                                 Uid:  -1,
2349                                 Gid:  -1,
2350                                 Data: []byte(`
2351 this
2352 is
2353 a
2354 simple
2355 file
2356 `),
2357                                 OrigGroup: "group",
2358                         },
2359                         func() {
2360                                 ft.CreateFile("file", `this
2361 is
2362 file
2363 !
2364 `, 0644)
2365                         },
2366                         true,
2367                         []ft.File{
2368                                 root,
2369                                 {
2370                                         Path: "file",
2371                                         Mode: 0644,
2372                                         Data: []byte(`this
2373 is
2374 file
2375 !
2376 `),
2377                                 },
2378                         },
2379                         safcm.MsgSyncResp{
2380                                 FileChanges: []safcm.FileChange{
2381                                         {
2382                                                 Path: "file",
2383                                                 Old: safcm.FileChangeInfo{
2384                                                         Mode:  0644,
2385                                                         User:  user,
2386                                                         Uid:   uid,
2387                                                         Group: group,
2388                                                         Gid:   gid,
2389                                                 },
2390                                                 New: safcm.FileChangeInfo{
2391                                                         Mode:  0644,
2392                                                         User:  user,
2393                                                         Uid:   uid,
2394                                                         Group: group,
2395                                                         Gid:   gid,
2396                                                 },
2397                                                 DataDiff: `@@ -1,5 +1,7 @@
2398 +
2399  this
2400  is
2401 +a
2402 +simple
2403  file
2404 -!
2405  
2406 `,
2407                                         },
2408                                 },
2409                         },
2410                         []string{
2411                                 `4: files: "file" (group): content differs`,
2412                                 `3: files: "file" (group): updating`,
2413                                 `4: files: "file" (group): dry-run, skipping changes`,
2414                         },
2415                         nil,
2416                 },
2417
2418                 {
2419                         "diff: binary both",
2420                         safcm.MsgSyncReq{
2421                                 DryRun: true,
2422                         },
2423                         &safcm.File{
2424                                 Path:      "file",
2425                                 Mode:      0644,
2426                                 Uid:       -1,
2427                                 Gid:       -1,
2428                                 Data:      []byte("\x00\x01\x02\x03"),
2429                                 OrigGroup: "group",
2430                         },
2431                         func() {
2432                                 ft.CreateFile("file", "\x00\x01\x02", 0644)
2433                         },
2434                         true,
2435                         []ft.File{
2436                                 root,
2437                                 {
2438                                         Path: "file",
2439                                         Mode: 0644,
2440                                         Data: []byte("\x00\x01\x02"),
2441                                 },
2442                         },
2443                         safcm.MsgSyncResp{
2444                                 FileChanges: []safcm.FileChange{
2445                                         {
2446                                                 Path: "file",
2447                                                 Old: safcm.FileChangeInfo{
2448                                                         Mode:  0644,
2449                                                         User:  user,
2450                                                         Uid:   uid,
2451                                                         Group: group,
2452                                                         Gid:   gid,
2453                                                 },
2454                                                 New: safcm.FileChangeInfo{
2455                                                         Mode:  0644,
2456                                                         User:  user,
2457                                                         Uid:   uid,
2458                                                         Group: group,
2459                                                         Gid:   gid,
2460                                                 },
2461                                                 DataDiff: "Binary files differ (3 -> 4 bytes), cannot show diff",
2462                                         },
2463                                 },
2464                         },
2465                         []string{
2466                                 `4: files: "file" (group): content differs`,
2467                                 `3: files: "file" (group): updating`,
2468                                 `4: files: "file" (group): dry-run, skipping changes`,
2469                         },
2470                         nil,
2471                 },
2472
2473                 {
2474                         "diff: binary old",
2475                         safcm.MsgSyncReq{
2476                                 DryRun: true,
2477                         },
2478                         &safcm.File{
2479                                 Path:      "file",
2480                                 Mode:      0644,
2481                                 Uid:       -1,
2482                                 Gid:       -1,
2483                                 Data:      []byte("content\n"),
2484                                 OrigGroup: "group",
2485                         },
2486                         func() {
2487                                 ft.CreateFile("file", "\x00\x01\x02", 0644)
2488                         },
2489                         true,
2490                         []ft.File{
2491                                 root,
2492                                 {
2493                                         Path: "file",
2494                                         Mode: 0644,
2495                                         Data: []byte("\x00\x01\x02"),
2496                                 },
2497                         },
2498                         safcm.MsgSyncResp{
2499                                 FileChanges: []safcm.FileChange{
2500                                         {
2501                                                 Path: "file",
2502                                                 Old: safcm.FileChangeInfo{
2503                                                         Mode:  0644,
2504                                                         User:  user,
2505                                                         Uid:   uid,
2506                                                         Group: group,
2507                                                         Gid:   gid,
2508                                                 },
2509                                                 New: safcm.FileChangeInfo{
2510                                                         Mode:  0644,
2511                                                         User:  user,
2512                                                         Uid:   uid,
2513                                                         Group: group,
2514                                                         Gid:   gid,
2515                                                 },
2516                                                 DataDiff: `@@ -1,2 +1,2 @@
2517 -<binary content, 3 bytes>
2518 +content
2519  
2520 `,
2521                                         },
2522                                 },
2523                         },
2524                         []string{
2525                                 `4: files: "file" (group): content differs`,
2526                                 `3: files: "file" (group): updating`,
2527                                 `4: files: "file" (group): dry-run, skipping changes`,
2528                         },
2529                         nil,
2530                 },
2531
2532                 {
2533                         "diff: binary new",
2534                         safcm.MsgSyncReq{
2535                                 DryRun: true,
2536                         },
2537                         &safcm.File{
2538                                 Path:      "file",
2539                                 Mode:      0644,
2540                                 Uid:       -1,
2541                                 Gid:       -1,
2542                                 Data:      []byte("\x00\x01\x02\x03"),
2543                                 OrigGroup: "group",
2544                         },
2545                         func() {
2546                                 ft.CreateFile("file", "content\n", 0644)
2547                         },
2548                         true,
2549                         []ft.File{
2550                                 root,
2551                                 {
2552                                         Path: "file",
2553                                         Mode: 0644,
2554                                         Data: []byte("content\n"),
2555                                 },
2556                         },
2557                         safcm.MsgSyncResp{
2558                                 FileChanges: []safcm.FileChange{
2559                                         {
2560                                                 Path: "file",
2561                                                 Old: safcm.FileChangeInfo{
2562                                                         Mode:  0644,
2563                                                         User:  user,
2564                                                         Uid:   uid,
2565                                                         Group: group,
2566                                                         Gid:   gid,
2567                                                 },
2568                                                 New: safcm.FileChangeInfo{
2569                                                         Mode:  0644,
2570                                                         User:  user,
2571                                                         Uid:   uid,
2572                                                         Group: group,
2573                                                         Gid:   gid,
2574                                                 },
2575                                                 DataDiff: `@@ -1,2 +1,2 @@
2576 -content
2577 +<binary content, 4 bytes>
2578  
2579 `,
2580                                         },
2581                                 },
2582                         },
2583                         []string{
2584                                 `4: files: "file" (group): content differs`,
2585                                 `3: files: "file" (group): updating`,
2586                                 `4: files: "file" (group): dry-run, skipping changes`,
2587                         },
2588                         nil,
2589                 },
2590         }
2591
2592         for _, tc := range tests {
2593                 t.Run(tc.name, func(t *testing.T) {
2594                         // Create separate test directory for each test case
2595                         path := filepath.Join(cwd, "testdata", "file-"+tc.name)
2596                         err := os.Mkdir(path, 0700)
2597                         if err != nil {
2598                                 t.Fatal(err)
2599                         }
2600                         err = os.Chdir(path)
2601                         if err != nil {
2602                                 t.Fatal(err)
2603                         }
2604
2605                         if tc.prepare != nil {
2606                                 tc.prepare()
2607                         }
2608
2609                         s, res := prepareSync(tc.req, &testRunner{
2610                                 t: t,
2611                         })
2612                         err = s.setDefaults()
2613                         if err != nil {
2614                                 t.Fatal(err)
2615                         }
2616
2617                         // Deterministic temporary symlink names
2618                         rand.Seed(0)
2619
2620                         var changed bool
2621                         err = s.syncFile(tc.file, &changed)
2622                         testutil.AssertErrorEqual(t, "err", err, tc.expErr)
2623                         dbg := res.Wait()
2624                         // Remove random file names from result
2625                         for i, x := range dbg {
2626                                 dbg[i] = randFilesRegexp.ReplaceAllString(x, `RND"`)
2627                         }
2628                         testutil.AssertEqual(t, "dbg", dbg, tc.expDbg)
2629
2630                         files, err := ft.WalkDir(path)
2631                         if err != nil {
2632                                 t.Fatal(err)
2633                         }
2634                         testutil.AssertEqual(t, "files", files, tc.expFiles)
2635
2636                         testutil.AssertEqual(t, "changed", changed, tc.expChanged)
2637                         testutil.AssertEqual(t, "resp", s.resp, tc.expResp)
2638                 })
2639         }
2640
2641         if !t.Failed() {
2642                 err = os.RemoveAll(filepath.Join(cwd, "testdata"))
2643                 if err != nil {
2644                         t.Fatal(err)
2645                 }
2646         }
2647 }