]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - cmd/safcm/sync_sync_test.go
tests: use subtests
[safcm/safcm.git] / cmd / safcm / sync_sync_test.go
1 // Copyright (C) 2021  Simon Ruderich
2 //
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16 package main
17
18 import (
19         "fmt"
20         "io"
21         "io/fs"
22         "log"
23         "os"
24         "path/filepath"
25         "reflect"
26         "testing"
27
28         "github.com/google/go-cmp/cmp"
29
30         "ruderich.org/simon/safcm"
31 )
32
33 func TestHostSyncReq(t *testing.T) {
34         cwd, err := os.Getwd()
35         if err != nil {
36                 t.Fatal(err)
37         }
38         defer os.Chdir(cwd)
39
40         tests := []struct {
41                 name      string
42                 project   string
43                 host      string
44                 detected  []string
45                 level     safcm.LogLevel
46                 exp       safcm.MsgSyncReq
47                 expEvents []string
48                 expErr    error
49         }{
50
51                 // NOTE: Also update MsgSyncReq in safcm-remote test cases
52                 // when changing anything here!
53
54                 {
55                         "project: host1",
56                         "project",
57                         "host1.example.org",
58                         nil,
59                         safcm.LogDebug3,
60                         safcm.MsgSyncReq{
61                                 Groups: []string{
62                                         "all",
63                                         "group",
64                                         "remove",
65                                         "host1.example.org",
66                                 },
67                                 Files: map[string]*safcm.File{
68                                         "/": &safcm.File{Path: "/",
69                                                 OrigGroup: "group",
70                                                 Mode:      fs.ModeDir | 0755 | fs.ModeSetgid,
71                                                 Uid:       -1,
72                                                 Gid:       -1,
73                                                 TriggerCommands: []string{
74                                                         "touch /.update",
75                                                 },
76                                         },
77                                         "/etc": &safcm.File{
78                                                 OrigGroup: "group",
79                                                 Path:      "/etc",
80                                                 Mode:      fs.ModeDir | 0755,
81                                                 Uid:       -1,
82                                                 Gid:       -1,
83                                         },
84                                         "/etc/.hidden": &safcm.File{
85                                                 OrigGroup: "group",
86                                                 Path:      "/etc/.hidden",
87                                                 Mode:      0100 | fs.ModeSetuid | fs.ModeSetgid | fs.ModeSticky,
88                                                 Uid:       -1,
89                                                 Gid:       -1,
90                                                 Data:      []byte("..."),
91                                         },
92                                         "/etc/motd": &safcm.File{
93                                                 OrigGroup: "group",
94                                                 Path:      "/etc/motd",
95                                                 Mode:      0644,
96                                                 Uid:       -1,
97                                                 Gid:       -1,
98                                                 Data:      []byte("Welcome to Host ONE\n\n\n\n"),
99                                         },
100                                         "/etc/rc.local": &safcm.File{
101                                                 OrigGroup: "group",
102                                                 Path:      "/etc/rc.local",
103                                                 Mode:      0700,
104                                                 Uid:       -1,
105                                                 Gid:       -1,
106                                                 Data:      []byte("#!/bin/sh\n"),
107                                                 TriggerCommands: []string{
108                                                         "/etc/rc.local",
109                                                 },
110                                         },
111                                         "/etc/resolv.conf": &safcm.File{
112                                                 OrigGroup: "group",
113                                                 Path:      "/etc/resolv.conf",
114                                                 Mode:      0641,
115                                                 User:      "user",
116                                                 Uid:       -1,
117                                                 Group:     "group",
118                                                 Gid:       -1,
119                                                 Data:      []byte("nameserver ::1\n"),
120                                                 TriggerCommands: []string{
121                                                         "echo resolv.conf updated",
122                                                 },
123                                         },
124                                         "/etc/test": &safcm.File{
125                                                 OrigGroup: "group",
126                                                 Path:      "/etc/test",
127                                                 Mode:      os.ModeSymlink | 0777,
128                                                 Uid:       -1,
129                                                 Gid:       -1,
130                                                 Data:      []byte("doesnt-exist"),
131                                         },
132                                 },
133                                 Packages: []string{
134                                         "unbound",
135                                         "unbound-anchor",
136                                 },
137                                 Services: []string{
138                                         "unbound",
139                                 },
140                                 Commands: []string{
141                                         "echo command one",
142                                         "echo -n command two",
143                                 },
144                         },
145                         []string{
146                                 "host1.example.org: <nil> 3 host groups: all group host1.example.org remove",
147                                 "host1.example.org: <nil> 3 host group priorities (desc. order): host1.example.org",
148                         },
149                         nil,
150                 },
151
152                 {
153                         "project: host1 (log level info)",
154                         "project",
155                         "host1.example.org",
156                         nil,
157                         safcm.LogInfo,
158                         safcm.MsgSyncReq{
159                                 Groups: []string{
160                                         "all",
161                                         "group",
162                                         "remove",
163                                         "host1.example.org",
164                                 },
165                                 Files: map[string]*safcm.File{
166                                         "/": &safcm.File{Path: "/",
167                                                 OrigGroup: "group",
168                                                 Mode:      fs.ModeDir | 0755 | fs.ModeSetgid,
169                                                 Uid:       -1,
170                                                 Gid:       -1,
171                                                 TriggerCommands: []string{
172                                                         "touch /.update",
173                                                 },
174                                         },
175                                         "/etc": &safcm.File{
176                                                 OrigGroup: "group",
177                                                 Path:      "/etc",
178                                                 Mode:      fs.ModeDir | 0755,
179                                                 Uid:       -1,
180                                                 Gid:       -1,
181                                         },
182                                         "/etc/.hidden": &safcm.File{
183                                                 OrigGroup: "group",
184                                                 Path:      "/etc/.hidden",
185                                                 Mode:      0100 | fs.ModeSetuid | fs.ModeSetgid | fs.ModeSticky,
186                                                 Uid:       -1,
187                                                 Gid:       -1,
188                                                 Data:      []byte("..."),
189                                         },
190                                         "/etc/motd": &safcm.File{
191                                                 OrigGroup: "group",
192                                                 Path:      "/etc/motd",
193                                                 Mode:      0644,
194                                                 Uid:       -1,
195                                                 Gid:       -1,
196                                                 Data:      []byte("Welcome to Host ONE\n\n\n\n"),
197                                         },
198                                         "/etc/rc.local": &safcm.File{
199                                                 OrigGroup: "group",
200                                                 Path:      "/etc/rc.local",
201                                                 Mode:      0700,
202                                                 Uid:       -1,
203                                                 Gid:       -1,
204                                                 Data:      []byte("#!/bin/sh\n"),
205                                                 TriggerCommands: []string{
206                                                         "/etc/rc.local",
207                                                 },
208                                         },
209                                         "/etc/resolv.conf": &safcm.File{
210                                                 OrigGroup: "group",
211                                                 Path:      "/etc/resolv.conf",
212                                                 Mode:      0641,
213                                                 User:      "user",
214                                                 Uid:       -1,
215                                                 Group:     "group",
216                                                 Gid:       -1,
217                                                 Data:      []byte("nameserver ::1\n"),
218                                                 TriggerCommands: []string{
219                                                         "echo resolv.conf updated",
220                                                 },
221                                         },
222                                         "/etc/test": &safcm.File{
223                                                 OrigGroup: "group",
224                                                 Path:      "/etc/test",
225                                                 Mode:      os.ModeSymlink | 0777,
226                                                 Uid:       -1,
227                                                 Gid:       -1,
228                                                 Data:      []byte("doesnt-exist"),
229                                         },
230                                 },
231                                 Packages: []string{
232                                         "unbound",
233                                         "unbound-anchor",
234                                 },
235                                 Services: []string{
236                                         "unbound",
237                                 },
238                                 Commands: []string{
239                                         "echo command one",
240                                         "echo -n command two",
241                                 },
242                         },
243                         nil,
244                         nil,
245                 },
246
247                 {
248                         "conflict: file",
249                         "project-conflict-file",
250                         "host1.example.org",
251                         nil,
252                         safcm.LogDebug3,
253                         safcm.MsgSyncReq{},
254                         []string{
255                                 "host1.example.org: <nil> 3 host groups: all dns host1.example.org",
256                                 "host1.example.org: <nil> 3 host group priorities (desc. order): host1.example.org",
257                         },
258                         fmt.Errorf("groups dns and all both provide file \"/etc/resolv.conf\"\nUse 'group_order' in config.yaml to declare preference"),
259                 },
260                 {
261                         "conflict: file from detected group",
262                         "project-conflict-file",
263                         "host2.example.org",
264                         []string{
265                                 "detected_other",
266                         },
267                         safcm.LogDebug3,
268                         safcm.MsgSyncReq{},
269                         []string{
270                                 "host2.example.org: <nil> 3 host groups: all detected_other host2.example.org other",
271                                 "host2.example.org: <nil> 3 host group priorities (desc. order): host2.example.org",
272                         },
273                         fmt.Errorf("groups other and all both provide file \"/etc/resolv.conf\"\nUse 'group_order' in config.yaml to declare preference"),
274                 },
275
276                 {
277                         "conflict: dir",
278                         "project-conflict-dir",
279                         "host1.example.org",
280                         nil,
281                         safcm.LogDebug3,
282                         safcm.MsgSyncReq{},
283                         []string{
284                                 "host1.example.org: <nil> 3 host groups: all dns host1.example.org",
285                                 "host1.example.org: <nil> 3 host group priorities (desc. order): host1.example.org",
286                         },
287                         fmt.Errorf("groups dns and all both provide file \"/etc\"\nUse 'group_order' in config.yaml to declare preference"),
288                 },
289                 {
290                         "conflict: dir from detected group",
291                         "project-conflict-dir",
292                         "host2.example.org",
293                         []string{
294                                 "detected_other",
295                         },
296                         safcm.LogDebug3,
297                         safcm.MsgSyncReq{},
298                         []string{
299                                 "host2.example.org: <nil> 3 host groups: all detected_other host2.example.org other",
300                                 "host2.example.org: <nil> 3 host group priorities (desc. order): host2.example.org",
301                         },
302                         fmt.Errorf("groups other and all both provide file \"/etc\"\nUse 'group_order' in config.yaml to declare preference"),
303                 },
304
305                 {
306                         "group: cycle",
307                         "project-group-cycle",
308                         "host1.example.org",
309                         nil,
310                         safcm.LogDebug3,
311                         safcm.MsgSyncReq{},
312                         nil,
313                         fmt.Errorf("groups.yaml: cycle while expanding group \"group-b\""),
314                 },
315
316                 {
317                         "group_order",
318                         "project-group_order",
319                         "host1.example.org",
320                         nil,
321                         safcm.LogDebug3,
322                         safcm.MsgSyncReq{
323                                 Groups: []string{"all", "group-b", "group-a", "host1.example.org"},
324                                 Files: map[string]*safcm.File{
325                                         "/": {
326                                                 Path:      "/",
327                                                 Mode:      fs.ModeDir | 0755,
328                                                 Uid:       -1,
329                                                 Gid:       -1,
330                                                 OrigGroup: "host1.example.org",
331                                         },
332                                         "/etc": {
333                                                 Path:      "/etc",
334                                                 Mode:      fs.ModeDir | 0755,
335                                                 Uid:       -1,
336                                                 Gid:       -1,
337                                                 OrigGroup: "host1.example.org",
338                                         },
339                                         "/etc/dir-to-file": {
340                                                 Path:      "/etc/dir-to-file",
341                                                 Mode:      0644,
342                                                 Uid:       -1,
343                                                 Gid:       -1,
344                                                 Data:      []byte("dir-to-file: from group-a\n"),
345                                                 OrigGroup: "group-a",
346                                         },
347                                         "/etc/dir-to-filex": {
348                                                 OrigGroup: "group-b",
349                                                 Path:      "/etc/dir-to-filex",
350                                                 Mode:      0644,
351                                                 Uid:       -1,
352                                                 Gid:       -1,
353                                                 Data:      []byte("dir-to-filex\n"),
354                                         },
355                                         "/etc/dir-to-link": {
356                                                 Path:      "/etc/dir-to-link",
357                                                 Mode:      fs.ModeSymlink | 0777,
358                                                 Uid:       -1,
359                                                 Gid:       -1,
360                                                 Data:      []byte("target"),
361                                                 OrigGroup: "group-a",
362                                         },
363                                         "/etc/dir-to-linkx": {
364                                                 OrigGroup: "group-b",
365                                                 Path:      "/etc/dir-to-linkx",
366                                                 Mode:      0644,
367                                                 Uid:       -1,
368                                                 Gid:       -1,
369                                                 Data:      []byte("dir-to-linkx\n"),
370                                         },
371                                         "/etc/file-to-dir": {
372                                                 Path:      "/etc/file-to-dir",
373                                                 Mode:      fs.ModeDir | 0755,
374                                                 Uid:       -1,
375                                                 Gid:       -1,
376                                                 OrigGroup: "group-a",
377                                         },
378                                         "/etc/file-to-dir/file": {
379                                                 Path:      "/etc/file-to-dir/file",
380                                                 Mode:      0644,
381                                                 Uid:       -1,
382                                                 Gid:       -1,
383                                                 Data:      []byte("file: from group-a\n"),
384                                                 OrigGroup: "group-a",
385                                         },
386                                         "/etc/file-to-dir/dir": {
387                                                 Path:      "/etc/file-to-dir/dir",
388                                                 Mode:      fs.ModeDir | 0755,
389                                                 Uid:       -1,
390                                                 Gid:       -1,
391                                                 OrigGroup: "group-a",
392                                         },
393                                         "/etc/file-to-dir/dir/file2": {
394                                                 Path:      "/etc/file-to-dir/dir/file2",
395                                                 Mode:      0644,
396                                                 Uid:       -1,
397                                                 Gid:       -1,
398                                                 Data:      []byte("file2: from group-a\n"),
399                                                 OrigGroup: "group-a",
400                                         },
401                                         "/etc/motd": {
402                                                 Path:      "/etc/motd",
403                                                 Mode:      0644,
404                                                 Uid:       -1,
405                                                 Gid:       -1,
406                                                 Data:      []byte("motd: from host1\n"),
407                                                 OrigGroup: "host1.example.org",
408                                         },
409                                 },
410                         },
411                         []string{
412                                 "host1.example.org: <nil> 3 host groups: all group-a group-b host1.example.org",
413                                 "host1.example.org: <nil> 3 host group priorities (desc. order): host1.example.org group-a group-b all",
414                                 `host1.example.org: <nil> 4 files: "/etc": group group-a overwrites triggers from group group-b`,
415                                 `host1.example.org: <nil> 4 files: "/etc": group host1.example.org overwrites triggers from group group-a`,
416                         },
417                         nil,
418                 },
419         }
420
421         for _, tc := range tests {
422                 t.Run(tc.name, func(t *testing.T) {
423                 err = os.Chdir(filepath.Join(cwd, "testdata", tc.project))
424                 if err != nil {
425                         t.Fatal(err)
426                 }
427
428                 // `safcm fixperms` in case user has strict umask
429                 log.SetOutput(io.Discard)
430                 err := MainFixperms()
431                 if err != nil {
432                         t.Fatal(err)
433                 }
434                 log.SetOutput(os.Stderr)
435
436                 cfg, allHosts, allGroups, err := LoadBaseFiles()
437                 if err != nil {
438                         t.Fatal(err)
439                 }
440                 cfg.LogLevel = tc.level
441
442                 var events []string
443                 ch := make(chan Event)
444                 done := make(chan struct{})
445                 go func() {
446                         for {
447                                 x, ok := <-ch
448                                 if !ok {
449                                         break
450                                 }
451                                 if x.ConnEvent.Type != 0 {
452                                         panic("unexpected ConnEvent")
453                                 }
454                                 events = append(events,
455                                         fmt.Sprintf("%s: %v %d %s",
456                                                 x.Host.Name,
457                                                 x.Error, x.Log.Level,
458                                                 x.Log.Text))
459                         }
460                         done <- struct{}{}
461                 }()
462
463                 s := &Sync{
464                         host:      allHosts.Map[tc.host],
465                         config:    cfg,
466                         allHosts:  allHosts,
467                         allGroups: allGroups,
468                         events:    ch,
469                 }
470
471                 res, err := s.hostSyncReq(tc.detected)
472                 if !reflect.DeepEqual(tc.exp, res) {
473                         t.Errorf("res: %s",
474                                 cmp.Diff(tc.exp, res))
475                 }
476                 // Ugly but the simplest way to compare errors (including nil)
477                 if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", tc.expErr) {
478                         t.Errorf("err = %#v, want %#v",
479                                 err, tc.expErr)
480                 }
481
482                 close(ch)
483                 <-done
484                 if !reflect.DeepEqual(tc.expEvents, events) {
485                         t.Errorf("events: %s",
486                                 cmp.Diff(tc.expEvents, events))
487                 }
488                 })
489         }
490 }