diff --git a/pom.xml b/pom.xml index b3727ce..7b0a8c1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ cf.maybelambda fedora-setup-script - 3.0.4 + 3.0.5 21 @@ -51,7 +51,7 @@ true - cf.maybelambda.fedora.PostInstallUpdater + cf.maybelambda.fedora.Main diff --git a/src/main/java/cf/maybelambda/fedora/Main.java b/src/main/java/cf/maybelambda/fedora/Main.java new file mode 100644 index 0000000..6c9491b --- /dev/null +++ b/src/main/java/cf/maybelambda/fedora/Main.java @@ -0,0 +1,99 @@ +package cf.maybelambda.fedora; + +import static cf.maybelambda.fedora.ConsoleIOHelper.GREEN; +import static cf.maybelambda.fedora.ConsoleIOHelper.RED; +import static cf.maybelambda.fedora.ConsoleIOHelper.color; +import static cf.maybelambda.fedora.ConsoleIOHelper.confirm; +import static cf.maybelambda.fedora.ConsoleIOHelper.promptForExclusions; +import static java.util.Arrays.asList; + +import java.util.List; +import java.util.Scanner; + +public class Main { + static List CMD_RPM_IMPORT = asList("sudo", "rpm", "--import"); + static List CMD_DNF_INST_REPOS = asList("sudo", "dnf", "install", "-y"); + static List CMD_DNF_INST = asList("sudo", "dnf", "--refresh", "install", "-y"); + static List CMD_DNF_RM = asList("sudo", "dnf", "remove", "-y", "--noautoremove"); + static List CMD_DNF_MARK = asList("sudo", "dnf", "mark", "user", "flatpak"); // single arg appended to cmd + static List CMD_DNF_AUTORM = asList("sudo", "dnf", "autoremove", "-y"); + static List CMD_FLATPAK_REMOTE_ADD = asList("sudo", "flatpak", "remote-add", "--if-not-exists"); + static List CMD_FLATPAK_INST = asList("flatpak", "install", "-y"); + static List CMD_GETENT = asList("getent", "group"); + static List CMD_ADD_GROUP = asList("sudo", "groupadd"); + static List CMD_ADD_USER_TO_GROUP = asList("sudo", "usermod", "-aG"); + static List CMD_SYSTEMCTL_ENABLE = asList("sudo", "systemctl", "enable", "--now", "cockpit.socket"); // single arg appended to cmd + + public static void main(String[] args) { + run(args, new PostInstallUpdater()); + } + + static void run(String[] args, PostInstallUpdater updater) { + if (asList(args).contains("-h") || asList(args).contains("--help")) { + ConsoleIOHelper.printHelp(); + return; + } + + System.out.println(color("]|I{•------» Fedora Setup Script «------•}I|[\n", GREEN)); + + List dnfInstallPackages = ConfigManager.getDnfInstallPackages(); + List dnfRemovePackages = ConfigManager.getDnfRemovePackages(); + List flatpakInstallPackages = ConfigManager.getFlatpakInstallPackages(); + Scanner scanner = new Scanner(System.in); + + updater.setDryRun(asList(args).contains("--dry-run")); + if (updater.isDryRun()) { + System.out.println(color("---[Dry Run Mode] Shell Commands will not be executed.---\n", RED)); + } + + if (confirm(scanner, "Install RPMFusion repos?")) { + for (String key : ConfigManager.getRPMFusionGpgKeys()) { + updater.runCommand(CMD_RPM_IMPORT, asList(key)); + } + + List repos = ConfigManager.getRPMFusionRepos(); + updater.runCommand(CMD_DNF_INST_REPOS, repos); + } + + if (confirm(scanner, "Install additional packages with DNF?")) { + List filtered = promptForExclusions(dnfInstallPackages, scanner); + updater.runCommand(CMD_DNF_INST, filtered); + } + + if (confirm(scanner, "Remove all DNF packages marked for removal?")) { + List filtered = promptForExclusions(dnfRemovePackages, scanner); + updater.runCommand(CMD_DNF_RM, filtered); + updater.runCommand(CMD_DNF_MARK, asList()); + updater.runCommand(CMD_DNF_AUTORM, asList()); + } + + if (confirm(scanner, "Install Flatpak apps?")) { + String name = ConfigManager.getFlatpakRemoteName(); + String url = ConfigManager.getFlatpakRemoteUrl(); + updater.runCommand(CMD_FLATPAK_REMOTE_ADD, asList(name, url)); + + List filtered = promptForExclusions(flatpakInstallPackages, scanner); + filtered.addFirst(name); + updater.runCommand(CMD_FLATPAK_INST, filtered); + } + + if (confirm(scanner, "Ensure admin groups exist and add current user to them?")) { + String user = System.getProperty("user.name"); + for (String group : ConfigManager.getAdminGroups()) { + int exit = updater.runCommand(CMD_GETENT, asList(group)); + boolean groupExists = (exit == 0); + if (!groupExists) { + System.out.println("Group '" + group + "' does not exist. Creating..."); + updater.runCommand(CMD_ADD_GROUP, asList(group)); + } + updater.runCommand(CMD_ADD_USER_TO_GROUP, asList(group, user)); + } + } + + if (confirm(scanner, "Enable and start cockpit.socket service?")) { + updater.runCommand(CMD_SYSTEMCTL_ENABLE, asList()); + } + + System.out.println(color("\n.o0×X×0o. All actions completed. Goodbye. .o0×X×0o.", GREEN)); + } +} diff --git a/src/main/java/cf/maybelambda/fedora/PostInstallUpdater.java b/src/main/java/cf/maybelambda/fedora/PostInstallUpdater.java index 5712f18..30a7de8 100644 --- a/src/main/java/cf/maybelambda/fedora/PostInstallUpdater.java +++ b/src/main/java/cf/maybelambda/fedora/PostInstallUpdater.java @@ -1,135 +1,33 @@ package cf.maybelambda.fedora; +import static cf.maybelambda.fedora.ConsoleIOHelper.BLUE; +import static cf.maybelambda.fedora.ConsoleIOHelper.YELLOW; +import static cf.maybelambda.fedora.ConsoleIOHelper.color; +import static java.util.stream.Stream.concat; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; -import java.util.Scanner; - -import static cf.maybelambda.fedora.ConsoleIOHelper.BLUE; -import static cf.maybelambda.fedora.ConsoleIOHelper.GREEN; -import static cf.maybelambda.fedora.ConsoleIOHelper.RED; -import static cf.maybelambda.fedora.ConsoleIOHelper.YELLOW; -import static cf.maybelambda.fedora.ConsoleIOHelper.color; -import static cf.maybelambda.fedora.ConsoleIOHelper.confirm; -import static cf.maybelambda.fedora.ConsoleIOHelper.promptForExclusions; public class PostInstallUpdater { - private static boolean dryRun; - - public static void main(String[] args) { - if (Arrays.asList(args).contains("-h") || Arrays.asList(args).contains("--help")) { - ConsoleIOHelper.printHelp(); - return; - } - - List dnfInstallPackages = ConfigManager.getDnfInstallPackages(); - List dnfRemovePackages = ConfigManager.getDnfRemovePackages(); - List flatpakInstallPackages = ConfigManager.getFlatpakInstallPackages(); - Scanner scanner = new Scanner(System.in); - - System.out.println(color("]|I{•------» Fedora Setup Script «------•}I|[\n", GREEN)); - setDryRun(Arrays.asList(args).contains("--dry-run")); - if (isDryRun()) { - System.out.println(color("---[Dry Run Mode] Shell Commands will not be executed.---\n", RED)); - } - - if (confirm(scanner, "Install RPMFusion repos?")) { - for (String key : ConfigManager.getRPMFusionGpgKeys()) { - runCommand(new String[]{"sudo", "rpm", "--import", key}); - } - - List repos = ConfigManager.getRPMFusionRepos(); - String[] cmd = new String[repos.size() + 4]; - cmd[0] = "sudo"; - cmd[1] = "dnf"; - cmd[2] = "install"; - cmd[3] = "-y"; - for (int i = 0; i < repos.size(); i++) { - cmd[4 + i] = repos.get(i); - } - runCommand(cmd); - } - - if (confirm(scanner, "Install additional packages with DNF?")) { - List filtered = promptForExclusions(dnfInstallPackages, scanner); - String[] cmd = new String[filtered.size() + 5]; - cmd[0] = "sudo"; - cmd[1] = "dnf"; - cmd[2] = "--refresh"; - cmd[3] = "install"; - cmd[4] = "-y"; - for (int i = 0; i < filtered.size(); i++) { - cmd[5 + i] = filtered.get(i); - } - runCommand(cmd); - } - - if (confirm(scanner, "Remove all DNF packages marked for removal?")) { - List filtered = promptForExclusions(dnfRemovePackages, scanner); - String[] cmd = new String[filtered.size() + 4]; - cmd[0] = "sudo"; - cmd[1] = "dnf"; - cmd[2] = "remove"; - cmd[3] = "-y"; - for (int i = 0; i < filtered.size(); i++) { - cmd[4 + i] = filtered.get(i); - } - runCommand(cmd); - runCommand(new String[]{"sudo", "dnf", "autoremove", "-y"}); - } - - if (confirm(scanner, "Install Flatpak apps?")) { - String name = ConfigManager.getFlatpakRemoteName(); - String url = ConfigManager.getFlatpakRemoteUrl(); - runCommand(new String[]{"sudo", "flatpak", "remote-add", "--if-not-exists", name, url}); - List filtered = promptForExclusions(flatpakInstallPackages, scanner); - String[] cmd = new String[filtered.size() + 4]; - cmd[0] = "flatpak"; - cmd[1] = "install"; - cmd[2] = "-y"; - cmd[3] = name; - for (int i = 0; i < filtered.size(); i++) { - cmd[4 + i] = filtered.get(i); - } - runCommand(cmd); - } - - if (confirm(scanner, "Ensure admin groups exist and add current user to them?")) { - String user = System.getProperty("user.name"); - for (String group : ConfigManager.getAdminGroups()) { - int exit = runCommand(new String[]{"getent", "group", group}); - boolean groupExists = (exit == 0); - if (!groupExists) { - System.out.println("Group '" + group + "' does not exist. Creating..."); - runCommand(new String[]{"sudo", "groupadd", group}); - } - runCommand(new String[]{"sudo", "usermod", "-aG", group, user}); - } - } - - if (confirm(scanner, "Enable and start cockpit.socket service?")) { - runCommand(new String[]{"sudo", "systemctl", "enable", "--now", "cockpit.socket"}); - } - - System.out.println(color("\n.o0×X×0o. All actions completed. Goodbye. .o0×X×0o.", GREEN)); - } + private boolean dryRun; - static boolean isDryRun() { + boolean isDryRun() { return dryRun; } - static void setDryRun(boolean dryRun) { - PostInstallUpdater.dryRun = dryRun; + void setDryRun(boolean dryRun) { + this.dryRun = dryRun; } - static ProcessBuilder createProcessBuilder(String[] cmd) { + ProcessBuilder createProcessBuilder(String[] cmd) { return new ProcessBuilder(cmd); } - static int runCommand(String[] command) { + int runCommand(List baseCmd, List args) { + String[] command = concat(baseCmd.stream(), args.stream()).toArray(String[]::new); System.out.println("Executing shell command: " + color(String.join(" ", command), BLUE)); if (isDryRun()) { System.out.println(color("Dry-run: command not executed.", YELLOW)); diff --git a/src/main/resources/dnf-remove.cf b/src/main/resources/dnf-remove.cf index d1f8c26..6d86291 100644 --- a/src/main/resources/dnf-remove.cf +++ b/src/main/resources/dnf-remove.cf @@ -13,6 +13,7 @@ krdc krfb ktnef neochat +plasma-discover plasma-welcome skanpage spectacle diff --git a/src/test/java/cf/maybelambda/fedora/MainTests.java b/src/test/java/cf/maybelambda/fedora/MainTests.java new file mode 100644 index 0000000..08e7f18 --- /dev/null +++ b/src/test/java/cf/maybelambda/fedora/MainTests.java @@ -0,0 +1,121 @@ +package cf.maybelambda.fedora; + +import static cf.maybelambda.fedora.ConfigManager.getAdminGroups; +import static cf.maybelambda.fedora.ConfigManager.getDnfInstallPackages; +import static cf.maybelambda.fedora.ConfigManager.getDnfRemovePackages; +import static cf.maybelambda.fedora.ConfigManager.getFlatpakInstallPackages; +import static cf.maybelambda.fedora.ConfigManager.getFlatpakRemoteName; +import static cf.maybelambda.fedora.ConfigManager.getFlatpakRemoteUrl; +import static cf.maybelambda.fedora.ConfigManager.getRPMFusionGpgKeys; +import static cf.maybelambda.fedora.ConfigManager.getRPMFusionRepos; +import static cf.maybelambda.fedora.Main.CMD_ADD_USER_TO_GROUP; +import static cf.maybelambda.fedora.Main.CMD_DNF_AUTORM; +import static cf.maybelambda.fedora.Main.CMD_DNF_INST; +import static cf.maybelambda.fedora.Main.CMD_DNF_INST_REPOS; +import static cf.maybelambda.fedora.Main.CMD_DNF_MARK; +import static cf.maybelambda.fedora.Main.CMD_DNF_RM; +import static cf.maybelambda.fedora.Main.CMD_FLATPAK_INST; +import static cf.maybelambda.fedora.Main.CMD_FLATPAK_REMOTE_ADD; +import static cf.maybelambda.fedora.Main.CMD_GETENT; +import static cf.maybelambda.fedora.Main.CMD_RPM_IMPORT; +import static cf.maybelambda.fedora.Main.CMD_SYSTEMCTL_ENABLE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class MainTests { + private PostInstallUpdater mockUpdater = mock(PostInstallUpdater.class); + + private void setupConfigManager(MockedStatic cfg) { + cfg.when(ConfigManager::getDnfInstallPackages).thenReturn(List.of("pkg1")); + cfg.when(ConfigManager::getDnfRemovePackages).thenReturn(List.of("pkg2")); + cfg.when(ConfigManager::getFlatpakInstallPackages).thenReturn(List.of("flatpak1")); + cfg.when(ConfigManager::getRPMFusionGpgKeys).thenReturn(List.of("key1")); + cfg.when(ConfigManager::getRPMFusionRepos).thenReturn(List.of("repo1")); + cfg.when(ConfigManager::getFlatpakRemoteName).thenReturn("flathub"); + cfg.when(ConfigManager::getFlatpakRemoteUrl).thenReturn("https://flathub"); + cfg.when(ConfigManager::getAdminGroups).thenReturn(List.of("wheel")); + } + + private void simulateUserInput() { + // Simulate user input: answers to 9 prompts (y/y/empty/y/empty/y/empty/y/y) + String input = String.join("\n", "y", "y", "", "y", "", "y", "", "y", "y") + "\n"; + System.setIn(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void runExecutesCommandStructureAndSequenceInCorrectOrdering() { + try (MockedStatic cfg = mockStatic(ConfigManager.class)) { + setupConfigManager(cfg); + simulateUserInput(); + when(mockUpdater.runCommand(any(List.class), any(List.class))).thenReturn(0); + + Main.run(new String[]{}, mockUpdater); + + ArgumentCaptor> captorPrefix = ArgumentCaptor.forClass(List.class); + ArgumentCaptor> captorArgs = ArgumentCaptor.forClass(List.class); + Mockito.verify(mockUpdater, Mockito.atLeastOnce()).runCommand(captorPrefix.capture(), captorArgs.capture()); + List> prefixes = captorPrefix.getAllValues(); + List> args = captorArgs.getAllValues(); + int i = 0; + + assertEquals(CMD_RPM_IMPORT, prefixes.get(i)); + assertEquals(getRPMFusionGpgKeys(), args.get(i)); + i++; + assertEquals(CMD_DNF_INST_REPOS, prefixes.get(i)); + assertEquals(getRPMFusionRepos(), args.get(i)); + i++; + assertEquals(CMD_DNF_INST, prefixes.get(i)); + assertEquals(getDnfInstallPackages(), args.get(i)); + i++; + assertEquals(CMD_DNF_RM, prefixes.get(i)); + assertEquals(getDnfRemovePackages(), args.get(i)); + i++; + assertEquals(CMD_DNF_MARK, prefixes.get(i)); + assertTrue(args.get(i).isEmpty()); + i++; + assertEquals(CMD_DNF_AUTORM, prefixes.get(i)); + assertTrue(args.get(i).isEmpty()); + i++; + assertEquals(CMD_FLATPAK_REMOTE_ADD, prefixes.get(i)); + assertEquals(List.of(getFlatpakRemoteName(), getFlatpakRemoteUrl()), args.get(i)); + i++; + assertEquals(CMD_FLATPAK_INST, prefixes.get(i)); + assertEquals(List.of(getFlatpakRemoteName(), getFlatpakInstallPackages().getFirst()), args.get(i)); + i++; + assertEquals(CMD_GETENT, prefixes.get(i)); + assertEquals(getAdminGroups(), args.get(i)); + i++; + assertEquals(CMD_ADD_USER_TO_GROUP, prefixes.get(i)); + assertTrue(args.get(i).containsAll(getAdminGroups())); + i++; + assertEquals(CMD_SYSTEMCTL_ENABLE, prefixes.get(i)); + assertTrue(args.get(i).isEmpty()); + } + } + + @Test + void helpOptionDisplaysHelpTextAndExits() { + try (var filesMock = mockStatic(ConfigManager.class)) { + filesMock.when(() -> ConfigManager.readResourceLines(any(String.class))) + .thenReturn(List.of("Usage instructions go here")); + + Main.main(new String[]{"--help"}); + Main.main(new String[]{"-h"}); + + filesMock.verify(() -> ConfigManager.getHelpText(), Mockito.times(2)); + } + } +} diff --git a/src/test/java/cf/maybelambda/fedora/PostInstallUpdaterTests.java b/src/test/java/cf/maybelambda/fedora/PostInstallUpdaterTests.java index cd5a8e7..c5db766 100644 --- a/src/test/java/cf/maybelambda/fedora/PostInstallUpdaterTests.java +++ b/src/test/java/cf/maybelambda/fedora/PostInstallUpdaterTests.java @@ -1,82 +1,61 @@ package cf.maybelambda.fedora; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.CALLS_REAL_METHODS; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; class PostInstallUpdaterTests { + private PostInstallUpdater updater; + private ProcessBuilder mockBuilder; + + @BeforeEach + void setUp() { + this.updater = Mockito.spy(new PostInstallUpdater()); + this.mockBuilder = mock(ProcessBuilder.class); + } + @Test void runCommandReturnsStatusCodeOfCommandExecuted() throws Exception { Process mockProcess = mock(Process.class); when(mockProcess.getInputStream()).thenReturn(new ByteArrayInputStream("ok".getBytes(StandardCharsets.UTF_8))); when(mockProcess.waitFor()).thenReturn(0); - - ProcessBuilder mockBuilder = mock(ProcessBuilder.class); when(mockBuilder.start()).thenReturn(mockProcess); when(mockBuilder.redirectErrorStream(true)).thenReturn(mockBuilder); + Mockito.doReturn(mockBuilder).when(updater).createProcessBuilder(any(String[].class)); - try (MockedStatic updaterMock = Mockito.mockStatic(PostInstallUpdater.class, CALLS_REAL_METHODS)) { - updaterMock.when(() -> PostInstallUpdater.createProcessBuilder(any(String[].class))) - .thenReturn(mockBuilder); + int exitCode = updater.runCommand(asList("echo"), asList("test")); - int exitCode = PostInstallUpdater.runCommand(new String[]{"echo", "test"}); - - assertEquals(0, exitCode); - } + assertEquals(0, exitCode); } @Test void runCommandReturnsStatusMinusOneOnIOException() throws Exception { - ProcessBuilder mockBuilder = mock(ProcessBuilder.class); when(mockBuilder.start()).thenThrow(new IOException("Simulated I/O error")); + Mockito.doReturn(mockBuilder).when(updater).createProcessBuilder(any(String[].class)); - try (MockedStatic updaterMock = Mockito.mockStatic(PostInstallUpdater.class, CALLS_REAL_METHODS)) { - updaterMock.when(() -> PostInstallUpdater.createProcessBuilder(any(String[].class))) - .thenReturn(mockBuilder); - - int exitCode = PostInstallUpdater.runCommand(new String[]{"failing", "cmd"}); + int exitCode = updater.runCommand(asList("failing"), asList("cmd")); - assertEquals(-1, exitCode); - } + assertEquals(-1, exitCode); } @Test void runCommandSkipsExecutionInDryRunMode() { - PostInstallUpdater.setDryRun(true); - try (MockedStatic updaterMock = Mockito.mockStatic(PostInstallUpdater.class, CALLS_REAL_METHODS)) { - updaterMock.when(() -> PostInstallUpdater.createProcessBuilder(any(String[].class))) - .thenThrow(new AssertionError("Should not create ProcessBuilder in dry-run mode")); - - int exitCode = PostInstallUpdater.runCommand(new String[]{"fake", "cmd"}); - - assertEquals(0, exitCode); - } finally { - PostInstallUpdater.setDryRun(false); - } - } - - @Test - void helpOptionDisplaysHelpTextAndExits() { - try (MockedStatic filesMock = mockStatic(ConfigManager.class)) { - filesMock.when(() -> ConfigManager.readResourceLines(any(String.class))) - .thenReturn(List.of("Usage instructions go here")); + updater.setDryRun(true); + Mockito.doThrow(new AssertionError("Should not create ProcessBuilder in dry-run mode")) + .when(updater).createProcessBuilder(any(String[].class)); - PostInstallUpdater.main(new String[]{"--help"}); - PostInstallUpdater.main(new String[]{"-h"}); + int exitCode = updater.runCommand(asList("fake"), asList("cmd")); - filesMock.verify(() -> ConfigManager.getHelpText(), Mockito.times(2)); - } + assertEquals(0, exitCode); } }