// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2021-2024 Simon Ruderich package main import ( "fmt" "io" "io/fs" "log" "os" "path/filepath" "testing" "ruderich.org/simon/safcm" "ruderich.org/simon/safcm/testutil" ) func TestHostSyncReq(t *testing.T) { cwd, err := os.Getwd() if err != nil { t.Fatal(err) } defer os.Chdir(cwd) //nolint:errcheck tests := []struct { name string project string host string detected []string exp safcm.MsgSyncReq expEvents []string expErr error }{ // NOTE: Also update MsgSyncReq in safcm-remote test cases // changing the MsgSyncReq struct! { "project: host1", "project", "host1.example.org", nil, safcm.MsgSyncReq{ Groups: []string{ "all", "group", "group3", "remove", "host1.example.org", }, Files: map[string]*safcm.File{ "/": { OrigGroup: "group", Path: "/", Mode: fs.ModeDir | 0755 | fs.ModeSetgid, Uid: -1, Gid: -1, TriggerCommands: []string{ "touch /.update", }, }, "/etc": { OrigGroup: "group", Path: "/etc", Mode: fs.ModeDir | 0755, Uid: -1, Gid: -1, }, "/etc/.hidden": { OrigGroup: "group", Path: "/etc/.hidden", Mode: 0100 | fs.ModeSetuid | fs.ModeSetgid | fs.ModeSticky, Uid: -1, Gid: -1, Data: []byte("..."), }, "/etc/motd": { OrigGroup: "group", Path: "/etc/motd", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("Welcome to Host ONE\n\n\n\n\n\nall\n\n\nhost1.example.org\n\n\n\n"), }, "/etc/rc.local": { OrigGroup: "group", Path: "/etc/rc.local", Mode: 0700, Uid: -1, Gid: -1, Data: []byte("#!/bin/sh\n"), TriggerCommands: []string{ "/etc/rc.local", }, }, "/etc/resolv.conf": { OrigGroup: "group", Path: "/etc/resolv.conf", Mode: 0641, User: "user", Uid: -1, Group: "group", Gid: -1, Data: []byte("nameserver ::1\n"), TriggerCommands: []string{ "echo resolv.conf updated", }, }, "/etc/test": { OrigGroup: "group", Path: "/etc/test", Mode: os.ModeSymlink | 0777, Uid: -1, Gid: -1, Data: []byte("doesnt-exist"), }, }, Packages: []string{ "unbound", "unbound-anchor", }, Services: []string{ "unbound", }, Commands: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo command one", }, { OrigGroup: "group", Cmd: "echo -n command two", }, }, }, []string{ "3 false host groups: all group group3 host1.example.org remove", "3 false host group priorities (descending): host1.example.org", }, nil, }, { "conflict: file", "project-conflict-file", "host1.example.org", nil, safcm.MsgSyncReq{}, []string{ "3 false host groups: all dns host1.example.org", "3 false host group priorities (descending): host1.example.org", }, fmt.Errorf("groups dns and all both provide \"/etc/resolv.conf\"\nUse 'group_priority' in config.yaml to declare preference"), }, { "conflict: file from detected group", "project-conflict-file", "host2.example.org", []string{ "detected_other", }, safcm.MsgSyncReq{}, []string{ "3 false host groups: all detected_other host2.example.org other", "3 false host group priorities (descending): host2.example.org", }, fmt.Errorf("groups other and all both provide \"/etc/resolv.conf\"\nUse 'group_priority' in config.yaml to declare preference"), }, { "conflict: dir", "project-conflict-dir", "host1.example.org", nil, safcm.MsgSyncReq{}, []string{ "3 false host groups: all dns host1.example.org", "3 false host group priorities (descending): host1.example.org", }, fmt.Errorf("groups dns and all both provide \"/etc\"\nUse 'group_priority' in config.yaml to declare preference"), }, { "conflict: dir from detected group", "project-conflict-dir", "host2.example.org", []string{ "detected_other", }, safcm.MsgSyncReq{}, []string{ "3 false host groups: all detected_other host2.example.org other", "3 false host group priorities (descending): host2.example.org", }, fmt.Errorf("groups other and all both provide \"/etc\"\nUse 'group_priority' in config.yaml to declare preference"), }, { "group: cycle", "project-group-cycle", "host1.example.org", nil, safcm.MsgSyncReq{}, nil, fmt.Errorf("groups.yaml: cycle while expanding group \"group-b\""), }, { "group_priority", "project-group_priority", "host1.example.org", nil, safcm.MsgSyncReq{ Groups: []string{"all", "group-b", "group-a", "host1.example.org"}, Files: map[string]*safcm.File{ "/": { OrigGroup: "host1.example.org", Path: "/", Mode: fs.ModeDir | 0755, Uid: -1, Gid: -1, }, "/etc": { OrigGroup: "host1.example.org", Path: "/etc", Mode: fs.ModeDir | 0755, Uid: -1, Gid: -1, }, "/etc/dir-to-file": { OrigGroup: "group-a", Path: "/etc/dir-to-file", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("dir-to-file: from group-a\n"), }, "/etc/dir-to-filex": { OrigGroup: "group-b", Path: "/etc/dir-to-filex", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("dir-to-filex\n"), }, "/etc/dir-to-link": { OrigGroup: "group-a", Path: "/etc/dir-to-link", Mode: fs.ModeSymlink | 0777, Uid: -1, Gid: -1, Data: []byte("target"), }, "/etc/dir-to-linkx": { OrigGroup: "group-b", Path: "/etc/dir-to-linkx", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("dir-to-linkx\n"), }, "/etc/file-to-dir": { OrigGroup: "group-a", Path: "/etc/file-to-dir", Mode: fs.ModeDir | 0755, Uid: -1, Gid: -1, }, "/etc/file-to-dir/file": { OrigGroup: "group-a", Path: "/etc/file-to-dir/file", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("file: from group-a\n"), }, "/etc/file-to-dir/dir": { OrigGroup: "group-a", Path: "/etc/file-to-dir/dir", Mode: fs.ModeDir | 0755, Uid: -1, Gid: -1, }, "/etc/file-to-dir/dir/file2": { OrigGroup: "group-a", Path: "/etc/file-to-dir/dir/file2", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("file2: from group-a\n"), }, "/etc/motd": { OrigGroup: "host1.example.org", Path: "/etc/motd", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("motd: from host1\n"), }, }, }, []string{ "3 false host groups: all group-a group-b host1.example.org", "3 false host group priorities (descending): host1.example.org group-a group-b all", `4 false files: "/etc": group group-a overwrites triggers from group group-b`, `4 false files: "/etc": group host1.example.org overwrites triggers from group group-a`, }, nil, }, { "group_priority (single group)", "project-group_priority-single", "host1.example.org", nil, safcm.MsgSyncReq{ Groups: []string{"all", "group-b", "group-a", "host1.example.org"}, Files: map[string]*safcm.File{ "/": { OrigGroup: "group-a", Path: "/", Mode: fs.ModeDir | 0755, Uid: -1, Gid: -1, }, "/file.txt": { OrigGroup: "group-a", Path: "/file.txt", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("file.txt: from group-a\n"), }, }, }, []string{ "3 false host groups: all group-a group-b host1.example.org", "3 false host group priorities (descending): host1.example.org group-a", }, nil, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err = os.Chdir(filepath.Join(cwd, "testdata", tc.project)) if err != nil { t.Fatal(err) } // `safcm fixperms` in case user has strict umask log.SetOutput(io.Discard) err := MainFixperms() if err != nil { t.Fatal(err) } log.SetOutput(os.Stderr) cfg, allHosts, allGroups, err := LoadBaseFiles() if err != nil { t.Fatal(err) } var events []string s := &Sync{ host: allHosts.Map[tc.host], config: cfg, allHosts: allHosts, allGroups: allGroups, logFunc: func(level safcm.LogLevel, escaped bool, msg string) { events = append(events, fmt.Sprintf("%d %v %s", level, escaped, msg)) }, } res, err := s.hostSyncReq(tc.detected) testutil.AssertEqual(t, "res", res, tc.exp) testutil.AssertErrorEqual(t, "err", err, tc.expErr) testutil.AssertEqual(t, "events", events, tc.expEvents) }) } }