// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2021-2024 Simon Ruderich package frontend import ( "io/fs" "testing" "ruderich.org/simon/safcm" "ruderich.org/simon/safcm/testutil" ) func TestFormatChanges(t *testing.T) { tests := []struct { name string dryRun bool quiet bool isTTY bool resp safcm.MsgSyncResp exp string }{ // Just a few basic tests and border cases; see the other // tests for more detailed tests of each format function { "no changes", false, false, false, safcm.MsgSyncResp{}, "no changes", }, { "changes", false, false, false, safcm.MsgSyncResp{ FileChanges: []safcm.FileChange{ { Path: "created", Created: true, New: safcm.FileChangeInfo{ Mode: 0644, User: "user", Uid: 1000, Group: "group", Gid: 2000, }, }, }, PackageChanges: []safcm.PackageChange{ { Name: "package-one", }, { Name: "package-two", }, }, ServiceChanges: []safcm.ServiceChange{ { Name: "service-one", Started: true, }, { Name: "service-two", Enabled: true, }, { Name: "service-three", Started: true, Enabled: true, }, }, CommandChanges: []safcm.CommandChange{ { Command: "fake command", Output: "fake output", }, { Command: "fake command with no output", }, }, }, "\nchanged 1 file(s):\n\"created\": created, file, user(1000) group(2000), 0644\n\ninstalled 2 package(s):\n\"package-one\"\n\"package-two\"\n\nmodified 3 service(s):\n\"service-one\": started\n\"service-two\": enabled\n\"service-three\": started, enabled\n\nexecuted 2 command(s):\n\"fake command\":\n > fake output\n > \\ No newline at end of file\n\"fake command with no output\"\n", }, { "command changes only, dry-run", true, false, false, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "fake command", }, { Command: "fake command with no output", }, { Command: "fake command with newline", }, { Command: "fake command with more output", }, { Command: "fake failed command", }, }, }, "\nwill execute 5 command(s): (dry-run)\n\"fake command\"\n\"fake command with no output\"\n\"fake command with newline\"\n\"fake command with more output\"\n\"fake failed command\"\n", }, { "command changes only, quiet & dry-run", true, true, false, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "fake command", }, { Command: "fake command with no output", }, { Command: "fake command with newline", }, { Command: "fake command with more output", }, { Command: "fake failed command", }, }, }, "will execute 5 command(s) (dry-run)\n", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c := Changes{ DryRun: tc.dryRun, Quiet: tc.quiet, IsTTY: tc.isTTY, } res := c.FormatChanges(tc.resp) testutil.AssertEqual(t, "res", res, tc.exp) }) } } func TestFormatFileChanges(t *testing.T) { tests := []struct { name string dryRun bool isTTY bool changes []safcm.FileChange exp string }{ { "regular", false, 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 `, }, { "regular (tty)", false, true, []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):\n\x1b[36m\"created: file\"\x1b[0m: \x1b[32mcreated\x1b[0m, file, user(1000) group(2000), 0644\n\x1b[36m\"created: link\"\x1b[0m: \x1b[32mcreated\x1b[0m, symlink, user(1000) group(2000), 0777\n\x1b[36m\"type change: file -> dir\"\x1b[0m: file -> dir\n @@ -1,2 +1 @@\n\x1b[31m -content\x1b[0m\n \n\x1b[36m\"user change\"\x1b[0m: user(1000) group(2000) -> user2(1001) group(2000)\n\x1b[36m\"group change\"\x1b[0m: user(1000) group(2000) -> user(1000) group2(2001)\n\x1b[36m\"mode change\"\x1b[0m: 0755 -> 0750\n\x1b[36m\"mode change (setuid)\"\x1b[0m: 0755 -> 04755\n\x1b[36m\"content change\"\x1b[0m:\n @@ -1,2 +1,2 @@\n\x1b[31m -old content\x1b[0m\n\x1b[32m +content\x1b[0m\n \n\x1b[36m\"multiple changes\"\x1b[0m: file -> dir, user(1000) group(2000) -> user2(1001) group2(2001), 0644 -> 0755\n @@ -1,2 +1 @@\n\x1b[31m -content\x1b[0m\n \n", }, { "dry-run", true, false, []safcm.FileChange{ { Path: "file", Created: true, New: safcm.FileChangeInfo{ Mode: 0644, User: "user", Uid: 1000, Group: "group", Gid: 2000, }, }, }, `will change 1 file(s): (dry-run) "file": created, file, user(1000) group(2000), 0644 `, }, { "dry-run (tty)", true, true, []safcm.FileChange{ { Path: "file", Created: true, New: safcm.FileChangeInfo{ Mode: 0644, User: "user", Uid: 1000, Group: "group", Gid: 2000, }, }, }, "will change 1 file(s): (dry-run)\n\x1B[36m\"file\"\x1B[0m: \x1B[32mcreated\x1B[0m, file, user(1000) group(2000), 0644\n", }, { "escaping", false, 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 `, }, { "escaping (tty)", false, true, []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):\n\x1b[36m\"\\x00\"\x1b[0m: \x1b[32mcreated\x1b[0m, invalid type dLDpSc?---------, \\x01(-1) \\x02(-2), 07777\n \\x03\n \\ No newline at end of file\n\x1b[36m\"\\x00\"\x1b[0m: file -> invalid type dLDpSc?---------, \\x01(-1) \\x02(-2) -> \\x03(-3) \\x04(-4), 0 -> 07777\n \\x05\n \\ No newline at end of file\n", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c := Changes{ DryRun: tc.dryRun, IsTTY: tc.isTTY, } res := c.FormatFileChanges(tc.changes) testutil.AssertEqual(t, "res", res, tc.exp) }) } } func TestFormatPackageChanges(t *testing.T) { tests := []struct { name string dryRun bool isTTY bool changes []safcm.PackageChange exp string }{ { "regular", false, false, []safcm.PackageChange{ { Name: "package-one", }, { Name: "package-two", }, }, `installed 2 package(s): "package-one" "package-two" `, }, { "regular (tty)", false, true, []safcm.PackageChange{ { Name: "package-one", }, { Name: "package-two", }, }, "installed 2 package(s):\n\x1b[36m\"package-one\"\x1b[0m\n\x1b[36m\"package-two\"\x1b[0m\n", }, { "dry-run", true, false, []safcm.PackageChange{ { Name: "package-one", }, { Name: "package-two", }, }, `will install 2 package(s): (dry-run) "package-one" "package-two" `, }, { "dry-run (tty)", true, true, []safcm.PackageChange{ { Name: "package-one", }, { Name: "package-two", }, }, "will install 2 package(s): (dry-run)\n\x1b[36m\"package-one\"\x1b[0m\n\x1b[36m\"package-two\"\x1b[0m\n", }, { "escaping", false, false, []safcm.PackageChange{ { Name: "\x00", }, }, `installed 1 package(s): "\x00" `, }, { "escaping (tty)", false, true, []safcm.PackageChange{ { Name: "\x00", }, }, "installed 1 package(s):\n\x1b[36m\"\\x00\"\x1b[0m\n", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c := Changes{ DryRun: tc.dryRun, IsTTY: tc.isTTY, } res := c.FormatPackageChanges(tc.changes) testutil.AssertEqual(t, "res", res, tc.exp) }) } } func TestFormatServiceChanges(t *testing.T) { tests := []struct { name string dryRun bool isTTY bool changes []safcm.ServiceChange exp string }{ { "regular", false, 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 `, }, { "regular (tty)", false, true, []safcm.ServiceChange{ { Name: "service-one", Started: true, }, { Name: "service-two", Enabled: true, }, { Name: "service-three", Started: true, Enabled: true, }, }, "modified 3 service(s):\n\x1b[36m\"service-one\"\x1b[0m: started\n\x1b[36m\"service-two\"\x1b[0m: enabled\n\x1b[36m\"service-three\"\x1b[0m: started, enabled\n", }, { "dry-run", true, false, []safcm.ServiceChange{ { Name: "service-one", Started: true, }, { Name: "service-two", Enabled: true, }, { Name: "service-three", Started: true, Enabled: true, }, }, `will modify 3 service(s): (dry-run) "service-one": started "service-two": enabled "service-three": started, enabled `, }, { "dry-run (tty)", true, true, []safcm.ServiceChange{ { Name: "service-one", Started: true, }, { Name: "service-two", Enabled: true, }, { Name: "service-three", Started: true, Enabled: true, }, }, "will modify 3 service(s): (dry-run)\n\x1b[36m\"service-one\"\x1b[0m: started\n\x1b[36m\"service-two\"\x1b[0m: enabled\n\x1b[36m\"service-three\"\x1b[0m: started, enabled\n", }, { "escaping", false, false, []safcm.ServiceChange{ { Name: "\x00", }, { Name: "\x01", Started: true, Enabled: true, }, }, `modified 2 service(s): "\x00": "\x01": started, enabled `, }, { "escaping (tty)", false, true, []safcm.ServiceChange{ { Name: "\x00", }, { Name: "\x01", Started: true, Enabled: true, }, }, "modified 2 service(s):\n\x1b[36m\"\\x00\"\x1b[0m: \n\x1b[36m\"\\x01\"\x1b[0m: started, enabled\n", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c := Changes{ DryRun: tc.dryRun, IsTTY: tc.isTTY, } res := c.FormatServiceChanges(tc.changes) testutil.AssertEqual(t, "res", res, tc.exp) }) } } func TestFormatCommandChanges(t *testing.T) { tests := []struct { name string dryRun bool quiet bool isTTY bool changes []safcm.CommandChange exp string }{ { "regular", false, false, 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 `, }, { "regular (tty)", false, false, true, []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):\n\x1b[36m\"fake command\"\x1b[0m:\n > fake output\n > \\ No newline at end of file\n\x1b[36m\"fake command with no output\"\x1b[0m\n\x1b[36m\"fake command with newline\"\x1b[0m:\n > fake output\n\x1b[36m\"fake command with more output\"\x1b[0m:\n > fake out\n > fake put\n > fake\n\x1b[36m\"fake failed command\"\x1b[0m, failed: \"fake error\":\n > fake output\n > \\ No newline at end of file\n", }, { "dry-run", true, false, false, []safcm.CommandChange{ { Command: "fake command", }, }, `will execute 1 command(s): (dry-run) "fake command" `, }, { "dry-run (tty)", true, false, true, []safcm.CommandChange{ { Command: "fake command", }, }, "will execute 1 command(s): (dry-run)\n\x1b[36m\"fake command\"\x1b[0m\n", }, { "quiet", false, true, 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), 1 with no output (hidden): "fake command": > fake output > \ No newline at end of file "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 `, }, { "quiet (tty)", false, true, true, []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), 1 with no output (hidden):\n\x1b[36m\"fake command\"\x1b[0m:\n > fake output\n > \\ No newline at end of file\n\x1b[36m\"fake command with newline\"\x1b[0m:\n > fake output\n\x1b[36m\"fake command with more output\"\x1b[0m:\n > fake out\n > fake put\n > fake\n\x1b[36m\"fake failed command\"\x1b[0m, failed: \"fake error\":\n > fake output\n > \\ No newline at end of file\n", }, { "quiet (only quiet commands)", false, true, false, []safcm.CommandChange{ { Command: "fake command with no output", }, { Command: "fake command with no output", }, }, `executed 2 command(s), 2 with no output (hidden) `, }, { "quiet (quiet with errors)", false, true, false, []safcm.CommandChange{ { Command: "fake command with no output but error", Error: "fake error", }, { Command: "fake command with no output", }, }, `executed 2 command(s), 1 with no output (hidden): "fake command with no output but error", failed: "fake error" `, }, { "quiet & dry-run", true, true, false, []safcm.CommandChange{ { Command: "fake command", }, { Command: "fake command with no output", }, { Command: "fake command with newline", }, { Command: "fake command with more output", }, { Command: "fake failed command", }, }, `will execute 5 command(s) (dry-run) `, }, { "escaping", false, false, 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 `, }, { "escaping (tty)", false, false, true, []safcm.CommandChange{ { Command: "\x00", Trigger: "\x01", Output: "\x02", Error: "\x03", }, }, "executed 1 command(s):\n\x1b[36m\"\\x00\"\x1b[0m, trigger for \"\\x01\", failed: \"\\x03\":\n > \x1b[35m\\x02\x1b[0m\n > \\ No newline at end of file\n", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c := Changes{ DryRun: tc.dryRun, Quiet: tc.quiet, IsTTY: tc.isTTY, } res := c.FormatCommandChanges(tc.changes) testutil.AssertEqual(t, "res", res, tc.exp) }) } }