+// Copyright (C) 2021 Simon Ruderich
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "io/fs"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "ruderich.org/simon/safcm"
+ "ruderich.org/simon/safcm/cmd/safcm/config"
+)
+
+func TestFormatFileChanges(t *testing.T) {
+ tests := []struct {
+ name string
+ dryRun bool
+ changes []safcm.FileChange
+ exp string
+ }{
+
+ {
+ "regular",
+ false,
+ []safcm.FileChange{
+ {
+ Path: "created: file",
+ Created: true,
+ New: safcm.FileChangeInfo{
+ Mode: 0644,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ },
+ {
+ Path: "created: link",
+ Created: true,
+ New: safcm.FileChangeInfo{
+ Mode: fs.ModeSymlink | 0777,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ },
+ {
+ Path: "type change: file -> dir",
+ Old: safcm.FileChangeInfo{
+ Mode: 0751,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ New: safcm.FileChangeInfo{
+ Mode: fs.ModeDir | 0751,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ DataDiff: `@@ -1,2 +1 @@
+-content
+
+`,
+ },
+ {
+ Path: "user change",
+ Old: safcm.FileChangeInfo{
+ Mode: 0755,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ New: safcm.FileChangeInfo{
+ Mode: 0755,
+ User: "user2",
+ Uid: 1001,
+ Group: "group",
+ Gid: 2000,
+ },
+ },
+ {
+ Path: "group change",
+ Old: safcm.FileChangeInfo{
+ Mode: 0755,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ New: safcm.FileChangeInfo{
+ Mode: 0755,
+ User: "user",
+ Uid: 1000,
+ Group: "group2",
+ Gid: 2001,
+ },
+ },
+ {
+ Path: "mode change",
+ Old: safcm.FileChangeInfo{
+ Mode: 0755,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ New: safcm.FileChangeInfo{
+ Mode: 0750,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ },
+ {
+ Path: "mode change (setuid)",
+ Old: safcm.FileChangeInfo{
+ Mode: 0755,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ New: safcm.FileChangeInfo{
+ Mode: 0755 | fs.ModeSetuid,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ },
+ {
+ Path: "content change",
+ Old: safcm.FileChangeInfo{
+ Mode: 0644,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ New: safcm.FileChangeInfo{
+ Mode: 0644,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ DataDiff: `@@ -1,2 +1,2 @@
+-old content
++content
+
+`,
+ },
+ {
+ Path: "multiple changes",
+ Old: safcm.FileChangeInfo{
+ Mode: 0644,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ New: safcm.FileChangeInfo{
+ Mode: fs.ModeDir | 0755,
+ User: "user2",
+ Uid: 1001,
+ Group: "group2",
+ Gid: 2001,
+ },
+ DataDiff: `@@ -1,2 +1 @@
+-content
+
+`,
+ },
+ },
+ `changed 9 file(s):
+"created: file": created, file, user(1000) group(2000), 0644
+"created: link": created, symlink, user(1000) group(2000), 0777
+"type change: file -> dir": file -> dir
+ @@ -1,2 +1 @@
+ -content
+
+"user change": user(1000) group(2000) -> user2(1001) group(2000)
+"group change": user(1000) group(2000) -> user(1000) group2(2001)
+"mode change": 0755 -> 0750
+"mode change (setuid)": 0755 -> 04755
+"content change":
+ @@ -1,2 +1,2 @@
+ -old content
+ +content
+
+"multiple changes": file -> dir, user(1000) group(2000) -> user2(1001) group2(2001), 0644 -> 0755
+ @@ -1,2 +1 @@
+ -content
+
+`,
+ },
+
+ {
+ "dry-run",
+ true,
+ []safcm.FileChange{
+ {
+ Path: "file",
+ Created: true,
+ New: safcm.FileChangeInfo{
+ Mode: 0644,
+ User: "user",
+ Uid: 1000,
+ Group: "group",
+ Gid: 2000,
+ },
+ },
+ },
+ `changed 1 file(s): (dry-run)
+"file": created, file, user(1000) group(2000), 0644
+`,
+ },
+
+ {
+ "escaping",
+ false,
+ []safcm.FileChange{
+ {
+ Path: "\x00",
+ Created: true,
+ New: safcm.FileChangeInfo{
+ Mode: 0xFFFFFFFF,
+ User: "\x01",
+ Uid: -1,
+ Group: "\x02",
+ Gid: -2,
+ },
+ DataDiff: "\x03",
+ },
+ {
+ Path: "\x00",
+ Old: safcm.FileChangeInfo{
+ Mode: 0x00000000,
+ User: "\x01",
+ Uid: -1,
+ Group: "\x02",
+ Gid: -2,
+ },
+ New: safcm.FileChangeInfo{
+ Mode: 0xFFFFFFFF,
+ User: "\x03",
+ Uid: -3,
+ Group: "\x04",
+ Gid: -4,
+ },
+ DataDiff: "\x05",
+ },
+ },
+ `changed 2 file(s):
+"\x00": created, invalid type dLDpSc?---------, \x01(-1) \x02(-2), 07777
+ \x03
+ \ No newline at end of file
+"\x00": file -> invalid type dLDpSc?---------, \x01(-1) \x02(-2) -> \x03(-3) \x04(-4), 0 -> 07777
+ \x05
+ \ No newline at end of file
+`,
+ },
+ }
+
+ for _, tc := range tests {
+ s := &Sync{
+ config: &config.Config{
+ DryRun: tc.dryRun,
+ },
+ }
+
+ res := s.formatFileChanges(tc.changes)
+ if tc.exp != res {
+ t.Errorf("%s: res: %s", tc.name,
+ cmp.Diff(tc.exp, res))
+ }
+ }
+}
+
+func TestFormatPackageChanges(t *testing.T) {
+ tests := []struct {
+ name string
+ dryRun bool
+ changes []safcm.PackageChange
+ exp string
+ }{
+
+ {
+ "regular",
+ false,
+ []safcm.PackageChange{
+ {
+ Name: "package-one",
+ },
+ {
+ Name: "package-two",
+ },
+ },
+ `installed 2 package(s):
+"package-one"
+"package-two"
+`,
+ },
+
+ {
+ "dry-run",
+ true,
+ []safcm.PackageChange{
+ {
+ Name: "package-one",
+ },
+ {
+ Name: "package-two",
+ },
+ },
+ `installed 2 package(s): (dry-run)
+"package-one"
+"package-two"
+`,
+ },
+
+ {
+ "escaping",
+ false,
+ []safcm.PackageChange{
+ {
+ Name: "\x00",
+ },
+ },
+ `installed 1 package(s):
+"\x00"
+`,
+ },
+ }
+
+ for _, tc := range tests {
+ s := &Sync{
+ config: &config.Config{
+ DryRun: tc.dryRun,
+ },
+ }
+
+ res := s.formatPackageChanges(tc.changes)
+ if tc.exp != res {
+ t.Errorf("%s: res: %s", tc.name,
+ cmp.Diff(tc.exp, res))
+ }
+ }
+}
+
+func TestFormatServiceChanges(t *testing.T) {
+ tests := []struct {
+ name string
+ dryRun bool
+ changes []safcm.ServiceChange
+ exp string
+ }{
+
+ {
+ "regular",
+ false,
+ []safcm.ServiceChange{
+ {
+ Name: "service-one",
+ Started: true,
+ },
+ {
+ Name: "service-two",
+ Enabled: true,
+ },
+ {
+ Name: "service-three",
+ Started: true,
+ Enabled: true,
+ },
+ },
+ `modified 3 service(s):
+"service-one": started
+"service-two": enabled
+"service-three": started, enabled
+`,
+ },
+
+ {
+ "dry-run",
+ true,
+ []safcm.ServiceChange{
+ {
+ Name: "service-one",
+ Started: true,
+ },
+ {
+ Name: "service-two",
+ Enabled: true,
+ },
+ {
+ Name: "service-three",
+ Started: true,
+ Enabled: true,
+ },
+ },
+ `modified 3 service(s): (dry-run)
+"service-one": started
+"service-two": enabled
+"service-three": started, enabled
+`,
+ },
+
+ {
+ "escaping",
+ false,
+ []safcm.ServiceChange{
+ {
+ Name: "\x00",
+ },
+ {
+ Name: "\x01",
+ Started: true,
+ Enabled: true,
+ },
+ },
+ `modified 2 service(s):
+"\x00":
+"\x01": started, enabled
+`,
+ },
+ }
+
+ for _, tc := range tests {
+ s := &Sync{
+ config: &config.Config{
+ DryRun: tc.dryRun,
+ },
+ }
+
+ res := s.formatServiceChanges(tc.changes)
+ if tc.exp != res {
+ t.Errorf("%s: res: %s", tc.name,
+ cmp.Diff(tc.exp, res))
+ }
+ }
+}
+
+func TestFormatCommandChanges(t *testing.T) {
+ tests := []struct {
+ name string
+ dryRun bool
+ changes []safcm.CommandChange
+ exp string
+ }{
+
+ {
+ "regular",
+ false,
+ []safcm.CommandChange{
+ {
+ Command: "fake command",
+ Output: "fake output",
+ },
+ {
+ Command: "fake command with no output",
+ },
+ {
+ Command: "fake command with newline",
+ Output: "fake output\n",
+ },
+ {
+ Command: "fake command with more output",
+ Output: "fake out\nfake put\nfake\n",
+ },
+ {
+ Command: "fake failed command",
+ Output: "fake output",
+ Error: "fake error",
+ },
+ },
+ `executed 5 command(s):
+"fake command":
+ > fake output
+ > \ No newline at end of file
+"fake command with no output"
+"fake command with newline":
+ > fake output
+"fake command with more output":
+ > fake out
+ > fake put
+ > fake
+"fake failed command", failed: "fake error":
+ > fake output
+ > \ No newline at end of file
+`,
+ },
+
+ {
+ "dry-run",
+ true,
+ []safcm.CommandChange{
+ {
+ Command: "fake command",
+ Output: "fake output",
+ },
+ },
+ `executed 1 command(s): (dry-run)
+"fake command":
+ > fake output
+ > \ No newline at end of file
+`,
+ },
+
+ {
+ "escaping",
+ false,
+ []safcm.CommandChange{
+ {
+ Command: "\x00",
+ Trigger: "\x01",
+ Output: "\x02",
+ Error: "\x03",
+ },
+ },
+ `executed 1 command(s):
+"\x00", trigger for "\x01", failed: "\x03":
+ > \x02
+ > \ No newline at end of file
+`,
+ },
+ }
+
+ for _, tc := range tests {
+ s := &Sync{
+ config: &config.Config{
+ DryRun: tc.dryRun,
+ },
+ }
+
+ res := s.formatCommandChanges(tc.changes)
+ if tc.exp != res {
+ t.Errorf("%s: res: %s", tc.name,
+ cmp.Diff(tc.exp, res))
+ }
+ }
+}